# Runtime Outbound Tools

> Canonical path: `specs/daemon/runtime-outbound-tools.md`
> Scope: Desktop-owned outbound tool mechanisms that let local agent runtimes call back into AgentTeam through the daemon.

## Purpose

Agent runtimes need a controlled way to send messages, read context, mutate tasks, inspect daemon state, and access authorized provider or knowledge surfaces. Desktop provides those capabilities as outbound tool bridges from the runtime process into the daemon.

There are two transports:

- **MCP HTTP JSON-RPC** for runtime-native tools/list and tools/call integration. Ordinary AgentTeam agents use a single first-party `agentteam` MCP namespace for message, memory, and KB tools.
- **AgentTeam Exec CLI** for built-in first-party actions and authorized App-hosted
  Extension actions executed as `agentteam exec <domain-or-extension> <action>`
  through the runtime's local command capability.

Both transports are communication mechanisms. They do not own product semantics. Each tool call must resolve to a single product owner behind the bridge: channel, daemon management, provider/app actions, or knowledge retrieval.

[AgentTeam Token 消耗诊断](./token-optimization.md) 定义了
`agentteam token analyze` command。它是独立治理的 read-only local diagnostic
surface，不属于 AgentTeam Exec action 或 MCP tool，也不会自动注入 Agent Prompt。

## Invariants

- Every authenticated `tools/list` request sees the complete Desktop-owned MCP
  inventory for that route. Desktop does not apply runtime- or session-specific
  tool filtering.
- Each tool has one model-visible transport by static ownership and
  registration. Exec-owned actions are not registered as MCP tools; Desktop
  does not enforce transport ownership by hiding MCP schemas per session.
- Authorization is checked at the daemon boundary before dispatching the call to the product owner.
- 需要回复的入站消息只向 runtime 显示 exact target。MCP `message_send` 必须携带
  exact `target + content`；Desktop 做格式 canonicalization 和本地边界检查，
  Server/product API 负责实际 authorization。Task、review、schedule 等 Exec
  action 使用 typed request 的显式 locator，不从最近 delivery 或 active hint
  注入默认值。规范见
  [Turn-Target 回复路由](./turn-target-reply-routing.md)。
- Tool results are bounded at the final protocol boundary before returning to the runtime.
- Owner-private context IDs, scoped provider tokens, and daemon-local
  capability records must not be exposed as general model-visible identifiers.
  Runtime-injected transport tokens are secrets, not identifiers that a model
  should copy into tool input or output.
- Provider-app authorization remains server-side and must be revalidated for provider calls.
- Knowledge retrieval in an ordinary AgentTeam agent session is bound to the current
  delivery target and initiating Human message. Desktop forwards the opaque
  target key, target type, and message id from session metadata; AgentTeam Core must
  revalidate workspace ownership, scope type, message membership, Human
  authorship, and agent readability for `channel`, `dm`, `thread`, and `task`
  targets before forwarding to MeshX. These transport fields are not KB grants;
  MeshX remains the owner of KB and source authorization.
- Tool schemas and prompt guidance describe current product behavior, not implementation history or migration state.

## MCP Transport

`desktop/internal/wire/mcp` provides shared MCP infrastructure. It handles:

- HTTP routes, stream endpoints, and JSON-RPC request parsing;
- Bearer token validation through the daemon token manager;
- agent and server credential binding;
- JSON-RPC methods `initialize`, `ping`, `tools/list`, and `tools/call`;
- one complete Desktop-owned tool inventory for every runtime;
- final tool result budgeting through `desktop/internal/wire/mcp/resultbudget`.

MCP handlers implement `types.MCPHandler`:

```go
type MCPHandler interface {
    Tools() []MCPToolDef
    CallTool(ctx context.Context, agentID, name string, args json.RawMessage) *MCPToolResult
    SkillDoc(ToolNamer) string
    SkillDocSplit(ToolNamer) SkillDocParts
}
```

`Tools` is the complete runtime-visible inventory. Local transport checks and
product authorization are enforced by `CallTool`, the only execution entry point;
Desktop does not hide tools to represent permissions.

### MCP Routes

Desktop serves these MCP route families from the local API server:

| Route family | Owner | Runtime-visible purpose |
| --- | --- | --- |
| `/api/internal/mcp/servers/{serverId}/agents/{agentId}` | AgentTeam handler selected after credential validation | Ordinary agent credentials receive the `agentteam` inventory for that server and agent binding. |
| `/mcp/agentteam/{serverId}/{agentId}` | Legacy alias | Uses the same authenticated handler and inventory as the canonical route. |

