Repository Public
1/**2 * Tool vocabulary (CONTRACT §4). `ToolDef` is structurally identical to the one in `@aos/providers`3 * (declared here too so that tools do not depend on providers).4 */5import type { z } from 'zod';6import type { EffectClass, RiskClass, Trust } from '@aos/shared';7import type { ControlPayload } from './control.js';89export interface ToolDef {10 name: string;11 description: string;12 /** JSON Schema, strict-tool-calling compatible (additionalProperties:false, all properties required, optionals nullable). */13 inputSchema: Record<string, unknown>;14}1516/**17 * Per-run file freshness map: workspace-relative POSIX path (of the real, symlink-resolved file) → sha256 hex of18 * the content this run last read or wrote. Owned and persisted by the engine: it is rebuilt on replay by folding19 * `ToolRunResult.fileStateOut` of every recorded tool step, in step order, with `applyFileStateDelta`.20 */21export type FileState = Record<string, string>;22/** Delta produced by one tool call: path → new sha256, or null when the entry must be dropped. */23export type FileStateDelta = Record<string, string | null>;2425export interface SandboxOverrides {26 /** Path of the sandbox launcher. Default `/usr/bin/sandbox-exec`. Tests override it to prove fail-closed behaviour. */27 sandboxExecPath?: string;28 /** Extra absolute paths the sandboxed shell must not read (added to the built-in secret locations). */29 extraDenyRead?: string[];30}3132export interface ToolContext {33 runId: string;34 missionId: string;35 workspaceAbs: string;36 network: 'none' | 'open';37 dataDir: string;38 signal: AbortSignal;39 idemKey: string;40 putArtifact(name: string, data: Buffer | string, mime?: string): Promise<{ sha256: string; size: number }>;41 // ---- additive to CONTRACT §4 (all optional) -------------------------------------------------------------42 /** `fileStateIn`: the run's file freshness map before this call. Absent ⇒ treated as empty (every edit is stale). */43 fileState?: FileState;44 /** Resolve an artifact by name for `read_artifact`. Absent ⇒ lookup in `<dataDir>/artifacts` by the sha embedded in the name. */45 getArtifact?(name: string): Promise<Buffer | null>;46 sandbox?: SandboxOverrides;47}4849export interface ToolRunResult {50 ok: boolean;51 /** Full text for the model (not yet truncated/offloaded — see `finalizeToolOutput`). */52 output: string;53 trust?: Trust;54 /** `fileStateOut`: delta to fold into the run's file state (present on read_file / edit_file / write_file). */55 fileStateOut?: FileStateDelta;56 /** Bounded unified diff for the console (edit_file / write_file / git_diff). Never shown to the model twice. */57 diff?: string;58 /** Present (and only present) on successful control tools: the validated payload the engine must act on. */59 control?: ControlPayload;60}6162export type ReconcileResult = { done: true; output: string } | { done: false } | { unknown: true };6364export interface Tool<I = unknown> {65 def: ToolDef;66 input: z.ZodType<I>;67 /** The optional second argument is additive: `shell` depends on `ctx.network`. */68 effect: EffectClass | ((input: I, ctx?: Pick<ToolContext, 'network'>) => EffectClass);69 risk: RiskClass | ((input: I, ctx?: Pick<ToolContext, 'network'>) => RiskClass);70 parallelSafe: boolean;71 run(input: I, ctx: ToolContext): Promise<ToolRunResult>;72 reconcile?: (input: I, ctx: ToolContext) => Promise<ReconcileResult>;73 describeForApproval?(input: I): { title: string; detail: string; diff?: string };74}7576export function effectOf<I>(tool: Tool<I>, input: I, ctx?: Pick<ToolContext, 'network'>): EffectClass {77 return typeof tool.effect === 'function' ? tool.effect(input, ctx) : tool.effect;78}79export function riskOf<I>(tool: Tool<I>, input: I, ctx?: Pick<ToolContext, 'network'>): RiskClass {80 return typeof tool.risk === 'function' ? tool.risk(input, ctx) : tool.risk;81}8283/** Fold one tool result's delta into a file state (pure; returns a new object with sorted keys ⇒ canonical JSON stable). */84export function applyFileStateDelta(state: FileState | undefined, delta: FileStateDelta | undefined): FileState {85 const next: FileState = { ...(state ?? {}) };86 if (delta) {87 for (const [k, v] of Object.entries(delta)) {88 if (v === null) delete next[k];89 else next[k] = v;90 }91 }92 const sorted: FileState = {};93 for (const k of Object.keys(next).sort()) sorted[k] = next[k] as string;94 return sorted;95}9697export const fail = (output: string): ToolRunResult => ({ ok: false, output });98