ToolContext
Context for tool execution. Contains information about who/where/what permissions for authorization.
ToolContext::__construct()
public function __construct(?string $principal = null, string $channel = 'unknown', array $scopes = [], ?string $request_id = null, ?string $ip = null, ?string $userAgent = null, array $extra = [], string $mode = 'execute'):Parameters
| Name | Type | Description |
|---|---|---|
| $principal | ?string | |
| $channel | string | |
| $scopes | array<string> | |
| $request_id | ?string | |
| $ip | ?string | |
| $userAgent | ?string | |
| $extra | array<string, mixed> | |
| $mode | string |
ToolContext::asPlan()
public function asPlan(): selfClone this context in plan mode (dry-run). Plan mode validates and authorizes but does NOT execute the tool callback.
ToolContext::isPlanMode()
public function isPlanMode(): boolCheck if context is in plan mode.
ToolContext::isExecuteMode()
public function isExecuteMode(): boolCheck if context is in execute mode.
ToolContext::hasScope()
public function hasScope(string $scope): boolCheck if context has a specific scope.
Parameters
| Name | Type | Description |
|---|---|---|
| $scope | string |
ToolContext::hasAnyScope()
public function hasAnyScope(array $scopes): boolCheck if context has any of the given scopes.
Parameters
| Name | Type | Description |
|---|---|---|
| $scopes | array<string> |
ToolContext::hasAllScopes()
public function hasAllScopes(array $scopes): boolCheck if context has all of the given scopes.
Parameters
| Name | Type | Description |
|---|---|---|
| $scopes | array<string> |
ToolContext::cli()
public static function cli(?string $requestId = null, string $mode = 'execute'): selfCreate context for a local shell, before anyone has proved who they are. The principal is `local-shell` and that is the honest name for it: not a person, a process that reached the machine. Scraping the OS for a name was the previous attempt and it was answering the wrong question — `id -u` reports which account the kernel attached to a process, which is a fact about the process and not a statement anyone made about an action. `SUDO_USER` is worse: an environment variable, and one command rewrites it. Identity on this surface arrives the same way it arrives for a release: somebody signs the thing. See {@see authorizedBy()}, and the channel policy that stops mutating calls from running without one.
Parameters
| Name | Type | Description |
|---|---|---|
| $requestId | (string | null) | Optional request ID |
| $mode | string | Execution mode: 'execute' or 'plan' |
ToolContext::tui()
public static function tui(?string $requestId = null, string $mode = 'execute'): selfEl shell TUI: la misma máquina y la misma persona que `cli`, en otra superficie. Canal propio y no un alias, por lo que ADR-0035 enseñó: `cli` era a la vez canal y principal hasta que tool-runtime 0.8 los separó, y mientras decían lo mismo nadie podía distinguir un cambio de significado de un cambio de nombre. El principal sigue siendo `local-shell` —quien tiene un shell tiene el TUI— y el canal dice desde DÓNDE se llamó, que es lo que decide cómo se pide consentimiento y cómo se pinta un resultado.
Parameters
| Name | Type | Description |
|---|---|---|
| $requestId | ?string | |
| $mode | string |
ToolContext::authorizedBy()
public static function authorizedBy(Milpa\ToolRuntime\Identity\VerifiedSigner $signer, array $scopes, ?string $requestId = null, string $mode = 'execute'): selfCreate context for a call a verified signature authorized. The principal is the key's fingerprint, so the audit line stops being the system's testimony about who acted and becomes a record a third party can re-check — against the same key that verifies this project's releases, fetched over WKD from the domain in its own address. `scopes` is deliberately not the wildcard. A signature authorizes the operation it names and nothing else, so the caller passes that operation's own requirements: the grant is exactly as wide as what was consented to.
Parameters
| Name | Type | Description |
|---|---|---|
| $signer | Milpa\ToolRuntime\Identity\VerifiedSigner | |
| $scopes | array<string> | the scopes of the operation that was signed for — never `['*']` |
| $requestId | ?string | |
| $mode | string |
ToolContext::stdio()
public static function stdio(string $requestId, string $principal = 'stdio', array $scopes = []): selfCreate context for a trusted local MCP stdio server process. ⚠️ **PROCESS-LEVEL TRUST.** This hard-codes `principal: 'stdio'` and the wildcard `['*']` scope by default — exactly the same "no real auth, but the channel police * accepts a hard-coded identity" shape {@see cli()} already uses for CLI scripts. It is only appropriate for a transport where the OS process boundary IS the authentication boundary: a local stdio MCP server that trusts whatever spawned it (e.g. an editor or agent runtime launching the binary as a child process), with no separate per-caller identity to authenticate over the wire. Do NOT use this for any MCP transport exposed over a network (HTTP+SSE, WebSocket, a shared multi-tenant socket, ...) where distinct callers are NOT process-trusted — build a {@see mcp()} context per authenticated caller instead. Exists because the `mcp` channel's built-in policy sets `require_auth: true` (see {@see \Milpa\ToolRuntime\PolicyGate}), so a bare `new ToolContext(channel: 'mcp')` (no `principal`) denies every call with "channel 'mcp' requires an authenticated principal" — the exact trap a no-auth stdio server falls into with no documented way out. `stdio()` is that documented way out, in one call instead of hand-rolling the same `principal: 'stdio', scopes: ['*']` workaround.
Parameters
| Name | Type | Description |
|---|---|---|
| $requestId | string | The MCP request ID |
| $principal | string | Opaque principal recorded for audit/logging — defaults to `'stdio'` since there is no real caller to authenticate |
| $scopes | array<string> | Scopes granted to this context — defaults to `['*']` (full access), matching the process-level trust model |
ToolContext::mcp()
public static function mcp(string $requestId, ?string $principal = null, array $scopes = [], string $mode = 'execute'): selfCreate context for an authenticated MCP server caller. **Not for a no-auth, process-trusted transport.** This is the factory for a real, per-caller identity on the `mcp` channel — `$principal` should be whatever your host actually authenticated (a user id, an API-token subject, ...), not a placeholder. If `$principal` is omitted it defaults to the literal string `'mcp'`, which satisfies {@see \Milpa\ToolRuntime\PolicyGate}'s `require_auth` check for the `mcp` channel (a non-empty string) but records every caller under the SAME fake identity — indistinguishable from each other in audit logs/DB policy rules keyed by principal. Passing an explicit empty string (`principal: ''`) is worse: `??` only substitutes on `null`, so `''` survives as-is and trips `PolicyGate::authorize()`'s `empty($ctx->principal)` check into a denied `AuthorizationResult` for every call on that context — a footgun that surfaces as a runtime authorization failure, not a construction-time error. For a local, no-auth stdio MCP server (the OS process boundary IS the trust boundary — e.g. an editor or agent runtime launching this as a child process), use {@see stdio()} instead: same `mcp` channel, but its `principal: 'stdio'` / `scopes: ['*']` defaults name that trust model explicitly instead of leaning on this factory's `'mcp'` placeholder. Separately, omitting `$scopes` still raises an `E_USER_DEPRECATED` notice (unchanged) — that warning is about scope hygiene, not about the principal trap described above; fixing one does not fix the other.
Parameters
| Name | Type | Description |
|---|---|---|
| $requestId | string | The MCP request ID |
| $principal | (string | null) | The authenticated principal (user/service) — pass a real, per-caller identity; do not rely on the `'mcp'` default or pass an empty string (see above) |
| $scopes | array<string> | Explicit scopes granted to this context |
| $mode | string | Execution mode: 'execute' or 'plan' |
ToolContext::telegram()
public static function telegram(string $chatId, ?string $userId = null, string $mode = 'execute'): selfCreate context for Telegram.
Parameters
| Name | Type | Description |
|---|---|---|
| $chatId | string | Telegram chat ID |
| $userId | (string | null) | Telegram user ID |
| $mode | string | Execution mode: 'execute' or 'plan' |
ToolContext::web()
public static function web(string $principal, array $scopes): selfCreate context for an authenticated web (HTTP) caller. The end of the faked wildcard on the web surface. Where {@see cli()} and {@see stdio()} encode *process-level* trust with a blanket `['*']`, a web caller is a real, per-request identity — so this carries the EXACT scopes the host verified, never a wildcard. A caller with no scopes gets no scopes, and the `web` channel's built-in policy in {@see \Milpa\ToolRuntime\PolicyGate} (`allow_all: false`, `require_auth: true`) will then deny any scope-protected tool. That denial is the point: the transport stops being trusted, the verified context is. **Decoupled on purpose.** This takes primitives — a principal id and a scope list — NOT a milpa/auth `Actor`. tool-runtime stays a leaf with zero dependency on the auth package; the HTTP host (e.g. the skeleton's HttpProjector), which already owns both, is what maps `Actor{id, scopes}` → `web($actor->id, $actor->scopes)`. The coupling lives up in the host that depends on both, never down here.
Parameters
| Name | Type | Description |
|---|---|---|
| $principal | string | the authenticated identity's id — must be non-empty (an empty principal trips {@see \Milpa\ToolRuntime\PolicyGate}'s `require_auth` check for the `web` channel) |
| $scopes | array<string> | the exact scopes the host verified for this caller — the real set the identity holds, never a blanket `['*']` |
ToolContext::toArray()
public function toArray(): arraySerialize this context to a plain array for logging or transport.