The local API router authenticates first, retains immutable token information in
the request context, and resolves the handler only after the credential's agent
and server binding matches the route. Token-analysis and unknown scopes are
rejected on this route. Missing or mismatched bindings fail closed.

### Runtime Tool Names

MCP handlers own raw tool names such as `message_send`, `message_history_read`, or `memory_search`. Runtime drivers may map those names into runtime-specific model-visible names:

- Claude/Codex style: `mcp__agentteam__message_send`;
- OpenCode style: `mcp_agentteam_message_send`.

Drivers translate model-visible names back to `{server, raw tool}` before dispatch. Product rules must be written against raw tool semantics, not a single runtime's display convention.

### Codex delegated-child effect gate

Codex app-server 的两类 AgentTeam tool-call transport 都携带 provider thread id；driver
同时持有 primary thread id。在发起 AgentTeam MCP HTTP 请求前：

- primary thread 正常继续；
- 被明确识别为 non-primary 的 child 只允许调用可信 AgentTeam `tools/list` inventory
  中 `annotations.readOnlyHint=true` 的工具；
- annotation 缺失、false 或 inventory 中未知的工具一律按 write/effectful
  fail closed，错误只要求 child `return result to leader`；
- 分类来自 daemon 拉取的 inventory，不读取模型参数，不维护手写 write denylist。

该首批边界覆盖 Codex native MCP 调用；AgentTeam Exec credential 仍属于整个 OS
process，Claude/Kimi/OpenCode 也尚无同等 per-call child identity。不得把本边界
描述为 runtime-neutral child-scoped capability；后续需要 daemon-signed
child credential/call proof。

### Prompt Integration

Each handler provides `SkillDoc` and `SkillDocSplit` so the prompt compiler can
render stable tool-use guidance. Prompt text must match the complete MCP
inventory registered for the route; transport ownership is resolved statically,
not through session-specific prompt or schema filtering.

## AgentTeam Exec CLI Transport

The AgentTeam Exec CLI transport is a daemon-owned command path for built-in
first-party actions and authorized App-hosted Extension actions:

```text
agentteam exec <domain-or-extension> <action> -
```

The command is registered only for daemon-launched runtime processes with a
valid injected base URL and credential source:

- `AGENTTEAM_EXEC_BASE_URL`: the canonical process authorization root
  `/api/internal/exec/servers/{serverID}/agents/{agentID}`;
- `AGENTTEAM_EXEC_TOKEN_FILE`: the path to an OS-account-private file containing the
  current signed process credential. Its random parent and file use
  owner-only `0700/0600` modes on Unix and a protected current-user/SYSTEM DACL
  on Windows.

New daemon launches inject only the base URL and token-file path. They remove
any inherited `AGENTTEAM_EXEC_TOKEN` instead of placing a bearer credential directly
in the runtime environment. The CLI does not accept a raw-token fallback.
Canonical and retired Exec/Extension variables are daemon-owned and compared
case-insensitively. A daemon started inside another AgentTeam runtime clears the
parent's variables from its own process before it starts probes or helpers, and
runtime/probe environment assembly strips ambient, shell-captured, and
mixed-case overrides before appending a newly authorized runtime's canonical
base/file pair.

For each invocation, the CLI reads the declared token file exactly once and
sends exactly one JSON request to
`POST ${AGENTTEAM_EXEC_BASE_URL}/{domain}/{action}`. There is no authorize/execute
two-step, one-shot lease, request hash binding, wildcard action endpoint, or
transport-generated target. A declared file that is missing, unreadable, empty,
or invalid fails closed without falling back to `AGENTTEAM_EXEC_TOKEN`. Manual
execution without a valid injected base URL and credential source also fails
closed.

One logical Exec runtime authorization belongs to one runtime process and lives
for that process's lifetime. Its immutable `RuntimeCatalog` combines the fixed
built-in descriptor Catalog with the verified Extension snapshot. The
authorization binds the server, agent, session, run, workdir, process generation
and Catalog snapshot. It does not store a launch target or conversation routing
context; action locators remain explicit and are authorized by their owning
Server API.
The bearer token stored in the private file is a short-lived generation of that
authorization, not the authorization identity:

- each generation has a bounded TTL of 12 hours by default;
- before expiry, the daemon issues a new generation and atomically replaces the
  private token file without restarting the runtime;
