UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

119 lines (118 loc) • 8.07 kB
/** * design/178 §3 — the tool content-origin classification (three tiers by CAPABILITY, fail-closed). * * The question this face answers per mounted tool: can invoking it bring content from OUTSIDE the * deployment's trust boundary into the session — and therefore, via the automatic harvest path, * into long-term memory? Three tiers: * * - "external" — network-retrieval families and external protocol channels. Invocation marks the * session's memory EXTERNALLY EXPOSED (one-way, durable; what the mark does at * harvest is the provenance mode's question — design/336: tag-admit with an origin * marker under "carry", full quarantine under "off"). * - "execution" — general execution (shell/exec). A NAMED residual channel: it can reach external * content indirectly, but the two industry anchors this tier is calibrated against * gate on network/protocol families only, and excluding every shell session would * disable memory for the main coding scenario. Not polluting by default; the * `execIsExternalContent` strict knob upgrades it to external. * - "local" — purely local reads/computation. * * WHO CLASSIFIES (single-source rule): core built-ins declare `contentOrigin` on their own ToolSpec * (the classification lives with the tool definition); protocol tools (MCP/A2A) are structurally * external (the channel itself crosses the boundary); a HOST tool declares its own — and when it * does not, the default is EXTERNAL (fail-closed: unknown = external), overridable only through the * explicit `TaskSpec.memory.trustedTools` allowlist. A core-mounted built-in without a declaration * defaults local: core owns those definitions, and the fail-closed default exists for tools core * CANNOT vouch for, not for its own. A declaration is honored only when it SPELLS a member of the * closed vocabulary — an unreadable one is not a declaration. * * The three tier names are BOUNDARY-RELATIVE, not statements about topology: `"local"` means "does * not bring content from outside the deployment's trust boundary", which a unix socket to another * process the deployment itself runs satisfies and a loopback HTTP call to somebody else's service * does not. Distance is not the axis; whose content it is, is. * * There is deliberately no "gate inactive" state: every mounted tool gets a class. */ import type { ToolContentOrigin } from "../types.js"; export interface ClassifyToolContentOriginInput { /** The tool's own `contentOrigin` declaration (ToolSpec/AgentTool carry) — wins when present AND * a member of the closed vocabulary ({@link isToolContentOrigin}); a present non-member is the * unreadable-declaration case and classifies EXTERNAL. The annotation is intent, not a guarantee: * the property rides tool objects untyped. */ declared?: ToolContentOrigin; /** True ⇔ the name belongs to a protocol namespace (mcp__/a2a__ …) — an external channel. */ isProtocolTool: boolean; /** True ⇔ the tool came from the caller/host surface (`TaskSpec.tools`), not a core mount. */ isCallerTool: boolean; /** True ⇔ `TaskSpec.memory.trustedTools` names this tool (explicit host exemption). */ trusted: boolean; } /** The vocabulary in wire/message order — the one spelling list a refusal quotes back. */ export declare const TOOL_CONTENT_ORIGINS: readonly ToolContentOrigin[]; /** Runtime membership in the closed vocabulary. */ export declare function isToolContentOrigin(value: unknown): value is ToolContentOrigin; export declare function classifyToolContentOrigin(input: ClassifyToolContentOriginInput): ToolContentOrigin; /** * Declare `origin` on a tool object built OUTSIDE `defineTool` — a raw AgentTool literal, which is * how core's protocol-facing tools are constructed. The axis rides the tool object untyped, exactly * as `defineTool` spreads it, and this function is the ONE place that cast lives; the classifier * reads the same property either way, so the "classification lives with the tool definition" rule * holds for both construction styles. */ export declare function withContentOrigin<T extends object>(tool: T, origin: ToolContentOrigin): T; /** One offered agent type's tool narrowing, as the delegation tool declares it. */ export interface AgentToolFace { name: string; allowTools?: readonly string[]; denyTools?: readonly string[]; /** True ⇔ a child of this type is itself handed a delegation tool (the depth budget permits another * level and this face does not exclude the delegation tool). Such a child can reach any SIBLING * face, so its own narrowing is not the boundary — see {@link delegationCallIsExternal}. */ canRedelegate?: boolean; } /** One tool in the pool a delegated child draws from — enough to classify it, nothing more. */ export interface AgentPoolTool { name: string; aliases?: readonly string[]; /** The pool row's own declaration — a STATIC surface the deployment writes when it composes the * delegation tool, and the only class information this judgment has about the child's roster. * * A per-entry MCP declaration (design/378, `McpServerSpec.contentOrigin`) does NOT reach here: the * pool is a declaration, not a live roster, and this judgment never sees the server entries. A * deployment that hands a declared server's `mcp__` tools to children MIRRORS the value onto the * matching pool rows; not mirroring it leaves them on the protocol arm, so the delegation reads * external and the parent over-marks — the safe direction, and the reason this is a documented * duty rather than a refusal. */ contentOrigin?: ToolContentOrigin; } /** * design/178 §3 (ruled 2026-08-08) — classify ONE delegation call by the child's tool face. * * A child that can reach a network or protocol tool returns content from outside the trust boundary * just as surely as calling that tool in this session would, and the parent's transcript is where it * lands. The judgment is STATIC — the child's face is known from its definition before the call — so * nothing here tracks what the child actually did (runtime provenance is design/180 half A). That * makes it conservative in the safe direction: a child that COULD reach external content counts as * having done so. Whether this verdict MARKS the judging session's memory when no attestation * arrives is the deployment's evidence standard (design/324, * `RunnerDeps.memoryDelegationEvidence`) — the classification itself is value-independent, and the * wrap computes it on every standard (the waiver announcement keys on it). * * `pool` is the CHILD'S pool, not the parent's roster: a delegation tool carries its own tool set, and * the two are explicitly allowed to differ — a parent mounting nothing but the delegation tool can * hand its children a network tool. Comparing against the parent's roster would call that local. * * FAIL-CLOSED on ignorance: an unnamed type, a type the delegation tool does not offer, a tool that * declares no faces, or a pool that cannot be enumerated statically (a spawn-time tool factory) is * EXTERNAL. "We could not tell" and "it is safe" are different answers. */ export declare function delegationCallIsExternal(input: { requestedType: string | undefined; faces: ReadonlyArray<AgentToolFace> | undefined; /** Undefined ⇔ the child's pool is not statically knowable ⇒ fail closed. */ pool: ReadonlyArray<AgentPoolTool> | undefined; /** Whether one pool tool carries a polluting class — the caller owns the deployment's config. */ isPolluting: (tool: AgentPoolTool) => boolean; }): boolean; /** Whether a tool of `origin` pollutes the session's memory when invoked (strict = the * `execIsExternalContent` upgrade). */ export declare function contentOriginPollutes(origin: ToolContentOrigin, strict: boolean): boolean;