Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
595d33f
feat(sdk): annotate_trace platform op (code half of the #4999 design)
mmabrouk Jul 3, 2026
936c160
docs(agent-workflows): approval-boundary workspace: flow explainer, b…
mmabrouk Jul 3, 2026
dac22b5
docs(approval-boundary): address review round 1 on how-approvals-work
mmabrouk Jul 3, 2026
6cb3b2b
docs(approval-boundary): fold in the PR #5054 loop diagnosis
mmabrouk Jul 3, 2026
c61701e
docs(approval-boundary): round 2: settle the permission model, add th…
mmabrouk Jul 3, 2026
29db193
docs(approval-boundary): settle runner.permissions naming and add the…
mmabrouk Jul 3, 2026
bbeb1d4
docs(approval-boundary): fold the Codex pre-implementation review (re…
mmabrouk Jul 3, 2026
827d4b3
feat(runner): permission wire types + decision core (approval-boundar…
mmabrouk Jul 3, 2026
99b7910
feat(runner): consult-first approval responder replaces park-by-defau…
mmabrouk Jul 3, 2026
2804526
feat(runner): relay enforces the shared permission plan; Pi gets rela…
mmabrouk Jul 3, 2026
ec9273d
feat(sdk): assemble and ship the permission plan; delete needs_approv…
mmabrouk Jul 3, 2026
cf551ed
feat(sdk): strip needs_approval from the platform op catalog (phase 3…
mmabrouk Jul 3, 2026
e9864fb
docs(approval-boundary): build notes for phase 3 + restack incident
mmabrouk Jul 3, 2026
8f4bbbf
feat(web): four-mode permission policy, Pi settings block, drop legac…
mmabrouk Jul 3, 2026
56170b9
feat(service): batch responses surface the paused state (phase 4a)
mmabrouk Jul 3, 2026
c73f5cd
chore(runner): delete legacy wire fields, rename acp-interactions + p…
mmabrouk Jul 3, 2026
b2f6394
docs(agent-workflows): sweep the old permission vocabulary; banner su…
mmabrouk Jul 3, 2026
fcb1dbb
test: cross-language permission-decision parity fixture (40 cases)
mmabrouk Jul 3, 2026
0cc133a
fix(sdk): drop unenforceable builtin tool permission loudly (QA finding)
mmabrouk Jul 3, 2026
81d9d50
fix(runner): a paused tool call's teardown frames no longer clobber t…
mmabrouk Jul 3, 2026
1cc9461
fix: reconcile the rebase onto big-agents (#5064/#5059)
mmabrouk Jul 4, 2026
a606bad
fix: address CodeRabbit review + pin the client-tool interaction row
mmabrouk Jul 4, 2026
c5baa03
docs(approval-boundary): status + build notes for the rebase, CodeRab…
mmabrouk Jul 4, 2026
f3a666f
fix(runner): stamp resolvedName on pause payloads; persist client-too…
mmabrouk Jul 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions docs/design/agent-workflows/documentation/adapters/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,25 @@ The first is static, written before the session starts: the claude adapter rende
delivered on the `harnessFiles` wire seam) whose `permissions.allow` / `permissions.ask` /
`permissions.deny` lists Claude Code reads via `settingSources`. Each backend-resolved EXECUTABLE
tool (callback/code) gets a per-tool rule `mcp__agenta-tools__<name>`, because that is how Claude
addresses a tool of the internal `agenta-tools` MCP server above. A tool whose
`effective_permission()` is `allow` gets an allow rule, so Claude runs it without raising a gate;
`deny` gets a deny rule; `ask` or unset gets no allow rule, so the gate still fires.

The second is dynamic: a gate that does fire arrives at `session.onPermissionRequest`, where the
runner answers for the (absent) human. When a human surface exists the runner parks the undecided
gate for a HITL approval round-trip; otherwise it falls to the per-run permission policy (auto or
deny). Pi does not need this hook because Pi does not gate tools this way.

Both layers are needed because Claude's gate fires BEFORE the runner relay that would otherwise
honor an `allow`. Without the settings.json rule, an `allow` resolved tool always parked
(finding F-046); the rule is what lets it run. The `ask`/unset path is left to the gate on
purpose, so HITL approval is preserved. Note that `permission_policy: "auto"` is NOT a blanket
bypass — it still means "gate, then HITL or policy", not "allow everything".
addresses a tool of the internal `agenta-tools` MCP server above. A tool whose effective
permission is `allow` (an explicit `allow`, or a read-hinted tool under the `allow_reads`
policy) gets an allow rule, so Claude runs it without raising a gate; `deny` gets a deny
rule; `ask` or unset gets no allow rule, so the gate still fires.

The second is dynamic: a gate that does fire arrives at `session.onPermissionRequest`, where
the runner's `ApprovalResponder` (`services/runner/src/responder.ts`) answers it. The verdict
comes from the shared decision module (`services/runner/src/permission-plan.ts`): the tool's
explicit permission wins, then a matching authored rule, then the policy mode in
`permissions.default`. `allow` approves the call, `deny` refuses it, and `ask` pauses the
turn, emits one approval request, and waits for a human. Pi never raises this hook; on Pi
the tool relay enforces the same decision function.

Both layers are needed because Claude's gate fires BEFORE the runner relay that would
otherwise honor an `allow`. Without the settings.json rule, an `allow` resolved tool always
paused (finding F-046); the rule is what lets it run. The `ask`/unset path is left to the
gate on purpose, so human approval is preserved. Note that `permissions.default: "allow"` is
not a per-tool override: a tool set to `ask` or `deny` keeps its own verdict, because the
explicit per-tool permission always beats the policy mode.

## Tracing from the event stream

Expand Down
55 changes: 33 additions & 22 deletions docs/design/agent-workflows/documentation/agent-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ The playground renders a single composite `agent_config` control. The field list
control is not hardcoded in the frontend. It is fetched from the backend catalog type
`agent_config`, which the SDK defines once as `AgentConfigSchema`. The runtime then re-parses
the same payload into one permissive `AgentConfig` (the run-selection fields `harness`,
`sandbox`, and `permission_policy` live on it), resolves tools and secrets server-side, and
hands a final wire request to the Node runner.
`sandbox`, and `runner.permissions.default` live on it), resolves tools and secrets
server-side, and hands a final wire request to the Node runner.

## Three objects share the name "AgentConfig"

Expand Down Expand Up @@ -69,12 +69,14 @@ existing control:
shape the prompt control uses.
- `mcp_servers` renders as a flat array. Each entry uses `McpServerItemControl`, which is a
JSON editor for one server entry.
- `harness`, `sandbox`, and `permission_policy` each render as an enum select.
- `harness`, `sandbox`, and the permission policy each render as an enum select. The
permission policy select has four modes: `allow`, `ask`, `deny`, `allow_reads` (the
default). It renders for Pi too; Pi now honors it the same way Claude does.

So the object the form produces is:

```
{ agents_md, model, tools[], mcp_servers[], harness, sandbox, permission_policy }
{ agents_md, model, tools[], mcp_servers[], harness, sandbox, runner: { permissions: { default } } }
```

The field set comes from the backend at runtime. The frontend fetches the catalog type with
Expand All @@ -100,7 +102,7 @@ Its fields and defaults:
| `mcp_servers` | `List[MCPServerConfig]` | empty list | typed |
| `harness` | `Literal["pi_core","claude","pi_agenta"]` | `"pi_core"` | enum |
| `sandbox` | `Literal["local","daytona"]` | `"local"` | enum |
| `permission_policy` | `Literal["auto","deny"]` | `"auto"` | enum |
| `runner.permissions.default` | `Literal["allow","ask","deny","allow_reads"]` | `"allow_reads"` | enum, four modes |

The schema is registered in `CATALOG_TYPES` under the key `"agent_config"`
(`sdks/python/agenta/sdk/utils/types.py:1132`). The API catalog imports `CATALOG_TYPES` from
Expand Down Expand Up @@ -142,7 +144,7 @@ class AgentConfig(BaseModel):
# the run-selection fields
harness: str = "pi_core"
sandbox: str = "local"
permission_policy: PermissionPolicy = "auto"
permission_default: PermissionMode = "allow_reads"
```

One correction to a common belief. This model is not `extra="allow"`. Its looseness comes
Expand All @@ -158,11 +160,18 @@ The genuinely loose object is the file-default dataclass at
built-in default, not user input.

The run-selection fields are on this neutral config too. `harness`, `sandbox`, and
`permission_policy` are plain fields on `AgentConfig` (in
`permission_default` are plain fields on `AgentConfig` (in
`sdks/python/agenta/sdk/agents/dtos.py`). They used to live on a separate `RunSelection`
object; that object is retired, because there is one agent definition, not an agent plus a
sidecar selection. The composite schema and the neutral config now agree: both keep these
fields next to the rest of the agent.
fields next to the rest of the agent. The authored form of the same value is
`runner.permissions.default`; the SDK flattens it onto `permission_default` when it parses
the template. The old three-name split (`runner.interactions.headless` authored,
`permission_policy` stored, `permissionPolicy` on the wire) is gone. The value now has four
modes (`allow`, `ask`, `deny`, `allow_reads`) instead of two (`auto`, `deny`), and the wire
field is `permissions: {default, rules?}`. Authors can also add `runner.permissions.rules`, a
list of `{pattern, permission}` entries for harness-builtin tools (for example
`Bash(rm:*)` set to `ask`). The runner checks rules before falling back to `default`.

Tool entries are strict even though the list is lenient. Each tool subclass is `extra="forbid"`
(`sdks/python/agenta/sdk/agents/tools/models.py`). `MCPServerConfig` is also `extra="forbid"`
Expand Down Expand Up @@ -191,9 +200,10 @@ resolves tools, MCP servers, and secrets server-side, bundles everything into a
reads `agent_config.sandbox` and passes it to `SandboxAgentBackend(sandbox=...)` instead.

The final wire shape the Node runner receives is `AgentRunRequest` in
`services/agent/src/protocol.ts` (around line 185). That is the true wired surface:
`services/runner/src/protocol.ts`. That is the true wired surface:
`harness`, `sandbox`, `agentsMd`, `systemPrompt`/`appendSystemPrompt`, `model`, `tools`
(builtin names), `skills`, `customTools`, `mcpServers`, `toolCallback`, `permissionPolicy`.
(builtin names), `skills`, `customTools`, `mcpServers`, `toolCallback`, and
`permissions: {default, rules?}`.

## Field-by-field: enforced vs loose, wired vs decorative

Expand All @@ -209,7 +219,7 @@ Legend: (a) catalog/schema, (b) SDK neutral config, (c) runtime.
| agents_md | yes, `agents_md: str` | yes, as `instructions` | wired to `agentsMd` | The schema names it `agents_md`. The neutral config names it `instructions`. |
| harness | yes, enum | yes, on `AgentConfig` | wired, picks the harness class | Enum-enforced. The runtime validates via `make_harness`. |
| sandbox | yes, enum | yes, on `AgentConfig` | wired to the backend, absent from `SessionConfig` | Backend concern, not agent identity. |
| permission_policy | yes, enum | yes, on `AgentConfig` | wired to `SessionConfig` | Only the Claude harness reads it. Pi ignores it, so it is decorative for `pi_core` and `pi_agenta`. |
| runner.permissions.default | yes, enum (4 modes) | yes, as `permission_default` on `AgentConfig` | wired to `SessionConfig` and the run request's `permissions.default` | Enforced for both harnesses: Claude at its settings file and the ACP responder, Pi at the tool relay. No longer decorative on Pi. |

## Notable gaps and quirks

Expand All @@ -219,13 +229,14 @@ list. `persona` is a forced append-system string. Neither appears in any schema,
appears on the neutral config, and the playground renders no control for either. Pi and
Claude harnesses get no forced skills or persona.

Per-harness divergence is real. `permission_policy` is wired only for Claude. Builtin tool
names are dropped for Claude with a warning, because builtins are Pi-only. Skills and persona
are Agenta-only. Pi's `system` and `append_system` overrides come through the
`harness_kwargs` escape hatch on the neutral config, which is itself absent from the schema.
Per-harness divergence is real in other ways, but not in permission enforcement anymore: the
permission policy is now enforced on both Claude and Pi. Builtin tool names are dropped for
Claude with a warning, because builtins are Pi-only. Skills and persona are Agenta-only. Pi's
`system` and `append_system` overrides come through the `harness_kwargs` escape hatch on the
neutral config, which is itself absent from the schema.

Harness, sandbox, and permission policy sit next to the agent definition in both the schema
and the neutral `AgentConfig`. The two agree on one control.
Harness, sandbox, and the permission policy sit next to the agent definition in both the
schema and the neutral `AgentConfig`. The two agree on one control.

## A concrete example config

Expand All @@ -248,15 +259,15 @@ This is what the playground saves and the runtime reads:
],
"harness": "pi_core",
"sandbox": "local",
"permission_policy": "auto"
"runner": { "permissions": { "default": "allow_reads" } }
}
```

With this config, the runtime reads `agents_md`, `model`, `tools`, `mcp_servers`, and the
run-selection fields `harness`, `sandbox`, and `permission_policy` through the one neutral
`AgentConfig`, resolves the tools and MCP servers server-side, and runs one turn on the Pi
harness in a local sandbox. The `permission_policy` value is ignored because the harness is
Pi, not Claude.
run-selection fields `harness`, `sandbox`, and `runner.permissions.default` through the one
neutral `AgentConfig`, resolves the tools and MCP servers server-side, and runs one turn on
the Pi harness in a local sandbox. Under `allow_reads`, `web_search` runs as a read with no
prompt; a write tool would pause for approval on this harness exactly as it would on Claude.

## See also

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Today the request surface includes:
- `parameters.agent.mcp_servers`
- `parameters.agent.harness`
- `parameters.agent.sandbox`
- `parameters.agent.permission_policy`
- `parameters.agent.runner.permissions.default` (four modes: `allow`, `ask`, `deny`, `allow_reads`)
- harness-specific options such as Pi prompt overrides

The runtime also has forced Agenta policy in `AgentaHarness`, but that content is
Expand Down
3 changes: 2 additions & 1 deletion docs/design/agent-workflows/documentation/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ Batch `/invoke` follows this path:

1. The workflow route calls `_agent` in `services/oss/src/agent/app.py`.
2. `_agent` parses one `AgentConfig` from request parameters; it carries the run-selection
fields `harness`, `sandbox`, and `permission_policy`.
fields `harness` and `sandbox`, plus the permission policy at
`runner.permissions.default`.
3. The service resolves three things independently: tools, MCP servers, and provider-key
secrets. MCP resolution is gated by `AGENTA_AGENT_ENABLE_MCP`
(`services/oss/src/agent/tools/resolver.py:23`, off by default).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ session representation and storage size.
## Config Ownership

`AgentConfig` describes the agent itself: instructions, model, tool references, MCP server
config, and per-harness option bags. It also carries the run-selection fields `harness`,
`sandbox`, and `permission_policy`; there is one agent config, not a config plus a separate
config, and per-harness option bags. It also carries the run-selection fields `harness` and
`sandbox`, plus the permission policy at `runner.permissions.default`; there is one agent
config, not a config plus a separate
`RunSelection` object. The handler reads `sandbox` to choose a backend, but the backend choice
itself is not stored on the config.

Expand Down
2 changes: 1 addition & 1 deletion docs/design/agent-workflows/documentation/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Request fields include:
| `messages` | Conversation history and current turn. |
| `secrets` | Provider env vars resolved by the service. |
| `tools`, `customTools`, `toolCallback`, `mcpServers` | Resolved tool delivery. |
| `permissionPolicy` | `auto` or `deny` for permission-gating harnesses. |
| `permissions` | Permission plan: `{default?, rules?}`. `default` is one of `allow`, `ask`, `deny`, or `allow_reads`; missing, it falls back to `allow_reads`, and a malformed block fails toward `ask`. `rules` is an optional list of `{pattern, permission}` entries for harness builtins. The runner enforces it on every harness. |
| `trace` | Trace context for nested spans. |

One-shot calls return one JSON result. Streaming calls use NDJSON internally: one
Expand Down
Loading
Loading