- publishing a new generation does not extend its predecessor: an old
  generation remains usable only until its original expiry;
- issuance or publication failure leaves the current file and still-valid
  generation intact; a failed replacement must not revoke the credential the
  runtime is currently using;
- process exit, replacement, launch abort, or daemon shutdown stops renewal,
  removes the credential file, revokes every generation of the logical
  authorization,
  and drains in-flight requests. Authorization is revoked before file cleanup;
  transient cleanup failures remain daemon-owned cleanup debt and are retried
  without restoring access. Strong local shutdown waits for file removal and
  request drain. If bounded daemon shutdown expires or the daemon crashes, the
  next singleton daemon removes stale credential directories before starting
  services.

Authorization admission counts revoked-but-undrained cleanup debt as well as
active records, so replacements cannot accumulate unbounded immutable
snapshots. Each authorization admits at most 32 acquired requests and the store
at most 1024 acquired requests globally; an otherwise authenticated request
above either limit is rejected with a bounded conflict response before its body
is read.
Dispatcher admission separately bounds the total of active runtime leases,
concurrent pre-publication activations, and leases whose credential cleanup is
still retrying to 512. Cleanup failures therefore remain discoverable without
creating unbounded retry owners; retries use capped exponential backoff.

Renewal is daemon-local credential maintenance. It neither depends on nor treats
an authoritative `agent:start` command as a credential renewal. Such a command
may replace the runtime process and therefore its logical authorization, but
ordinary token rotation does not restart the agent.

This credential-file boundary protects against other OS accounts. AgentTeam Agents
and runtime processes executing as the same login user are inside the same local
trust boundary; these file permissions do not provide per-process or per-Agent
isolation within that account.

The daemon owns:

- canonical `agentteam` binary path selection;
- runtime environment injection and private credential-file lifecycle;
- one daemon-owned `RuntimeAuthorizationStore`, whose process-scoped records
  bind each signed token generation to the logical authorization and immutable
  `RuntimeCatalog`;
- proactive token-generation rotation while preserving the logical
  authorization;
- authorization replacement/exit revocation and in-flight request drain;
- one merged Catalog route and one dynamic action route:
  `GET /api/internal/exec/servers/{serverID}/agents/{agentID}/catalog` and
  `POST /api/internal/exec/servers/{serverID}/agents/{agentID}/{domain}/{action}`;
- authentication and path binding before body reads, followed by the 1 MiB body
  cap under a five-second absolute local read deadline, strict JSON decoding,
  descriptor schema validation, then the typed built-in concrete handler or the
  Extension forwarder. The request context is
  canceled when its logical authorization is revoked, and the adapter fences
  body-read completion, handler/upstream dispatch, and success publication
  against that signal;
- `DefaultDescriptorCatalog()` as the descriptor-only source for CLI help,
  input/output schemas, examples, input validation, OpenAPI, and Dispatcher
  prompt summary; the prompt summary is a mixed index: it points to the
  root/domain/action help hierarchy instead of eagerly listing every action,
  while keeping canonical examples for the common task lifecycle and
  `review add` inline so those paths do not require a separate help turn; the
  catalog has no handlers or execution hooks;
- static separation between the Exec Catalog and MCP registration, so Exec-owned
  domains are not added to the MCP inventory;
- typed success/error envelopes, final result budgeting, and fallback
  classification.

Authentication failures exposed to the runtime remain a bounded generic `403`
response and must not reveal whether a credential was expired,
revoked/inactive, or valid for a different scope. Rate-limited daemon-internal
diagnostics retain those classifications so operators can distinguish expiry
from lifecycle revocation/inactivity and scope mismatch without logging bearer
material.

AgentTeam Exec CLI is not a general shell capability. The runtime may execute the command through its local command tool, but Desktop must not scan assistant prose, fenced code blocks, or arbitrary shell output to trigger tool execution.

Built-in actions retain one typed `RegisterJSON[I,O]` adapter behind the dynamic
route map. After authorization and validation, the adapter invokes the
corresponding concrete handler method with `ActionScopedJSON[I]`; the handler
uses the request's immutable authorized server or a narrow local service and
returns `ActionResult[O]`. Extension actions have descriptor-only entries in the
same `RuntimeCatalog`; Desktop injects the pinned Definition digest and Catalog
version from the authorization and forwards to AgentTeam Server `/runtime/ext`.

