@sema-agent/core
Version:
Stateless, task-oriented AI agent core
119 lines (118 loc) • 8.07 kB
TypeScript
/**
* 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;