diff --git a/docs/design/agent-workflows/documentation/adapters/agenta.md b/docs/design/agent-workflows/documentation/adapters/agenta.md index e8d669920e..ab602ab3fc 100644 --- a/docs/design/agent-workflows/documentation/adapters/agenta.md +++ b/docs/design/agent-workflows/documentation/adapters/agenta.md @@ -5,9 +5,6 @@ adapter](pi.md) and produces a Pi-shaped config, so it inherits everything Pi do tools, the system-prompt layers, tracing). What it adds is a fixed set of Agenta-shipped extras that the agent author cannot turn off: -- **Forced tools**: always unioned into the agent's resolved tools. At minimum `read` - (Pi only renders the skills section when `read` is enabled) and `bash` (so skills can run - their helper scripts). - **Forced skills**: Agenta-shipped Pi skills loaded on every run. - **A base AGENTS.md preamble**: the author's `instructions` are appended after it. - **A base persona**: forced onto Pi's `append_system`, with any author-supplied @@ -25,15 +22,14 @@ The forced *policy* lives in the SDK harness layer, in one editable module: `SessionConfig`, exactly where `PiHarness` and `ClaudeHarness` do their own translation. The forced skill *files* live with the runner that runs Pi, under -`services/agent/skills//` (each a directory with a `SKILL.md`). Skills are real files on +`services/runner/skills//` (each a directory with a `SKILL.md`). Skills are real files on disk because they reference relative scripts and assets, so they cannot ride the wire as text. The contract between the two halves is the skill **name**: `AGENTA_FORCED_SKILLS` lists names, and each must match a committed directory under the runner's skills root. Because the Agenta harness IS Pi, its tools are delivered the Pi-native way (through the -extension on the ACP path), never over MCP. There is no forced tool set any more: the runner -activates all seven Pi built-ins on every Pi run, so `read` and `bash` are there without anything -forcing them. +extension on the ACP path), never over MCP. The runner activates all seven Pi built-ins on every +Pi run, so `read` and `bash` are there without additional configuration. ## How a skill reaches the model @@ -47,7 +43,7 @@ runner lays the bundled directories into the Pi agent dir. 2. `runSandboxAgent` resolves each name against its bundled `skills/` root (`engines/skills.ts`, override with `AGENTA_AGENT_SKILLS_DIR`) and writes the directories into the Pi agent dir's `skills/` (user scope). -3. Pi loads them, and because the forced `read` tool is enabled, surfaces them in the system +3. Pi loads them, and because the native `read` tool is active, surfaces them in the system prompt. The model reads a skill's `SKILL.md` on demand (progressive disclosure). ## Two prompt layers, kept distinct @@ -86,7 +82,7 @@ removed after the run. A plain `pi_core` run is unchanged (it installs only the the shared agent dir). The base AGENTS.md preamble rides the wire as `agentsMd` (written into the session `cwd`), and -the forced `read` / `bash` tools are Pi defaults under pi-acp. The persona rides the wire as +the active `read` / `bash` built-ins are Pi defaults under pi-acp. The persona rides the wire as `appendSystemPrompt` and the engine writes it into the per-run Pi agent dir as `APPEND_SYSTEM.md` (`engines/sandbox_agent/pi-assets.ts`), so Pi loads it on the run. Daytona skill uploads are UTF-8 text only (`writeFsFile` takes a string body); binary skill assets are diff --git a/docs/design/agent-workflows/documentation/tools.md b/docs/design/agent-workflows/documentation/tools.md index 8f133114ef..3d6b8b5386 100644 --- a/docs/design/agent-workflows/documentation/tools.md +++ b/docs/design/agent-workflows/documentation/tools.md @@ -103,7 +103,7 @@ because it is the seam between the two lives of a tool: The resolved specs are also defined in `tools/models.py` (`CallbackToolSpec`, `CodeToolSpec`, `ClientToolSpec`), and the matching TypeScript shape is `ResolvedToolSpec` in -`services/agent/src/protocol.ts`. A run bundles them as a `ResolvedToolSet`: the list of specs +`services/runner/src/protocol.ts`. A run bundles them as a `ResolvedToolSet`: the list of specs and one `ToolCallback` (the endpoint callback tools post back to). ## How tools get resolved (the service side) @@ -177,13 +177,13 @@ specs in `customTools`, the callback in `toolCallback`, and resolved MCP servers The runner has to hand resolved tools to a harness, and harnesses do not accept tools the same way. The runner branches on a capability, `mcpTools`, not on the harness name (the branch is -`buildSessionMcpServers` in `services/agent/src/engines/sandbox_agent/mcp.ts`). A harness that +`buildSessionMcpServers` in `services/runner/src/engines/sandbox_agent/mcp.ts`). A harness that reports it can take tools over MCP gets them that way; a harness that cannot gets them natively. Today that splits cleanly into two paths. - **Pi takes native tools.** Pi has an extension API, so the runner registers each resolved spec as a Pi tool directly. The bundled Pi extension - (`services/agent/src/extensions/agenta.ts`) reads the public specs from + (`services/runner/src/extensions/agenta.ts`) reads the public specs from `AGENTA_TOOL_PUBLIC_SPECS` and registers them from inside Pi, then Pi runs the tool body the runner gives it. Pi gets no MCP server at all here: `buildSessionMcpServers` returns an empty list for Pi, so neither the synthetic `agenta-tools` server nor any user MCP server is @@ -213,7 +213,7 @@ natively. Today that splits cleanly into two paths. [runner-to-MCP interface page](../interfaces/cross-service/runner-to-mcp-server.md). Both paths funnel execution through one function, `runResolvedTool` in -`services/agent/src/tools/dispatch.ts`. It is the single place that branches on `kind`, so how +`services/runner/src/tools/dispatch.ts`. It is the single place that branches on `kind`, so how a tool type executes is defined once, not three times. ## Execution, type by type @@ -225,7 +225,7 @@ picks the tool and supplies the arguments, who actually runs it, and where? Execution is a callback. The harness selects the tool and supplies arguments, but the runner does not run the integration. The tool body POSTs the call to Agenta's `POST /tools/call` -(`services/agent/src/tools/callback.ts`, `callAgentaTool`), sending the `call_ref` slug and +(`services/runner/src/tools/callback.ts`, `callAgentaTool`), sending the `call_ref` slug and the model's arguments in an OpenAI-style envelope. The API re-resolves the connection, runs the Composio action through the provider adapter (`execute_tool` in `core/tools/service.py`), and returns the result, which the runner hands back to the model verbatim. @@ -305,7 +305,7 @@ new endpoint and no hidden logic. It resolves to a `CallbackToolSpec` carrying a descriptor (`{method, path, body?, context?, args_into?}`) instead of a `call_ref`, so the runner calls the endpoint directly with the run's caller credential. There is no `/tools/call` hop. The SSRF guard binds the call to the run's own Agenta origin and confines it to the API mount -(`directCallUrl` in `services/agent/src/tools/direct.ts`); the same dispatch handles the Daytona +(`directCallUrl` in `services/runner/src/tools/direct.ts`); the same dispatch handles the Daytona relay path. The runner needs no platform-specific code — it dispatches any `call` opaquely (the branch already exists for reference tools). @@ -321,7 +321,7 @@ direct call. ### Code tools: the runner runs them locally Execution is a local subprocess inside the runner. `runCodeTool` -(`services/agent/src/tools/code.ts`) writes the snippet to a temp file, spawns `python3` or +(`services/runner/src/tools/code.ts`) writes the snippet to a temp file, spawns `python3` or `node`, passes the model's arguments as JSON on stdin, and reads the JSON result from stdout. There is no callback. The code runs where the harness runs. @@ -335,7 +335,7 @@ Node as `main(inputs)`. A non-zero exit or a timeout becomes a tool error so the continues rather than crashing the run. The production image ships the interpreters: the runner Dockerfile installs `python3` -(`services/agent/docker/Dockerfile`), and `node` is already present. An earlier missing +(`services/runner/docker/Dockerfile.gh`), and `node` is already present. An earlier missing `python3` made Python code tools fail with `spawn python3 ENOENT`; that is fixed. One real constraint remains: the child only has the interpreter and the tool's own secrets, with no package-install step and no `NODE_PATH` to the runner's modules. So a code tool is limited to @@ -383,7 +383,7 @@ member of `RenderHint` that asks the frontend to draw the connect widget. ### Built-in tools: the harness runs them natively, gated through the same relay Execution is the harness's own. A built-in tool is just a name. The runner adds it to the -session's allowlist and Pi runs its own implementation of `read`, `write`, `web_search`, and so +session's allowlist and Pi runs its own implementation of `read`, `write`, `bash`, and so on. Nothing is resolved and nothing is delivered. Note that built-ins are a Pi concept here; they are not delivered to non-Pi harnesses over ACP, which bring their own native tool set. @@ -413,8 +413,10 @@ parity test pins that copy against the same fixture. Because activation is unconditional, the seven names are reserved. Pi registers custom tools in the same registry as its builtins, so a custom tool named `read` would replace the builtin `read` -silently. `ToolResolver` refuses such a config with `ReservedToolNameError`, and the extension -skips a colliding spec rather than registering it. +silently. The SDK's `ToolResolver` refuses such a declared custom tool with +`ReservedToolNameError`, and the extension skips a colliding spec rather than registering it. The +runner also folds colliding names into the built-in identity when matching permissions on an +unvalidated `/run` payload, so the same defense applies at the execution boundary. The wire's `tools` field is deprecated. A current runner ignores it. The SDK still fills it with all seven names so a runner from before this change — which read it as a grant list — activates @@ -659,18 +661,18 @@ never drift from the files that exist. The canonical playbook format lives in th | Discovery endpoint + reserved-handler dispatch | `api/oss/src/apis/fastapi/tools/router.py` (`/tools/discover`, `_call_reserved_agenta_tool`) | | Server-side platform-op handlers (reserved-ref registry, `test_run`) | `api/oss/src/core/tools/platform_handlers.py` | | Build-kit overlay defaults (`DEFAULT_BUILD_KIT_OPS` + skill/tool embeds) | `api/oss/src/apis/fastapi/applications/overlay.py` | -| Wire contract | `services/agent/src/protocol.ts`, `sdks/python/agenta/sdk/agents/utils/wire.py` | -| Tool-delivery fork (branch on `mcpTools`) | `services/agent/src/engines/sandbox_agent/mcp.ts` | -| Runtime dispatch (branch on `kind`) | `services/agent/src/tools/dispatch.ts` | -| Callback transport | `services/agent/src/tools/callback.ts` | -| Code execution | `services/agent/src/tools/code.ts` | +| Wire contract | `services/runner/src/protocol.ts`, `sdks/python/agenta/sdk/agents/utils/wire.py` | +| Tool-delivery fork (branch on `mcpTools`) | `services/runner/src/engines/sandbox_agent/mcp.ts` | +| Runtime dispatch (branch on `kind`) | `services/runner/src/tools/dispatch.ts` | +| Callback transport | `services/runner/src/tools/callback.ts` | +| Code execution | `services/runner/src/tools/code.ts` | | Daytona/non-Pi relay (runner-side loop) | `services/runner/src/tools/relay.ts` | | In-sandbox relay writer + wire protocol | `services/runner/src/tools/relay-client.ts`, `relay-protocol.ts` | | Relay wake sources (local `fs.watch`, Daytona watch exec) | `services/runner/src/tools/relay-watch.ts` | -| Pi native delivery | `services/agent/src/extensions/agenta.ts` | +| Pi native delivery | `services/runner/src/extensions/agenta.ts` | | `agenta-tools` channel for non-Pi harnesses (local loopback HTTP) | `services/runner/src/tools/mcp-bridge.ts`, `services/runner/src/tools/tool-mcp-http.ts` | | `agenta-tools` channel on Daytona (in-sandbox stdio shim: entrypoint, env contract, upload) | `services/runner/src/tools/tool-mcp-stdio.ts`, `services/runner/src/tools/tool-mcp-env.ts`, `services/runner/src/engines/sandbox_agent/tool-mcp-assets.ts` | -| Capability probe | `services/agent/src/engines/sandbox_agent/capabilities.ts` | +| Capability probe | `services/runner/src/engines/sandbox_agent/capabilities.ts` | | Permission decision (shared by both gates) | `services/runner/src/permission-plan.ts` | | ACP responder (`ApprovalResponder`) | `services/runner/src/responder.ts` | | Tool relay enforcement | `services/runner/src/tools/relay.ts` | diff --git a/docs/design/agent-workflows/interfaces/README.md b/docs/design/agent-workflows/interfaces/README.md index d68eee88d9..968f54812a 100644 --- a/docs/design/agent-workflows/interfaces/README.md +++ b/docs/design/agent-workflows/interfaces/README.md @@ -45,11 +45,11 @@ page. `Status` is read from each page's prose: **stable** (wired and unlikely to | [`/inspect`](public-edge/workflow-inspect.md) | public | `agent/schemas.py`, `agent/app.py` (builtin-URI binding), `models/workflows.py`, `decorators/routing.py` | stable | `unit/agents/test_dtos_agent_config.py`, `unit/agent/test_builtin_uri_binding.py` | | [`/messages`](public-edge/agent-messages.md) | public | `adapters/vercel/{routing,messages,stream}.py`, `agentRequest.ts` | evolving (create-or-resume not observable until storage lands) | `utils/test_messages_endpoint.py`, `unit/agents/test_ui_messages.py` | | [Agent config schema](public-edge/agent-config-schema.md) | public | `agent/schemas.py`, `sdk/utils/types.py`, `agents/dtos.py` (`HARNESS_IDENTITIES`), `sdk/agents/pi_builtins.py` (`PI_BUILTIN_TOOL_NAMES`) | stable | `unit/agents/test_dtos_agent_config.py`, `unit/agents/test_harness_identity.py`, `unit/agents/test_pi_builtins_parity.py` + `golden/pi_builtin_tools.json`, `services/oss/tests/pytest/unit/agent/test_default_agent_template.py` | -| [`/run`](cross-service/service-to-agent-runner.md) | cross-service (the spine) | `protocol.ts`, `utils/wire.py`, `utils/ts_runner.py`, `server.ts`/`cli.ts` | stable (pinned by golden) | `unit/agents/test_wire_contract.py` + `golden/`, `services/agent/tests/unit/wire-contract.test.ts` | -| [Runner to harness](cross-service/runner-to-harness.md) | cross-service (ACP) | `engines/sandbox_agent.ts` + `sandbox_agent/{run-plan,capabilities,permissions}.ts` | evolving | `services/agent/tests/unit/sandbox-agent-*.test.ts` | +| [`/run`](cross-service/service-to-agent-runner.md) | cross-service (the spine) | `protocol.ts`, `utils/wire.py`, `utils/ts_runner.py`, `server.ts`/`cli.ts` | stable (pinned by golden) | `unit/agents/test_wire_contract.py` + `golden/`, `services/runner/tests/unit/wire-contract.test.ts` | +| [Runner to harness](cross-service/runner-to-harness.md) | cross-service (ACP) | `engines/sandbox_agent.ts` + `sandbox_agent/{run-plan,capabilities,permissions}.ts` | evolving | `services/runner/tests/unit/sandbox-agent-*.test.ts` | | [Runner to MCP server](cross-service/runner-to-mcp-server.md) | cross-service | `agents/mcp/`, `engines/sandbox_agent/{mcp,tool-mcp-assets,relay-guard}.ts`, `tools/{mcp-bridge,tool-mcp-http,tool-mcp-stdio,tool-mcp-env,relay,relay-client,relay-protocol,relay-watch}.ts` | evolving (internal channel delivered locally over loopback HTTP and on Daytona via the in-sandbox stdio shim, `client` tools included — a client call parks via a paused relay answer; user stdio disabled) | `services/runner/tests/unit/{mcp-servers,session-mcp-layering,tool-mcp-assets,tool-mcp-stdio,tool-relay-guard}.test.ts` | -| [Runner to tool callback](cross-service/runner-to-tool-callback.md) | cross-service | `tools/{callback,dispatch,direct}.ts`, `apis/fastapi/tools/router.py` (`/tools/call`, `/tools/discover`, `_call_reserved_agenta_tool`), `core/tools/{discovery,service,platform_handlers}.py`, `agent/tools/resolver.py` | evolving (the `call` descriptor is wired and platform ops emit it; the legacy `tools.agenta.find_capabilities` route is deleted; reserved refs now dispatch server handlers, resolution flag-gated off until the runner half lands) | `services/agent/tests/unit/{code-tool,extension-tools}.test.ts`, `api unit/tools/{test_workflow_tool_call,test_discovery,test_platform_handlers}.py`, `unit/agents/platform/test_op_catalog.py` | -| [Service and runner trace export](cross-service/service-and-runner-trace-export.md) | cross-service | `agent/tracing.py`, `tracing/otel.ts`, `extensions/agenta.ts` | stable | `services/agent/tests/unit/` | +| [Runner to tool callback](cross-service/runner-to-tool-callback.md) | cross-service | `tools/{callback,dispatch,direct}.ts`, `apis/fastapi/tools/router.py` (`/tools/call`, `/tools/discover`, `_call_reserved_agenta_tool`), `core/tools/{discovery,service,platform_handlers}.py`, `agent/tools/resolver.py` | evolving (the `call` descriptor is wired and platform ops emit it; the legacy `tools.agenta.find_capabilities` route is deleted; reserved refs now dispatch server handlers, resolution flag-gated off until the runner half lands) | `services/runner/tests/unit/{code-tool,extension-tools}.test.ts`, `api unit/tools/{test_workflow_tool_call,test_discovery,test_platform_handlers}.py`, `unit/agents/platform/test_op_catalog.py` | +| [Service and runner trace export](cross-service/service-and-runner-trace-export.md) | cross-service | `agent/tracing.py`, `tracing/otel.ts`, `extensions/agenta.ts` | stable | `services/runner/tests/unit/` | | [Service to vault and tool providers](cross-service/service-to-vault-and-tool-providers.md) | cross-service (external) | `agent/app.py`, `platform/{resolve,connections}.py`, `agents/capabilities.py`, `tools/router.py` | stable | `unit/agents/connections/`, `unit/agents/platform/`, `unit/agents/tools/` | | [Agent service handler](in-service/agent-service-handler.md) | in-service | `services/oss/src/agent/app.py` | stable | `services/oss/tests/pytest/unit/agent/` | | [Neutral runtime DTOs](in-service/neutral-runtime-dtos.md) | in-service | `agents/dtos.py` | stable | `unit/agents/test_dtos_*.py`, `test_harness_identity.py` | @@ -60,11 +60,11 @@ page. `Status` is read from each page's prose: **stable** (wired and unlikely to | [Tool models and resolution](in-service/tool-models-and-resolution.md) | in-service | `agents/tools/models.py`, `platform/{gateway,workflow,op_catalog,platform_tools}.py`, `agent/tools/resolver.py` | evolving | `unit/agents/tools/`, `unit/agents/platform/test_op_catalog.py` | | [MCP models and resolution](in-service/mcp-models-and-resolution.md) | in-service | `agents/mcp/{models,resolver,wire}.py` | evolving (stdio wired; remote deferred; resolution feature-gated) | `unit/agents/mcp/` | | [Model connection resolution](in-service/model-connection-resolution.md) | in-service | `agent/app.py`, `agents/connections/`, `platform/{resolve,connections}.py`, `agents/capabilities.py` | stable | `unit/agents/connections/` | -| [Runner engine internals](in-service/runner-engine-internals.md) | in-service (runner) | `server.ts`, `cli.ts`, `engines/sandbox_agent.ts` | stable | `services/agent/tests/unit/{server,cli}.test.ts` | -| [Permission responder](in-service/permission-responder.md) | in-service (runner) | `responder.ts`, `engines/sandbox_agent/permissions.ts` | stable | `services/agent/tests/unit/{responder,sandbox-agent-permissions}.test.ts` | -| [Sandbox permission](in-service/sandbox-permission.md) | in-service (runner) | `agents/dtos.py`, `protocol.ts`, `engines/sandbox_agent/{provider,run-plan}.ts` | evolving (network enforced on Daytona only; local rejected; filesystem nowhere) | `services/agent/tests/unit/{sandbox-agent-provider,sandbox-agent-run-plan}.test.ts` | +| [Runner engine internals](in-service/runner-engine-internals.md) | in-service (runner) | `server.ts`, `cli.ts`, `engines/sandbox_agent.ts` | stable | `services/runner/tests/unit/{server,cli}.test.ts` | +| [Permission responder](in-service/permission-responder.md) | in-service (runner) | `responder.ts`, `engines/sandbox_agent/permissions.ts` | stable | `services/runner/tests/unit/{responder,sandbox-agent-permissions}.test.ts` | +| [Sandbox permission](in-service/sandbox-permission.md) | in-service (runner) | `agents/dtos.py`, `protocol.ts`, `engines/sandbox_agent/{provider,run-plan}.ts` | evolving (network enforced on Daytona only; local rejected; filesystem nowhere) | `services/runner/tests/unit/{sandbox-agent-provider,sandbox-agent-run-plan}.test.ts` | -Paths are relative to the owner package (`sdks/python/agenta/sdk/`, `services/agent/src/`, +Paths are relative to the owner package (`sdks/python/agenta/sdk/`, `services/runner/src/`, `services/oss/src/`, `api/oss/src/`); test paths are relative to each package's pytest root unless prefixed. diff --git a/docs/design/agent-workflows/interfaces/in-service/harness-adapters.md b/docs/design/agent-workflows/interfaces/in-service/harness-adapters.md index 5e211ffaa8..2fd200e67f 100644 --- a/docs/design/agent-workflows/interfaces/in-service/harness-adapters.md +++ b/docs/design/agent-workflows/interfaces/in-service/harness-adapters.md @@ -28,14 +28,14 @@ Each adapter implements `_to_harness_config(...)` and emits a different `/run` w inline skill packages on the wire like the others; the runner materializes them under `.claude/skills` in the session cwd, matching Claude's project-local skill layout. - **`AgentaHarness`** runs on the same Pi engine but forces Agenta's opinion: it composes the - base instructions over the author's, forces the Agenta tool set, and layers the Agenta + base instructions over the author's, forces the Agenta skills and persona, and layers the persona into `append_system`. The wire shapes, side by side: | | Pi | Claude | Agenta | |---|---|---|---| -| built-in tools | yes | no | forced set | +| built-in tools | yes | no | yes | | custom tools | native | over MCP | native | | prompt overrides | `system`/`append_system` | none (reads `harness_kwargs`) | forced `append_system` + author `system` | | permission policy | carried, enforced by the relay | carried, enforced by settings + the responder | carried, enforced by the relay | diff --git a/docs/design/agent-workflows/interfaces/public-edge/agent-config-schema.md b/docs/design/agent-workflows/interfaces/public-edge/agent-config-schema.md index 2ea42a29b0..9824eeff48 100644 --- a/docs/design/agent-workflows/interfaces/public-edge/agent-config-schema.md +++ b/docs/design/agent-workflows/interfaces/public-edge/agent-config-schema.md @@ -29,6 +29,9 @@ The fields and the full schema follow. | `sandbox_permission` | `SandboxPermission \| null` | `null` (form pre-fills one) | The declared network and filesystem boundary. See [Sandbox permission](../in-service/sandbox-permission.md). | | `skills` | `(SkillConfig \| EmbedRef)[]` | `[]` (the playground overlay embeds the `build-an-agent` playbook) | Inline SKILL.md packages, or `@ag.embed` references the backend inlines before the runner sees them. | +For legacy compatibility, a saved `builtin` entry in `tools` is accepted, ignored with a warning, +and rendered nowhere. Pi's built-ins are always active and are no longer configured in this field. + Note that `harness`, `sandbox`, and `permissions` are the run-selection fields. They live on `AgentConfig` itself, under `data.parameters.agent`, and the handler reads them in the one `AgentConfig.from_params(...)` parse along with the rest of the config. There is one agent diff --git a/docs/design/agent-workflows/projects/default-agent-builtins/addendum-always-active.md b/docs/design/agent-workflows/projects/default-agent-builtins/addendum-always-active.md index d9e6f513b3..4513afbfe3 100644 --- a/docs/design/agent-workflows/projects/default-agent-builtins/addendum-always-active.md +++ b/docs/design/agent-workflows/projects/default-agent-builtins/addendum-always-active.md @@ -30,11 +30,12 @@ remember to carry the four entries along. The location was the problem, not the The runner activates all seven every time it starts Pi; Pi alone activates only four. Activation is unconditional platform behavior. Permission interception stays conditional on the policy. -2. Permission control for Pi happens only through the main policy +2. Permission control for the seven Pi built-ins happens only through the main policy (`runner.permissions.default`: `allow`, `allow_reads`, `ask`, `deny`) and the three editable - rule lists `harness.permissions.{allow, ask, deny}`. The default is unchanged: all three lists - empty, policy `allow_reads`, so `read`, `grep`, `find` and `ls` run unattended and `bash`, - `edit` and `write` show the approval card. + rule lists `harness.permissions.{allow, ask, deny}`. Custom tools may still carry an explicit + `permission` override. The default is unchanged: all three lists empty, policy `allow_reads`, + so `read`, `grep`, `find` and `ls` run unattended and `bash`, `edit` and `write` show the + approval card. 3. The Permissions drawer shows the three lists, editable. The author can pick any of the seven canonical names (`Read`, `Bash`, `Edit`, `Write`, `Grep`, `Find`, `Ls`) into allow, ask or deny, and pattern rules such as `Bash(npm run:*)` stay supported and visible. A grant made from