Admit operator SCV routing

160134fe14d2 · AtlantisPleb · · parent cfb0568c0e28

Admit operator SCV routing

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified lib/openagents/modules/routing_policy.ex
  • modified lib/openagents/tools/conversation_execution_context.ex
  • modified lib/openagents/turns/turn_server.ex
  • modified test/openagents/modules/router_test.exs
  • modified test/openagents/tools/conversation_execution_context_test.exs

Diff

5 files changed, +129 -15

lib/openagents/modules/routing_policy.ex modified +31

@@ -78,6 +78,37 @@ defmodule OpenAgents.Modules.RoutingPolicy do

78 78
    policy
79 79
  end
80 80
81
  @doc """
82
  Policy for an OpenAgents operator using capacity owned by OpenAgents.
83
84
  This policy admits the SCV module's external effect and residency into the
85
  routing stage. Execution still requires the module-specific operator receipt,
86
  and `OpenAgents.SCV.Deployments` independently verifies the operator again
87
  before it spends capacity.
88
  """
89
  @spec operator() :: t()
90
  def operator do
91
    {:ok, policy} =
92
      new(%{
93
        id: "sarah.routing.policy.operator.v1",
94
        allowed_residencies: [
95
          "application_postgres",
96
          "application_process",
97
          "host",
98
          "openagents_capacity",
99
          "operator_machine"
100
        ],
101
        allowed_approval_classes: [
102
          "exact_current_user_consent",
103
          "explicit_operator_approval",
104
          "host_policy"
105
        ],
106
        allowed_side_effects: ["external_effect", "read_only", "reversible_write"]
107
      })
108
109
    policy
110
  end
111
81 112
  @spec new(map()) :: {:ok, t()} | {:error, atom()}
82 113
  def new(attributes) when is_map(attributes) do
83 114
    base = default()
lib/openagents/tools/conversation_execution_context.ex modified +16

@@ -7,8 +7,10 @@ defmodule OpenAgents.Tools.ConversationExecutionContext do

7 7
  one surface cannot silently leave the other surface behind.