Built-in registrations declare one typed request and concrete handler. The
dynamic route performs strict decode and descriptor schema validation before
calling that handler. Action locators are validated by the owning handler and
Server API; query filters and destructive resource locators never receive an
ambient delivery fallback, and handlers do not read a process-launch target.

`agentteam exec <domain-or-extension> <action> --help` renders concise usage and one
canonical example.
`--input-schema` and `--output-schema` return standalone machine-readable
schemas; `agentteam exec --error-schema` returns the shared error schema. Execution
accepts one inline JSON value or `-` for stdin; actions whose input accepts `{}`
may omit the payload. The CLI validates against the same
descriptor before forwarding and prints the bounded JSON envelope returned by
the daemon.

## Tool Ownership

| Tool family | Product owner behind the bridge | Notes |
| --- | --- | --- |
| Messages, history, reactions, attachments | AgentTeam composite MCP / AgentTeam server APIs | Must preserve exact AgentTeam target routing and bounded reads. |
| Tasks, agent list/get/management, review, schedules, workspace/profile/workflow/project/approval | AgentTeam Exec CLI / AgentTeam server APIs | Must expose concise action help and standalone `--input-schema` / `--output-schema`. |
| Provider PR watches | `agentteam exec github|gitea <action>` plus server-side App/Extension authorization | Agent-visible management uses the flattened Extension Catalog. Desktop has no built-in `pr-watch` domain or provider-app runtime bridge. |
| Knowledge retrieval | agentteam MCP composite handler plus AgentTeam/MeshX server-side KB authorization | The stable `kb_search` and `kb_fetch` tools are always advertised; each call is bound to the current Human-initiated turn and reauthorized server-side. |
| Memory full-text search | agentteam MCP composite handler and `desktop/internal/memorysearch` owner | Locator-only retrieval over the bound managed Memory clone. The stable `memory_search` tool surface and `memory-maintenance` runtime skill are injected together for ordinary interactive runtimes through the MCP-owned tool inventory; index unavailability fails at call time. |

A new tool should be added to the owning family rather than creating a new outbound bridge. Add a new route family only when the token scope, lifecycle, or product owner is genuinely different.

## Bounded non-Workflow collection reads

The first-party Exec collection actions that can grow without a Workflow
budget use server-side keyset pagination rather than client-side truncation:

- `project list` orders by `updatedAt DESC, id ASC`;
- `team list` orders by `displayName ASC, handle ASC, teamId ASC`;
- `review list` orders by `updatedAt DESC, id DESC` within the resolved thread;
- `scheduled-message runs` orders by the effective
  `COALESCE(attemptedAt, createdAt) DESC, id DESC` key within the resolved
  schedule; the public `attemptedAt` field carries that effective timestamp for
  unattempted rows.

Each page is bounded to a default of 20 items and a maximum of 100 items. The
response carries the compact collection field plus `hasMore` and an opaque
`nextCursor` only when another page exists. Empty and final pages carry an
empty collection and `hasMore: false`. The local `agent list` projection is
ordered by stable Agent ID and its `a2` cursor binds the normalized query and
an owning-profile collection revision; add/update/delete/reload/orphan
cleanup invalidates the revision. The profile index copies only
limit-plus-one matching configurations and resolves only the page's active
sessions. A deleted/renamed anchor, stale revision, malformed token, or filter
mismatch is a typed validation failure, never page one.

Exec Agent `list`/`get` intentionally expose a safe public projection. They may
return API-key-configured and normalized configured-header names, but never
header values, BaseURL/endpoint userinfo/query/fragment, source IDs, or raw
policy allowed paths; filesystem policy is represented only by a bounded count.
The change is an intentional narrowing of the released Exec response to keep
provider credentials and local filesystem details out of model-visible JSON.

Mutation actions that carry `requestId` or `idempotencyKey` forward the same
bounded selector as `Idempotency-Key` and `X-Request-ID`. The daemon rejects
body/header disagreement, conflicting dual selectors, malformed/oversized
selectors, and ambiguous applied receipts before dispatch; only transports
with a server receipt use bounded transient replay. Natural-key mutations
remain non-retried and direct callers must inspect the authoritative resource.

The Server rejects malformed,
non-canonical, or filter-mismatched cursors with the normal bounded validation
error envelope; it never treats an invalid cursor as a request for page one.
Cursors encode the complete keyset boundary and any stable filter binding, so
follow-up requests preserve deterministic ordering without exposing storage
rows or secrets. Existing small calls remain valid because the pagination
metadata is additive and resource identifiers needed by subsequent get,
update, delete, or next actions remain in the projections.

