Skip to content
docsv0.12.0

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

Parameters of __construct()
NameTypeDescription
$principal?string
$channelstring
$scopesarray<string>
$request_id?string
$ip?string
$userAgent?string
$extraarray<string, mixed>
$modestring

ToolContext::asPlan()

public function asPlan(): self

Clone this context in plan mode (dry-run). Plan mode validates and authorizes but does NOT execute the tool callback.

ToolContext::isPlanMode()

public function isPlanMode(): bool

Check if context is in plan mode.

ToolContext::isExecuteMode()

public function isExecuteMode(): bool

Check if context is in execute mode.

ToolContext::hasScope()

public function hasScope(string $scope): bool

Check if context has a specific scope.

Parameters

Parameters of hasScope()
NameTypeDescription
$scopestring

ToolContext::hasAnyScope()

public function hasAnyScope(array $scopes): bool

Check if context has any of the given scopes.

Parameters

Parameters of hasAnyScope()
NameTypeDescription
$scopesarray<string>

ToolContext::hasAllScopes()

public function hasAllScopes(array $scopes): bool

Check if context has all of the given scopes.

Parameters

Parameters of hasAllScopes()
NameTypeDescription
$scopesarray<string>

ToolContext::cli()

public static function cli(?string $requestId = null, string $mode = 'execute'): self

Create 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

Parameters of cli()
NameTypeDescription
$requestId(string | null)Optional request ID
$modestringExecution mode: 'execute' or 'plan'

ToolContext::tui()

public static function tui(?string $requestId = null, string $mode = 'execute'): self

El 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

Parameters of tui()
NameTypeDescription
$requestId?string
$modestring

ToolContext::authorizedBy()

public static function authorizedBy(Milpa\ToolRuntime\Identity\VerifiedSigner $signer, array $scopes, ?string $requestId = null, string $mode = 'execute'): self

Create 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

Parameters of authorizedBy()
NameTypeDescription
$signerMilpa\ToolRuntime\Identity\VerifiedSigner
$scopesarray<string>the scopes of the operation that was signed for — never `['*']`
$requestId?string
$modestring

ToolContext::stdio()

public static function stdio(string $requestId, string $principal = 'stdio', array $scopes = []): self

Create 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

Parameters of stdio()
NameTypeDescription
$requestIdstringThe MCP request ID
$principalstringOpaque principal recorded for audit/logging — defaults to `'stdio'` since there is no real caller to authenticate
$scopesarray<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'): self

Create 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

Parameters of mcp()
NameTypeDescription
$requestIdstringThe 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)
$scopesarray<string>Explicit scopes granted to this context
$modestringExecution mode: 'execute' or 'plan'

ToolContext::telegram()

public static function telegram(string $chatId, ?string $userId = null, string $mode = 'execute'): self

Create context for Telegram.

Parameters

Parameters of telegram()
NameTypeDescription
$chatIdstringTelegram chat ID
$userId(string | null)Telegram user ID
$modestringExecution mode: 'execute' or 'plan'

ToolContext::web()

public static function web(string $principal, array $scopes): self

Create 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

Parameters of web()
NameTypeDescription
$principalstringthe authenticated identity's id — must be non-empty (an empty principal trips {@see \Milpa\ToolRuntime\PolicyGate}'s `require_auth` check for the `web` channel)
$scopesarray<string>the exact scopes the host verified for this caller — the real set the identity holds, never a blanket `['*']`

ToolContext::toArray()

public function toArray(): array

Serialize this context to a plain array for logging or transport.