8 8
  """
9 9
10
  alias OpenAgents.Accounts
10 11
  alias OpenAgents.Accounts.User
11 12
  alias OpenAgents.Machines
13
  alias OpenAgents.Modules.RoutingPolicy
12 14
  alias OpenAgents.Repo
13 15
  alias OpenAgents.SCV.Deployments
14 16
  alias OpenAgents.Tools.ExecutionContext

@@ -28,6 +30,20 @@ defmodule OpenAgents.Tools.ConversationExecutionContext do

28 30
  @spec authorities() :: MapSet.t(String.t())
29 31
  def authorities, do: @authorities
30 32
33
  @doc "The routing policy admitted for a conversation owner."
34
  @spec routing_policy(String.t() | nil) :: RoutingPolicy.t()
35
  def routing_policy(user_id) when is_binary(user_id) do
36
    user = Repo.get(User, user_id)
37
38
    cond do
39
      Accounts.admin?(user) -> RoutingPolicy.operator()
40
      Machines.active_machine?(user_id) -> RoutingPolicy.paired_machine()
41
      true -> RoutingPolicy.default()
42
    end
43
  end
44
45
  def routing_policy(_user_id), do: RoutingPolicy.default()
46
31 47
  @doc "Build a tool execution context for a text or voice conversation."
32 48
  @spec build(map()) :: ExecutionContext.t()
33 49
  def build(
lib/openagents/turns/turn_server.ex modified +1 -10

@@ -11,7 +11,6 @@ defmodule OpenAgents.Turns.TurnServer do

11 11
    Machines,
12 12
    Modules.Lifecycle,
13 13
    Modules.Router,
14
    Modules.RoutingPolicy,
15 14
    Modules.RoutingReceipts,
16 15
    Preferences,
17 16
    ProfileMemory,

@@ -150,7 +149,7 @@ defmodule OpenAgents.Turns.TurnServer do

150 149
         current_usage: nil,
151 150
         usage: nil,
152 151
         tool_snapshot: tool_snapshot,
153
         routing_policy: routing_policy(owner),
152
         routing_policy: ConversationExecutionContext.routing_policy(owner.user_id),
154 153
         program_snapshot: program_snapshot,
155 154
         pending_tool: nil,
156 155
         tool_call_count: 0,

@@ -167,14 +166,6 @@ defmodule OpenAgents.Turns.TurnServer do

167 166
    end
168 167
  end
169 168
170
  defp routing_policy(owner) do
171
    if Machines.active_machine?(owner.user_id) do
172
      RoutingPolicy.paired_machine()
173
    else
174
      RoutingPolicy.default()
175
    end
176
  end
177
178 169
  defp current_user_text(messages) do
179 170
    messages
180 171
    |> Enum.reverse()
test/openagents/modules/router_test.exs modified +32 -4

@@ -3,7 +3,7 @@ defmodule OpenAgents.Modules.RouterTest do

3 3
4 4
  alias OpenAgents.Modules.{Router, RoutingPolicy, SurfacePolicy}
5 5
  alias OpenAgents.Provenance.Canonical
6
  alias OpenAgents.Tools.Registry
6
  alias OpenAgents.Tools.{Registry, ScvDeploy}
7 7
8 8
  test "identical intent, policy, and catalog select the same deterministic baseline" do
9 9
    snapshot = Registry.current!()

@@ -157,7 +157,7 @@ defmodule OpenAgents.Modules.RouterTest do

157 157
    assert decision.reason == "deterministic_baseline_selected"
158 158
  end
159 159
160
  test "machine-effect modules route under the paired-machine policy but not the default" do
160
  test "machine-effect modules route under paired-machine and operator policies" do
161 161
    snapshot = Registry.current!()
162 162
    artifact = Map.fetch!(snapshot.modules, {"sarah.tool.computer_run.v1", 1})
163 163
    proposal = reference(artifact, snapshot.digest)

@@ -181,11 +181,39 @@ defmodule OpenAgents.Modules.RouterTest do

181 181
                 "side_effect_policy_refused" in &1["reasons"])
182 182
           )
183 183
184
    assert {:ok, selected} =
185
             Router.route(snapshot, RoutingPolicy.paired_machine(), machine_input)
184
    for policy <- [RoutingPolicy.paired_machine(), RoutingPolicy.operator()] do
185
      assert {:ok, selected} = Router.route(snapshot, policy, machine_input)
186
      assert selected.status == "selected"
187
      assert selected.selected == proposal
188
    end
189
  end
190
191
  test "SCV deployment routes only under the operator policy" do
192
    assert {:ok, snapshot} = Registry.build([ScvDeploy])
193
    artifact = Map.fetch!(snapshot.modules, {"sarah.tool.scv_deploy.v1", 1})
194
    proposal = reference(artifact, snapshot.digest)
195
196
    scv_input =
197
      Map.merge(input(), %{
198
        required_capability: "scv.deploy",
199
        required_side_effect: "external_effect",
200
        authorities: MapSet.new(["scv.deploy"]),
201
        proposal: proposal,
202
        exact_proposal: true
203
      })
204
205
    for policy <- [RoutingPolicy.default(), RoutingPolicy.paired_machine()] do
206
      assert {:ok, refused} = Router.route(snapshot, policy, scv_input)
207
      assert refused.status == "refused"
208
      assert refused.selected == nil
209
      assert Enum.any?(refused.rejected, &("residency_refused" in &1["reasons"]))
210
    end
186 211
212
    operator_policy = RoutingPolicy.operator()
213
    assert {:ok, selected} = Router.route(snapshot, operator_policy, scv_input)
187 214
    assert selected.status == "selected"
188 215
    assert selected.selected == proposal
216
    assert {:ok, ^artifact} = Router.revalidate(selected, snapshot, operator_policy, scv_input)
189 217
  end
190 218
191 219
  test "an unadmitted routing program identity is rejected before it can propose" do
test/openagents/tools/conversation_execution_context_test.exs modified +49 -1

@@ -3,7 +3,9 @@ defmodule OpenAgents.Tools.ConversationExecutionContextTest do

3 3
4 4
  alias OpenAgents.AccountsFixtures
5 5
  alias OpenAgents.Conversations
6
  alias OpenAgents.Modules.SurfacePolicy
6
  alias OpenAgents.Machines
7
  alias OpenAgents.Modules.{Router, RoutingPolicy, SurfacePolicy}
8
  alias OpenAgents.Provenance.Canonical
7 9
  alias OpenAgents.Tools.{ConversationExecutionContext, Registry, ScvDeploy}
8 10
9 11
  setup do

@@ -38,6 +40,31 @@ defmodule OpenAgents.Tools.ConversationExecutionContextTest do

38 40
    assert {:ok, snapshot} = Registry.build([ScvDeploy])
39 41
    assert {:ok, artifact} = Registry.module_for_tool(snapshot, "scv_deploy", 1)
40 42
    assert :ok = SurfacePolicy.authorize_execution(artifact, voice)
43
44
    assert %RoutingPolicy{id: "sarah.routing.policy.operator.v1"} =
45
             policy = ConversationExecutionContext.routing_policy(user.id)
46
47
    proposal = %{
48
      "module_id" => artifact.module_id,
49
      "version" => artifact.version,
50
      "artifact_digest" => artifact.artifact_digest,
51
      "registry_digest" => snapshot.digest
52
    }
53
54
    route_input = %{
55
      intent_digest: Canonical.sha256("deploy an SCV"),
56
      required_capability: "scv.deploy",
57
      required_side_effect: "external_effect",
58
      surface: "text",
59
      data_scope: "browser_conversation",
60
      authorities: text.authorities,
61
      proposal: proposal,
62
      exact_proposal: true
63
    }
64
65
    assert {:ok, decision} = Router.route(snapshot, policy, route_input)
66
    assert decision.status == "selected"
67
    assert {:ok, ^artifact} = Router.revalidate(decision, snapshot, policy, route_input)
41 68
  end
42 69
43 70
  test "non-operators receive the shared authorities without an SCV receipt" do

@@ -57,6 +84,27 @@ defmodule OpenAgents.Tools.ConversationExecutionContextTest do

57 84
58 85
    assert {:error, :module_approval_required} =
59 86
             SurfacePolicy.authorize_execution(artifact, context)
87
88
    assert %RoutingPolicy{id: "sarah.routing.policy.default.v1"} =
89
             ConversationExecutionContext.routing_policy(user.id)
90
  end
91
92
  test "an active paired machine retains the paired-machine routing policy" do
93
    user = AccountsFixtures.repository_user_fixture("conversation-context-machine")
94
95
    {:ok, %{code: code}} =
96
      Machines.start_pairing(%{
97
        "name" => "context-machine",
98
        "tier" => "probe",
99
        "platform" => "linux-x64",
100
        "agent_version" => "0.1.0",
101
        "roots" => ["/workspace"]
102
      })
103
104
    assert {:ok, _machine} = Machines.approve_pairing(user, code)
105
106
    assert %RoutingPolicy{id: "sarah.routing.policy.paired-machine.v1"} =
107
             ConversationExecutionContext.routing_policy(user.id)
60 108
  end
61 109
62 110
  defp build(surface, conversation, user) do

This page updates live while a promote is in flight · changelog