## Exec/Server JSON Casing

First-party Exec/daemon-to-server JSON contracts use `lowerCamelCase`. Public
Exec schemas, canonical server DTOs, future server OpenAPI, and model-visible
Exec output describe only that canonical form.

Compatibility for a released daemon belongs behind the receiving server
handler or the explicitly private Agent Runtime HTTP transport. A handler may
accept documented legacy snake_case request aliases during migration. When an
old daemon also requires response aliases, that private transport may carry
canonical fields and the minimum safe aliases in the same response. The aliases
must be projections of the same compact canonical DTO; they must not be added
to the canonical DTO, advertised in OpenAPI, survive the typed Desktop
projection into model-visible output, or be derived from raw domain/storage
records. In particular, compatibility must not expand a compact result with
internal target, scope, binding, installation, connection, member, actor, or
correlation refs.

This rule does not bulk-rename provider-owned tool identities, internal
persistence keys, or the generic MCP contracts documented elsewhere. Each such
surface keeps its owning contract until a separate migration is specified.

## Result Boundaries

Every MCP `tools/call` result returned by Desktop must pass through the product result budget defined in [Agent MCP Result Budget Contract](./agent-mcp-result-budget.md). Raw proxied app/provider results must be normalized and capped before they are returned to the runtime.

CLI tool results must enforce the equivalent semantic boundary before printing the command envelope. The canonical built-in action risk/result-policy inventory is [`agentteam-exec-action-inventory.json`](./agentteam-exec-action-inventory.json); catalog construction and route registration fail closed when a registered action lacks a row. The shared Exec success boundary is 64 KiB after JSON serialization.

A complete typed success envelope at or below 64 KiB is returned unchanged. If serialization fails or the complete envelope exceeds 64 KiB, Exec never truncates or replaces `data` with a generic object. Read-only actions return the stable `result_budget_exceeded` error with action-specific `hint`/`next` continuation guidance from the inventory; cursor/page/workflow actions continue through their declared cursor or section operation, while bounded and narrow actions use a smaller or more specific request. Workflow's nested `plan`, `trigger`, `run`, and `step` operation unions use one closed operation matrix: list/show/version/validate, list/show/validate, list/show/logs, and list/get are reads; create/update/archive/publish, create/update/enable/disable/archive, start/cancel, and complete/fail/pause/park are side effects. A mutation response whose complete result serializes successfully but may already have committed instead returns the same stable error with `metadata.mutation_applied=true` and a compact closed `metadata.mutation_receipt` containing only deterministic allowlisted selectors, revision, idempotency, and a retrieval path when its target schema is complete; it has no replay advice. A mutation serialization/custom-marshaler failure cannot produce a trustworthy receipt, so it fails closed without `mutation_applied` and without replay advice. The local daemon client strictly re-projects only this closed metadata contract, rebuilding retrieval paths from validated catalog inputs rather than trusting wire paths; malformed, unknown, or oversized metadata is discarded. Private serialization causes and payload content never cross the boundary. The MCP transport body limit is not a product result contract.

## Failure Model

- Authentication failure returns an explicit denied/error result and must not dispatch to the product owner.
- An unknown tool fails closed at `CallTool`; registered tools enforce local
  transport checks and product authorization at that same call boundary.
- Provider-app proxy failures must distinguish provider/app authorization failures from ordinary provider API failures.
- The AgentTeam Core Knowledge proxy must return a structured outer failure stage so
  callers can distinguish a missing AgentTeam route, request validation,
  target-scope resolution, and MeshX upstream failures without inferring the
  failing hop from the outer `/agent-thread/upstream` route.
- Search/readiness gaps should return a classified tool result that tells the runtime what narrower or fallback action is available.
- MCP schema changes become visible when a runtime refreshes `tools/list`;
  authorization and target/product checks are enforced on every `tools/call`.

## Related Specs

- Desktop architecture: [AgentTeam Desktop Architecture](./desktop-architecture.md)
- First-party daemon contract: [AgentTeam Daemon](./daemon.md)
- Exact-target 回复路由：[Turn-Target 回复路由](./turn-target-reply-routing.md)
- MCP result budget: [Agent MCP Result Budget Contract](./agent-mcp-result-budget.md)
- Prompt compiler: [Prompt Compiler Architecture](./prompt-compiler-architecture.md)
- Delivery runtime: [Delivery Runtime](../message/delivery-runtime.md)
