@tangle-network/agent-runtime 0.189.0 → 0.190.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{activation-DuYIFVQm.js → activation-BgRWFKZi.js} +3 -3
- package/dist/{activation-DuYIFVQm.js.map → activation-BgRWFKZi.js.map} +1 -1
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +3 -3
- package/dist/{authoring-CIE6rF8N.js → authoring-dQQ2z1fT.js} +2 -2
- package/dist/{authoring-CIE6rF8N.js.map → authoring-dQQ2z1fT.js.map} +1 -1
- package/dist/candidate-execution/index.d.ts +3 -3
- package/dist/candidate-execution/index.js +5 -5
- package/dist/{candidate-execution-CQYU8E5a.js → candidate-execution-BSkZErxD.js} +255 -9
- package/dist/candidate-execution-BSkZErxD.js.map +1 -0
- package/dist/conversation.d.ts +1 -1
- package/dist/{coordination-driver-CtbyZv5d.js → coordination-driver-Da1IGdTP.js} +6 -6
- package/dist/{coordination-driver-CtbyZv5d.js.map → coordination-driver-Da1IGdTP.js.map} +1 -1
- package/dist/{durable-file-g3YdfEh4.js → durable-file-D24y9zg7.js} +2 -2
- package/dist/{durable-file-g3YdfEh4.js.map → durable-file-D24y9zg7.js.map} +1 -1
- package/dist/durable.d.ts +2 -2
- package/dist/durable.js +3 -3
- package/dist/{environment-provider-C4AQiUxj.d.ts → environment-provider-Bf-hgR6p.d.ts} +2 -2
- package/dist/{environment-provider-DZxuH3E4.js → environment-provider-DcelIawX.js} +2 -2
- package/dist/{environment-provider-DZxuH3E4.js.map → environment-provider-DcelIawX.js.map} +1 -1
- package/dist/environment-provider.d.ts +1 -1
- package/dist/environment-provider.js +1 -1
- package/dist/{graph-DzkWh1BI.js → graph-Bd1elhdL.js} +5 -3
- package/dist/graph-Bd1elhdL.js.map +1 -0
- package/dist/graph.d.ts +3 -3
- package/dist/graph.js +5 -5
- package/dist/{improvement-cycle-DP9zf-IL.js → improvement-cycle-BanlFMyL.js} +7 -7
- package/dist/{improvement-cycle-DP9zf-IL.js.map → improvement-cycle-BanlFMyL.js.map} +1 -1
- package/dist/{index-tf6g2B27.d.ts → index-C-gBk5M9.d.ts} +4 -4
- package/dist/{index-DCqHTdmp.d.ts → index-CUosKU4N.d.ts} +144 -4
- package/dist/{index-DA9jKuJc.d.ts → index-Clb7_qML.d.ts} +2 -2
- package/dist/{index-k5bcID4t.d.ts → index-DpNil9jh.d.ts} +113 -50
- package/dist/index.d.ts +10 -10
- package/dist/index.js +12 -12
- package/dist/intelligence.d.ts +5 -5
- package/dist/intelligence.js +6 -6
- package/dist/kernel.d.ts +6 -6
- package/dist/kernel.js +12 -12
- package/dist/{knowledge-C5lODZQC.js → knowledge-BXdXlv48.js} +5 -5
- package/dist/{knowledge-C5lODZQC.js.map → knowledge-BXdXlv48.js.map} +1 -1
- package/dist/knowledge.d.ts +1 -1
- package/dist/knowledge.js +1 -1
- package/dist/{loop-runner-bin-BKsjoaVu.js → loop-runner-bin-CGQzzWAc.js} +3 -3
- package/dist/{loop-runner-bin-BKsjoaVu.js.map → loop-runner-bin-CGQzzWAc.js.map} +1 -1
- package/dist/{loop-runner-bin-ncN_8Msd.d.ts → loop-runner-bin-Cy4f9M6p.d.ts} +3 -3
- package/dist/loop-runner-bin.d.ts +1 -1
- package/dist/loop-runner-bin.js +1 -1
- package/dist/mcp/bin.js +3 -3
- package/dist/mcp/index.d.ts +3 -3
- package/dist/mcp/index.js +7 -7
- package/dist/mcp/memory-bin.js +1 -1
- package/dist/{memory-server-DsKOhzGf.js → memory-server-Crkevawz.js} +2 -2
- package/dist/{memory-server-DsKOhzGf.js.map → memory-server-Crkevawz.js.map} +1 -1
- package/dist/{openai-tools-x83CTnJM.js → openai-tools-BSDk_umV.js} +2 -2
- package/dist/{openai-tools-x83CTnJM.js.map → openai-tools-BSDk_umV.js.map} +1 -1
- package/dist/{prepare-BtXhV0qj.js → prepare-B5fJ7mLS.js} +239 -227
- package/dist/prepare-B5fJ7mLS.js.map +1 -0
- package/dist/primeintellect/index.d.ts +1 -1
- package/dist/primeintellect/index.js +1 -1
- package/dist/profiles.js +1 -1
- package/dist/{protected-model-port-DHrayPt4.js → protected-model-port-B0xFSAjt.js} +2 -2
- package/dist/{protected-model-port-DHrayPt4.js.map → protected-model-port-B0xFSAjt.js.map} +1 -1
- package/dist/{provision-supervisor-B-_4I1In.js → provision-supervisor-bwpzajiz.js} +8 -8
- package/dist/{provision-supervisor-B-_4I1In.js.map → provision-supervisor-bwpzajiz.js.map} +1 -1
- package/dist/{redact-C9EWk1FT.d.ts → redact-Cv1GfOFO.d.ts} +3 -3
- package/dist/{run-layout-Cd_l2XWV.js → run-layout-B8I_LXN-.js} +3 -3
- package/dist/{run-layout-Cd_l2XWV.js.map → run-layout-B8I_LXN-.js.map} +1 -1
- package/dist/{runtime-EQgE3942.d.ts → runtime-BMWmSu4A.d.ts} +202 -4
- package/dist/{runtime-C98JsP83.js → runtime-Ddh7PyCy.js} +15 -15
- package/dist/{runtime-C98JsP83.js.map → runtime-Ddh7PyCy.js.map} +1 -1
- package/dist/{spawn-journal-CxRa-NJ2.js → spawn-journal-B9eTmVAn.js} +2 -2
- package/dist/{spawn-journal-CxRa-NJ2.js.map → spawn-journal-B9eTmVAn.js.map} +1 -1
- package/dist/{stream-agent-turn-BomJ21AK.d.ts → stream-agent-turn-uqzf9ZEh.d.ts} +2 -2
- package/dist/{structural-rollout-C8sB6B4q.js → structural-rollout-0NKvKuFm.js} +3 -3
- package/dist/{structural-rollout-C8sB6B4q.js.map → structural-rollout-0NKvKuFm.js.map} +1 -1
- package/dist/{supervise-DSu4d0b6.js → supervise-yDiZAqlN.js} +270 -21
- package/dist/supervise-yDiZAqlN.js.map +1 -0
- package/dist/{supervisor-BSNkaM2a.js → supervisor-CqJW4qap.js} +489 -11
- package/dist/supervisor-CqJW4qap.js.map +1 -0
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +13 -13
- package/dist/{top-app-DIBApkt1.js → top-app-KgIFpD9H.js} +3 -3
- package/dist/{top-app-DIBApkt1.js.map → top-app-KgIFpD9H.js.map} +1 -1
- package/dist/tui/bin.js +1 -1
- package/dist/tui/index.d.ts +1 -1
- package/dist/tui/index.js +2 -2
- package/dist/{types-BbkiJR1D.d.ts → types-02hXqnSP.d.ts} +37 -2
- package/dist/{util-BfoW6RU7.js → util-mCNUeboW.js} +21 -2
- package/dist/util-mCNUeboW.js.map +1 -0
- package/dist/{workspace-archive-SVeiWLNE.js → workspace-archive-yAPDklLV.js} +4 -4
- package/dist/{workspace-archive-SVeiWLNE.js.map → workspace-archive-yAPDklLV.js.map} +1 -1
- package/package.json +8 -8
- package/dist/candidate-execution-CQYU8E5a.js.map +0 -1
- package/dist/graph-DzkWh1BI.js.map +0 -1
- package/dist/prepare-BtXhV0qj.js.map +0 -1
- package/dist/supervise-DSu4d0b6.js.map +0 -1
- package/dist/supervisor-BSNkaM2a.js.map +0 -1
- package/dist/util-BfoW6RU7.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-server-DsKOhzGf.js","names":[],"sources":["../src/mcp/memory-server.ts"],"sourcesContent":["/**\n * Memory MCP server — the LIVE serving half of the `memory` profile surface\n * (Phase 5). A curated memory (lessons distilled from prior runs) is only\n * real if the DRIVEN agent can query it mid-task; this module serves a flat\n * store of `MemoryItem` rows as `memory_search` / `memory_get` tools over the\n * ONE in-repo stdio JSON-RPC core (`createStdioToolServer`), so the exact\n * wire protocol the same-host client (`connectStdioMcp` /\n * `materializeLocalMcp`, runtime/stdio-mcp-client.ts) speaks is guaranteed by\n * construction — serve and consume cannot drift.\n *\n * Retrieval is DETERMINISTIC lexical overlap (no vectors, no LLM): a lift\n * measured with this memory mounted is attributable to the lessons\n * themselves, never to retrieval-model noise. Every\n * `memory_search` can append one JSONL row to a retrieval log (`logPath`) —\n * the per-query record an off-policy retrieval estimator (agent-knowledge's\n * `RetrievalHoldout`) consumes. agent-knowledge is NOT a dependency of this\n * repo, so the log file is the flagged cross-package seam, not an import.\n *\n * Fail-loud discipline (mirrors `materializeLocalMcp`): an EMPTY memory is\n * never served — a profile without memory simply omits the artifact, and\n * silently serving zero rows would fake the with/without ablation.\n *\n * @experimental\n */\n\nimport { appendFileSync, mkdirSync, readFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport type { AgentProfileMcpServer } from '@tangle-network/agent-interface'\nimport { ValidationError } from '../errors'\nimport { createStdioToolServer, type McpToolDescriptor, type StdioToolServer } from './tool-server'\n\n/** One row of agent memory: a crisp lesson/fact with provenance. */\nexport interface MemoryItem {\n /** Stable id (content-hash by convention; see `memoryArtifactFromLessons`). */\n id: string\n /** The lesson itself — one imperative or observation the agent should recall. */\n text: string\n /** Optional retrieval tags, matched by `memory_search` alongside the text. */\n tags?: string[]\n /** Provenance: the finding / trace / curation pass this row came from. */\n source?: string\n}\n\n/**\n * The `memory` artifact payload — HOW a profile's memory is stored and served:\n *\n * - `store: 'file'` — served by the in-repo memory bin\n * (`agent-runtime-memory-mcp`, src/mcp/memory-bin.ts): rows load from\n * `path` (a JSON array or JSONL file of `MemoryItem`) and/or the inline\n * `items` seed (inline wins on id collision). At least one of\n * `path`/`items` is required.\n * - `store: 'mcp'` — an EXTERNAL, already-runnable MCP server that exposes\n * the memory tools itself; `server` is required and mounts verbatim.\n *\n * `logPath` makes the served memory append one JSONL row per `memory_search`\n * — the retrieval log a holdout estimator reads (see module doc).\n */\nexport interface AgentMemorySpec {\n store: 'file' | 'mcp'\n /** `store:'file'` — host path to the durable row store (JSON array or JSONL). */\n path?: string\n /** Inline seed rows, served alongside (and winning over) `path` rows. */\n items?: MemoryItem[]\n /** `store:'mcp'` — the external server that already serves memory tools. */\n server?: AgentProfileMcpServer\n /** JSONL retrieval log: one row per `memory_search` (ts, query, k, returned). */\n logPath?: string\n}\n\n/** Env var naming the durable row store file the memory bin loads (the\n * `memoryMcpServer` ↔ memory-bin contract). */\nexport const MEMORY_FILE_ENV = 'AGENT_MEMORY_FILE'\n/** Env var carrying inline JSON `MemoryItem` rows (win over file rows on id). */\nexport const MEMORY_ITEMS_ENV = 'AGENT_MEMORY_ITEMS'\n/** Env var naming the JSONL retrieval log (one row per `memory_search`). */\nexport const MEMORY_LOG_ENV = 'AGENT_MEMORY_LOG'\n/** Env var overriding the served display name (default 'agent-memory'). */\nexport const MEMORY_NAME_ENV = 'AGENT_MEMORY_NAME'\n\nexport interface CreateMemoryToolServerOptions {\n /** The rows to serve. MUST be non-empty (an empty memory is never served). */\n items: readonly MemoryItem[]\n /** Server display name surfaced via `initialize`. Default 'agent-memory'. */\n serverName?: string\n /** Server version surfaced via `initialize`. Default '0'. */\n serverVersion?: string\n /** Default result count for `memory_search`. Default 5. */\n defaultK?: number\n /** Append one JSONL row per `memory_search` (the retrieval-holdout seam). */\n logPath?: string\n}\n\n/**\n * Build the memory MCP server: `memory_search` (lexical top-k over the rows)\n * and `memory_get` (one row by id) on the generic stdio JSON-RPC core.\n */\nexport function createMemoryToolServer(opts: CreateMemoryToolServerOptions): StdioToolServer {\n if (opts.items.length === 0) {\n throw new ValidationError(\n 'createMemoryToolServer: refusing to serve an EMPTY memory — a profile without memory omits the artifact; serving zero rows would fake the with/without ablation',\n )\n }\n const byId = new Map<string, MemoryItem>()\n for (const item of opts.items) {\n if (byId.has(item.id)) {\n throw new ValidationError(`createMemoryToolServer: duplicate memory item id '${item.id}'`)\n }\n byId.set(item.id, item)\n }\n const items = [...byId.values()]\n const defaultK = opts.defaultK ?? 5\n const logPath = opts.logPath\n if (logPath) mkdirSync(dirname(logPath), { recursive: true })\n\n const search: McpToolDescriptor = {\n name: 'memory_search',\n description:\n 'Search the agent memory of lessons learned from prior runs. Give what you are about to do or the problem you face; returns the top-k lessons ranked by relevance. Consult it BEFORE repeating work a prior run already learned from.',\n inputSchema: {\n type: 'object',\n properties: {\n query: {\n type: 'string',\n description: 'The task/problem at hand — matched against lesson text and tags.',\n },\n k: { type: 'number', description: `Max results (default ${defaultK}).` },\n tags: {\n type: 'array',\n items: { type: 'string' },\n description: 'Only return items carrying at least one of these tags.',\n },\n },\n required: ['query'],\n },\n handler: async (raw) => {\n const args = (raw ?? {}) as { query?: unknown; k?: unknown; tags?: unknown }\n if (typeof args.query !== 'string' || args.query.trim().length === 0) {\n throw new TypeError('memory_search: query must be a non-empty string')\n }\n const k = args.k === undefined ? defaultK : Math.floor(Number(args.k))\n if (!Number.isFinite(k) || k < 1) {\n throw new TypeError('memory_search: k must be a positive integer')\n }\n let tagFilter: string[] | undefined\n if (args.tags !== undefined) {\n if (!Array.isArray(args.tags) || args.tags.some((t) => typeof t !== 'string')) {\n throw new TypeError('memory_search: tags must be an array of strings')\n }\n tagFilter = args.tags as string[]\n }\n const queryTokens = tokenize(args.query)\n const pool = tagFilter\n ? items.filter((i) => (i.tags ?? []).some((t) => tagFilter.includes(t)))\n : items\n const results = pool\n .map((item) => ({ item, score: scoreItem(queryTokens, item) }))\n .filter((r) => r.score > 0)\n // Highest score first; stable on ties (curation order is the tiebreak).\n .sort((a, b) => b.score - a.score)\n .slice(0, k)\n .map(({ item, score }) => ({\n id: item.id,\n text: item.text,\n score: Number(score.toFixed(4)),\n ...(item.tags ? { tags: item.tags } : {}),\n ...(item.source ? { source: item.source } : {}),\n }))\n if (logPath) {\n // The retrieval log: what was asked, what came back, at what rank —\n // exactly the per-query record an off-policy estimator needs.\n appendFileSync(\n logPath,\n `${JSON.stringify({\n ts: new Date().toISOString(),\n query: args.query,\n k,\n returned: results.map((r) => ({ id: r.id, score: r.score })),\n })}\\n`,\n )\n }\n return { query: args.query, results }\n },\n }\n\n const get: McpToolDescriptor = {\n name: 'memory_get',\n description: 'Fetch one memory item verbatim by its id (ids come from memory_search results).',\n inputSchema: {\n type: 'object',\n properties: { id: { type: 'string', description: 'The memory item id.' } },\n required: ['id'],\n },\n handler: async (raw) => {\n const args = (raw ?? {}) as { id?: unknown }\n if (typeof args.id !== 'string' || args.id.trim().length === 0) {\n throw new TypeError('memory_get: id must be a non-empty string')\n }\n const item = byId.get(args.id)\n if (!item) throw new Error(`memory_get: no memory item with id '${args.id}'`)\n return item\n },\n }\n\n return createStdioToolServer({\n serverName: opts.serverName ?? 'agent-memory',\n serverVersion: opts.serverVersion ?? '0',\n tools: [search, get],\n })\n}\n\n/** Coerce an untrusted JSON array into validated `MemoryItem` rows. */\nexport function parseMemoryItems(value: unknown, source: string): MemoryItem[] {\n if (!Array.isArray(value)) {\n throw new ValidationError(`${source}: expected a JSON array of memory items`)\n }\n return value.map((row, i) => coerceMemoryItem(row, `${source}[${i}]`))\n}\n\n/** Read a memory store file: a JSON array, or JSONL (one `MemoryItem` per line). */\nexport function readMemoryItemsFile(path: string): MemoryItem[] {\n let raw: string\n try {\n raw = readFileSync(path, 'utf8')\n } catch (err) {\n throw new ValidationError(\n `readMemoryItemsFile: cannot read '${path}': ${err instanceof Error ? err.message : String(err)}`,\n )\n }\n const trimmed = raw.trim()\n if (trimmed.length === 0) return []\n if (trimmed.startsWith('[')) {\n let parsed: unknown\n try {\n parsed = JSON.parse(trimmed)\n } catch (err) {\n throw new ValidationError(\n `readMemoryItemsFile: '${path}' is not valid JSON: ${(err as Error).message}`,\n )\n }\n return parseMemoryItems(parsed, path)\n }\n return trimmed\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0)\n .map((line, i) => {\n let parsed: unknown\n try {\n parsed = JSON.parse(line)\n } catch (err) {\n throw new ValidationError(\n `readMemoryItemsFile: '${path}' line ${i + 1} is not valid JSON: ${(err as Error).message}`,\n )\n }\n return coerceMemoryItem(parsed, `${path}:${i + 1}`)\n })\n}\n\n/** What the memory bin resolved from its environment. */\nexport interface ResolvedMemoryEnv {\n items: MemoryItem[]\n serverName?: string\n logPath?: string\n}\n\n/**\n * Resolve the bin's memory from `AGENT_MEMORY_FILE` (durable store) and/or\n * `AGENT_MEMORY_ITEMS` (inline JSON rows; wins on id collision). Zero rows is\n * a boot FAILURE, matching the fail-closed materialization discipline.\n */\nexport function resolveMemoryFromEnv(env: Record<string, string | undefined>): ResolvedMemoryEnv {\n const filePath = env[MEMORY_FILE_ENV]\n const inlineRaw = env[MEMORY_ITEMS_ENV]\n const fromFile = filePath ? readMemoryItemsFile(filePath) : []\n let inline: MemoryItem[] = []\n if (inlineRaw) {\n let parsed: unknown\n try {\n parsed = JSON.parse(inlineRaw)\n } catch (err) {\n throw new ValidationError(\n `${MEMORY_ITEMS_ENV} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,\n )\n }\n inline = parseMemoryItems(parsed, MEMORY_ITEMS_ENV)\n }\n const byId = new Map<string, MemoryItem>()\n for (const item of fromFile) byId.set(item.id, item)\n for (const item of inline) byId.set(item.id, item)\n if (byId.size === 0) {\n throw new ValidationError(\n `memory bin: no memory items — set ${MEMORY_FILE_ENV} and/or ${MEMORY_ITEMS_ENV}; an EMPTY memory must never be served (a profile without memory omits the artifact)`,\n )\n }\n const serverName = env[MEMORY_NAME_ENV]\n const logPath = env[MEMORY_LOG_ENV]\n return {\n items: [...byId.values()],\n ...(serverName ? { serverName } : {}),\n ...(logPath ? { logPath } : {}),\n }\n}\n\nfunction coerceMemoryItem(row: unknown, at: string): MemoryItem {\n if (!row || typeof row !== 'object' || Array.isArray(row)) {\n throw new ValidationError(`${at}: memory item must be an object`)\n }\n const r = row as Record<string, unknown>\n if (typeof r.id !== 'string' || r.id.trim().length === 0) {\n throw new ValidationError(`${at}: 'id' must be a non-empty string`)\n }\n if (typeof r.text !== 'string' || r.text.trim().length === 0) {\n throw new ValidationError(`${at}: 'text' must be a non-empty string`)\n }\n let tags: string[] | undefined\n if (r.tags !== undefined) {\n if (!Array.isArray(r.tags) || r.tags.some((t) => typeof t !== 'string')) {\n throw new ValidationError(`${at}: 'tags' must be an array of strings`)\n }\n tags = r.tags as string[]\n }\n if (r.source !== undefined && typeof r.source !== 'string') {\n throw new ValidationError(`${at}: 'source' must be a string`)\n }\n return {\n id: r.id,\n text: r.text,\n ...(tags ? { tags } : {}),\n ...(typeof r.source === 'string' ? { source: r.source } : {}),\n }\n}\n\n/** Unique lowercase alphanumeric tokens of length >= 2. */\nfunction tokenize(text: string): string[] {\n return [\n ...new Set(\n text\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((t) => t.length >= 2),\n ),\n ]\n}\n\n/** Fraction of query tokens present in the item's text + tags (0..1). */\nfunction scoreItem(queryTokens: readonly string[], item: MemoryItem): number {\n if (queryTokens.length === 0) return 0\n const hay = new Set(tokenize(`${item.text} ${(item.tags ?? []).join(' ')}`))\n let hit = 0\n for (const t of queryTokens) if (hay.has(t)) hit += 1\n return hit / queryTokens.length\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"memory-server-Crkevawz.js","names":[],"sources":["../src/mcp/memory-server.ts"],"sourcesContent":["/**\n * Memory MCP server — the LIVE serving half of the `memory` profile surface\n * (Phase 5). A curated memory (lessons distilled from prior runs) is only\n * real if the DRIVEN agent can query it mid-task; this module serves a flat\n * store of `MemoryItem` rows as `memory_search` / `memory_get` tools over the\n * ONE in-repo stdio JSON-RPC core (`createStdioToolServer`), so the exact\n * wire protocol the same-host client (`connectStdioMcp` /\n * `materializeLocalMcp`, runtime/stdio-mcp-client.ts) speaks is guaranteed by\n * construction — serve and consume cannot drift.\n *\n * Retrieval is DETERMINISTIC lexical overlap (no vectors, no LLM): a lift\n * measured with this memory mounted is attributable to the lessons\n * themselves, never to retrieval-model noise. Every\n * `memory_search` can append one JSONL row to a retrieval log (`logPath`) —\n * the per-query record an off-policy retrieval estimator (agent-knowledge's\n * `RetrievalHoldout`) consumes. agent-knowledge is NOT a dependency of this\n * repo, so the log file is the flagged cross-package seam, not an import.\n *\n * Fail-loud discipline (mirrors `materializeLocalMcp`): an EMPTY memory is\n * never served — a profile without memory simply omits the artifact, and\n * silently serving zero rows would fake the with/without ablation.\n *\n * @experimental\n */\n\nimport { appendFileSync, mkdirSync, readFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport type { AgentProfileMcpServer } from '@tangle-network/agent-interface'\nimport { ValidationError } from '../errors'\nimport { createStdioToolServer, type McpToolDescriptor, type StdioToolServer } from './tool-server'\n\n/** One row of agent memory: a crisp lesson/fact with provenance. */\nexport interface MemoryItem {\n /** Stable id (content-hash by convention; see `memoryArtifactFromLessons`). */\n id: string\n /** The lesson itself — one imperative or observation the agent should recall. */\n text: string\n /** Optional retrieval tags, matched by `memory_search` alongside the text. */\n tags?: string[]\n /** Provenance: the finding / trace / curation pass this row came from. */\n source?: string\n}\n\n/**\n * The `memory` artifact payload — HOW a profile's memory is stored and served:\n *\n * - `store: 'file'` — served by the in-repo memory bin\n * (`agent-runtime-memory-mcp`, src/mcp/memory-bin.ts): rows load from\n * `path` (a JSON array or JSONL file of `MemoryItem`) and/or the inline\n * `items` seed (inline wins on id collision). At least one of\n * `path`/`items` is required.\n * - `store: 'mcp'` — an EXTERNAL, already-runnable MCP server that exposes\n * the memory tools itself; `server` is required and mounts verbatim.\n *\n * `logPath` makes the served memory append one JSONL row per `memory_search`\n * — the retrieval log a holdout estimator reads (see module doc).\n */\nexport interface AgentMemorySpec {\n store: 'file' | 'mcp'\n /** `store:'file'` — host path to the durable row store (JSON array or JSONL). */\n path?: string\n /** Inline seed rows, served alongside (and winning over) `path` rows. */\n items?: MemoryItem[]\n /** `store:'mcp'` — the external server that already serves memory tools. */\n server?: AgentProfileMcpServer\n /** JSONL retrieval log: one row per `memory_search` (ts, query, k, returned). */\n logPath?: string\n}\n\n/** Env var naming the durable row store file the memory bin loads (the\n * `memoryMcpServer` ↔ memory-bin contract). */\nexport const MEMORY_FILE_ENV = 'AGENT_MEMORY_FILE'\n/** Env var carrying inline JSON `MemoryItem` rows (win over file rows on id). */\nexport const MEMORY_ITEMS_ENV = 'AGENT_MEMORY_ITEMS'\n/** Env var naming the JSONL retrieval log (one row per `memory_search`). */\nexport const MEMORY_LOG_ENV = 'AGENT_MEMORY_LOG'\n/** Env var overriding the served display name (default 'agent-memory'). */\nexport const MEMORY_NAME_ENV = 'AGENT_MEMORY_NAME'\n\nexport interface CreateMemoryToolServerOptions {\n /** The rows to serve. MUST be non-empty (an empty memory is never served). */\n items: readonly MemoryItem[]\n /** Server display name surfaced via `initialize`. Default 'agent-memory'. */\n serverName?: string\n /** Server version surfaced via `initialize`. Default '0'. */\n serverVersion?: string\n /** Default result count for `memory_search`. Default 5. */\n defaultK?: number\n /** Append one JSONL row per `memory_search` (the retrieval-holdout seam). */\n logPath?: string\n}\n\n/**\n * Build the memory MCP server: `memory_search` (lexical top-k over the rows)\n * and `memory_get` (one row by id) on the generic stdio JSON-RPC core.\n */\nexport function createMemoryToolServer(opts: CreateMemoryToolServerOptions): StdioToolServer {\n if (opts.items.length === 0) {\n throw new ValidationError(\n 'createMemoryToolServer: refusing to serve an EMPTY memory — a profile without memory omits the artifact; serving zero rows would fake the with/without ablation',\n )\n }\n const byId = new Map<string, MemoryItem>()\n for (const item of opts.items) {\n if (byId.has(item.id)) {\n throw new ValidationError(`createMemoryToolServer: duplicate memory item id '${item.id}'`)\n }\n byId.set(item.id, item)\n }\n const items = [...byId.values()]\n const defaultK = opts.defaultK ?? 5\n const logPath = opts.logPath\n if (logPath) mkdirSync(dirname(logPath), { recursive: true })\n\n const search: McpToolDescriptor = {\n name: 'memory_search',\n description:\n 'Search the agent memory of lessons learned from prior runs. Give what you are about to do or the problem you face; returns the top-k lessons ranked by relevance. Consult it BEFORE repeating work a prior run already learned from.',\n inputSchema: {\n type: 'object',\n properties: {\n query: {\n type: 'string',\n description: 'The task/problem at hand — matched against lesson text and tags.',\n },\n k: { type: 'number', description: `Max results (default ${defaultK}).` },\n tags: {\n type: 'array',\n items: { type: 'string' },\n description: 'Only return items carrying at least one of these tags.',\n },\n },\n required: ['query'],\n },\n handler: async (raw) => {\n const args = (raw ?? {}) as { query?: unknown; k?: unknown; tags?: unknown }\n if (typeof args.query !== 'string' || args.query.trim().length === 0) {\n throw new TypeError('memory_search: query must be a non-empty string')\n }\n const k = args.k === undefined ? defaultK : Math.floor(Number(args.k))\n if (!Number.isFinite(k) || k < 1) {\n throw new TypeError('memory_search: k must be a positive integer')\n }\n let tagFilter: string[] | undefined\n if (args.tags !== undefined) {\n if (!Array.isArray(args.tags) || args.tags.some((t) => typeof t !== 'string')) {\n throw new TypeError('memory_search: tags must be an array of strings')\n }\n tagFilter = args.tags as string[]\n }\n const queryTokens = tokenize(args.query)\n const pool = tagFilter\n ? items.filter((i) => (i.tags ?? []).some((t) => tagFilter.includes(t)))\n : items\n const results = pool\n .map((item) => ({ item, score: scoreItem(queryTokens, item) }))\n .filter((r) => r.score > 0)\n // Highest score first; stable on ties (curation order is the tiebreak).\n .sort((a, b) => b.score - a.score)\n .slice(0, k)\n .map(({ item, score }) => ({\n id: item.id,\n text: item.text,\n score: Number(score.toFixed(4)),\n ...(item.tags ? { tags: item.tags } : {}),\n ...(item.source ? { source: item.source } : {}),\n }))\n if (logPath) {\n // The retrieval log: what was asked, what came back, at what rank —\n // exactly the per-query record an off-policy estimator needs.\n appendFileSync(\n logPath,\n `${JSON.stringify({\n ts: new Date().toISOString(),\n query: args.query,\n k,\n returned: results.map((r) => ({ id: r.id, score: r.score })),\n })}\\n`,\n )\n }\n return { query: args.query, results }\n },\n }\n\n const get: McpToolDescriptor = {\n name: 'memory_get',\n description: 'Fetch one memory item verbatim by its id (ids come from memory_search results).',\n inputSchema: {\n type: 'object',\n properties: { id: { type: 'string', description: 'The memory item id.' } },\n required: ['id'],\n },\n handler: async (raw) => {\n const args = (raw ?? {}) as { id?: unknown }\n if (typeof args.id !== 'string' || args.id.trim().length === 0) {\n throw new TypeError('memory_get: id must be a non-empty string')\n }\n const item = byId.get(args.id)\n if (!item) throw new Error(`memory_get: no memory item with id '${args.id}'`)\n return item\n },\n }\n\n return createStdioToolServer({\n serverName: opts.serverName ?? 'agent-memory',\n serverVersion: opts.serverVersion ?? '0',\n tools: [search, get],\n })\n}\n\n/** Coerce an untrusted JSON array into validated `MemoryItem` rows. */\nexport function parseMemoryItems(value: unknown, source: string): MemoryItem[] {\n if (!Array.isArray(value)) {\n throw new ValidationError(`${source}: expected a JSON array of memory items`)\n }\n return value.map((row, i) => coerceMemoryItem(row, `${source}[${i}]`))\n}\n\n/** Read a memory store file: a JSON array, or JSONL (one `MemoryItem` per line). */\nexport function readMemoryItemsFile(path: string): MemoryItem[] {\n let raw: string\n try {\n raw = readFileSync(path, 'utf8')\n } catch (err) {\n throw new ValidationError(\n `readMemoryItemsFile: cannot read '${path}': ${err instanceof Error ? err.message : String(err)}`,\n )\n }\n const trimmed = raw.trim()\n if (trimmed.length === 0) return []\n if (trimmed.startsWith('[')) {\n let parsed: unknown\n try {\n parsed = JSON.parse(trimmed)\n } catch (err) {\n throw new ValidationError(\n `readMemoryItemsFile: '${path}' is not valid JSON: ${(err as Error).message}`,\n )\n }\n return parseMemoryItems(parsed, path)\n }\n return trimmed\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0)\n .map((line, i) => {\n let parsed: unknown\n try {\n parsed = JSON.parse(line)\n } catch (err) {\n throw new ValidationError(\n `readMemoryItemsFile: '${path}' line ${i + 1} is not valid JSON: ${(err as Error).message}`,\n )\n }\n return coerceMemoryItem(parsed, `${path}:${i + 1}`)\n })\n}\n\n/** What the memory bin resolved from its environment. */\nexport interface ResolvedMemoryEnv {\n items: MemoryItem[]\n serverName?: string\n logPath?: string\n}\n\n/**\n * Resolve the bin's memory from `AGENT_MEMORY_FILE` (durable store) and/or\n * `AGENT_MEMORY_ITEMS` (inline JSON rows; wins on id collision). Zero rows is\n * a boot FAILURE, matching the fail-closed materialization discipline.\n */\nexport function resolveMemoryFromEnv(env: Record<string, string | undefined>): ResolvedMemoryEnv {\n const filePath = env[MEMORY_FILE_ENV]\n const inlineRaw = env[MEMORY_ITEMS_ENV]\n const fromFile = filePath ? readMemoryItemsFile(filePath) : []\n let inline: MemoryItem[] = []\n if (inlineRaw) {\n let parsed: unknown\n try {\n parsed = JSON.parse(inlineRaw)\n } catch (err) {\n throw new ValidationError(\n `${MEMORY_ITEMS_ENV} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,\n )\n }\n inline = parseMemoryItems(parsed, MEMORY_ITEMS_ENV)\n }\n const byId = new Map<string, MemoryItem>()\n for (const item of fromFile) byId.set(item.id, item)\n for (const item of inline) byId.set(item.id, item)\n if (byId.size === 0) {\n throw new ValidationError(\n `memory bin: no memory items — set ${MEMORY_FILE_ENV} and/or ${MEMORY_ITEMS_ENV}; an EMPTY memory must never be served (a profile without memory omits the artifact)`,\n )\n }\n const serverName = env[MEMORY_NAME_ENV]\n const logPath = env[MEMORY_LOG_ENV]\n return {\n items: [...byId.values()],\n ...(serverName ? { serverName } : {}),\n ...(logPath ? { logPath } : {}),\n }\n}\n\nfunction coerceMemoryItem(row: unknown, at: string): MemoryItem {\n if (!row || typeof row !== 'object' || Array.isArray(row)) {\n throw new ValidationError(`${at}: memory item must be an object`)\n }\n const r = row as Record<string, unknown>\n if (typeof r.id !== 'string' || r.id.trim().length === 0) {\n throw new ValidationError(`${at}: 'id' must be a non-empty string`)\n }\n if (typeof r.text !== 'string' || r.text.trim().length === 0) {\n throw new ValidationError(`${at}: 'text' must be a non-empty string`)\n }\n let tags: string[] | undefined\n if (r.tags !== undefined) {\n if (!Array.isArray(r.tags) || r.tags.some((t) => typeof t !== 'string')) {\n throw new ValidationError(`${at}: 'tags' must be an array of strings`)\n }\n tags = r.tags as string[]\n }\n if (r.source !== undefined && typeof r.source !== 'string') {\n throw new ValidationError(`${at}: 'source' must be a string`)\n }\n return {\n id: r.id,\n text: r.text,\n ...(tags ? { tags } : {}),\n ...(typeof r.source === 'string' ? { source: r.source } : {}),\n }\n}\n\n/** Unique lowercase alphanumeric tokens of length >= 2. */\nfunction tokenize(text: string): string[] {\n return [\n ...new Set(\n text\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((t) => t.length >= 2),\n ),\n ]\n}\n\n/** Fraction of query tokens present in the item's text + tags (0..1). */\nfunction scoreItem(queryTokens: readonly string[], item: MemoryItem): number {\n if (queryTokens.length === 0) return 0\n const hay = new Set(tokenize(`${item.text} ${(item.tags ?? []).join(' ')}`))\n let hit = 0\n for (const t of queryTokens) if (hay.has(t)) hit += 1\n return hit / queryTokens.length\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as DELEGATION_STATUS_INPUT_SCHEMA, F as DELEGATION_HISTORY_INPUT_SCHEMA, G as DELEGATE_FEEDBACK_DESCRIPTION, I as DELEGATION_HISTORY_TOOL_NAME, K as DELEGATE_FEEDBACK_INPUT_SCHEMA, P as DELEGATION_HISTORY_DESCRIPTION, j as DELEGATION_STATUS_TOOL_NAME, k as DELEGATION_STATUS_DESCRIPTION, q as DELEGATE_FEEDBACK_TOOL_NAME } from "./supervise-yDiZAqlN.js";
|
|
2
2
|
//#region src/mcp/openai-tools.ts
|
|
3
3
|
function buildTool(name, description, parameters) {
|
|
4
4
|
return {
|
|
@@ -41,4 +41,4 @@ function mcpToolsForRuntimeMcpSubset(names) {
|
|
|
41
41
|
//#endregion
|
|
42
42
|
export { mcpToolsForRuntimeMcpSubset as n, mcpToolsForRuntimeMcp as t };
|
|
43
43
|
|
|
44
|
-
//# sourceMappingURL=openai-tools-
|
|
44
|
+
//# sourceMappingURL=openai-tools-BSDk_umV.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai-tools-
|
|
1
|
+
{"version":3,"file":"openai-tools-BSDk_umV.js","names":[],"sources":["../src/mcp/openai-tools.ts"],"sourcesContent":["/**\n *\n * OpenAI Chat Completions `tools[]` projection of the queue-bound agent-runtime\n * MCP delegation tools.\n *\n * Use when a caller-owned OpenAI-compatible transport needs the model to call\n * `delegate_feedback`, `delegation_status`, and `delegation_history`. Execution\n * is the caller's responsibility (typically the parent sandbox runtime's MCP\n * mount); Runtime's profile-bound model path materializes profile tools through\n * its executor instead.\n *\n * Sandbox-SDK callers do NOT need this helper: the sandbox runtime mounts\n * MCP servers natively and the in-sandbox harness discovers tools via the\n * runtime, not via an OpenAI tools array.\n *\n * Tool name + description + JSON-schema are pulled from the canonical\n * `DELEGATE_*` constants exported by `./tools/*` so the projection cannot\n * drift from the server's own validators.\n *\n * @experimental\n */\n\nimport type { OpenAIChatTool } from '../types'\nimport {\n DELEGATE_FEEDBACK_DESCRIPTION,\n DELEGATE_FEEDBACK_INPUT_SCHEMA,\n DELEGATE_FEEDBACK_TOOL_NAME,\n} from './tools/delegate-feedback'\nimport {\n DELEGATION_HISTORY_DESCRIPTION,\n DELEGATION_HISTORY_INPUT_SCHEMA,\n DELEGATION_HISTORY_TOOL_NAME,\n} from './tools/delegation-history'\nimport {\n DELEGATION_STATUS_DESCRIPTION,\n DELEGATION_STATUS_INPUT_SCHEMA,\n DELEGATION_STATUS_TOOL_NAME,\n} from './tools/delegation-status'\n\nfunction buildTool(\n name: string,\n description: string,\n parameters: Readonly<Record<string, unknown>>,\n): OpenAIChatTool {\n // `parameters` arrives as a deeply-readonly `as const` literal. The\n // OpenAI-compatible transports JSON-serialize the body, so a shallow copy\n // into a plain object is sufficient and shields callers that mutate\n // the returned descriptor from corrupting the source constant.\n return {\n type: 'function',\n function: { name, description, parameters: { ...parameters } },\n }\n}\n\n/**\n *\n * Returns the queue-bound delegation tools projected into OpenAI Chat\n * Completions `tools[]` shape. The order is stable: `delegate_feedback`,\n * `delegation_status`, `delegation_history`.\n *\n * @experimental\n */\nexport function mcpToolsForRuntimeMcp(): OpenAIChatTool[] {\n return [\n buildTool(\n DELEGATE_FEEDBACK_TOOL_NAME,\n DELEGATE_FEEDBACK_DESCRIPTION,\n DELEGATE_FEEDBACK_INPUT_SCHEMA as Readonly<Record<string, unknown>>,\n ),\n buildTool(\n DELEGATION_STATUS_TOOL_NAME,\n DELEGATION_STATUS_DESCRIPTION,\n DELEGATION_STATUS_INPUT_SCHEMA as Readonly<Record<string, unknown>>,\n ),\n buildTool(\n DELEGATION_HISTORY_TOOL_NAME,\n DELEGATION_HISTORY_DESCRIPTION,\n DELEGATION_HISTORY_INPUT_SCHEMA as Readonly<Record<string, unknown>>,\n ),\n ]\n}\n\n/**\n *\n * Subset filter — return only the projected tools whose `function.name`\n * appears in `names`. Useful for curated mounts (e.g. only the queue-bound\n * delegation tools, omitting `delegate_feedback`). Unknown names are\n * silently ignored; pass an empty array to get an empty result.\n *\n * @experimental\n */\nexport function mcpToolsForRuntimeMcpSubset(names: ReadonlyArray<string>): OpenAIChatTool[] {\n const allowed = new Set(names)\n return mcpToolsForRuntimeMcp().filter((tool) => allowed.has(tool.function.name))\n}\n"],"mappings":";;AAuCA,SAAS,UACP,MACA,aACA,YACgB;CAKhB,OAAO;EACL,MAAM;EACN,UAAU;GAAE;GAAM;GAAa,YAAY,EAAE,GAAG,WAAW;EAAE;CAC/D;AACF;;;;;;;;;AAUA,SAAgB,wBAA0C;CACxD,OAAO;EACL,UACE,6BACA,+BACA,8BACF;EACA,UACE,6BACA,+BACA,8BACF;EACA,UACE,8BACA,gCACA,+BACF;CACF;AACF;;;;;;;;;;AAWA,SAAgB,4BAA4B,OAAgD;CAC1F,MAAM,UAAU,IAAI,IAAI,KAAK;CAC7B,OAAO,sBAAsB,CAAC,CAAC,QAAQ,SAAS,QAAQ,IAAI,KAAK,SAAS,IAAI,CAAC;AACjF"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { x as contentAddress } from "./spawn-journal-
|
|
1
|
+
import { x as contentAddress } from "./spawn-journal-B9eTmVAn.js";
|
|
2
2
|
import { i as redactProtectedValue, n as createProtectedRecordRedactor, r as redactProtectedReason, t as assertNoProtectedBytes } from "./protected-redaction--F3v1oo8.js";
|
|
3
3
|
import { agentCandidateArtifactRefSchema, agentCandidateBenchmarkResultEvidenceSchema, agentCandidateBenchmarkSuiteSchema, agentCandidateBenchmarkTaskSchema, agentCandidateBundleSchema, agentCandidateContainerSchema, agentCandidateExecutionLimitsSchema, agentCandidateExecutionPlanEvidenceSchema, agentCandidateExecutionPlanMaterialSchema, agentCandidateMaterializationReceiptSchema, agentCandidateModelAccessNetworkSchema, agentCandidateModelSettlementEvidenceSchema, agentCandidateProfileActivationSchema, agentCandidateProfilePlanEvidenceSchema, agentCandidateProfileSchema, agentCandidateResolvedModelSchema, agentCandidateRunCellSchema, agentCandidateRunReceiptSchema, agentCandidateTaskOutcomeEvidenceSchema, agentCandidateTaskOutcomeSpecSchema, agentCandidateTerminationSchema, agentCandidateWorkspaceManifestMaterialSchema, agentCandidateWorkspaceSnapshotEvidenceSchema, agentProfileDiffSchema, agentProfileSchema, applyAgentProfileDiff, sha256DigestSchema } from "@tangle-network/agent-interface";
|
|
4
4
|
import { REDACTION_VERSION, canonicalJson, isLlmSpan } from "@tangle-network/agent-eval";
|
|
5
5
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import { lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rm, stat } from "node:fs/promises";
|
|
6
8
|
import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
7
9
|
import { applyAgentCandidateWorkspacePlan, isMaterializerHarness, materializeCandidateProfile, resolvePiRuntimePaths } from "@tangle-network/agent-profile-materialize";
|
|
8
|
-
import { lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rm, stat } from "node:fs/promises";
|
|
9
|
-
import { constants } from "node:fs";
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { hashKnowledgeBase } from "@tangle-network/agent-knowledge";
|
|
@@ -64,6 +64,240 @@ function deepFreezeCandidate(value, seen = /* @__PURE__ */ new Set()) {
|
|
|
64
64
|
return Object.freeze(value);
|
|
65
65
|
}
|
|
66
66
|
//#endregion
|
|
67
|
+
//#region src/candidate-execution/artifacts.ts
|
|
68
|
+
function artifactCacheKey(artifact) {
|
|
69
|
+
return `${artifact.sha256}:${artifact.byteLength}`;
|
|
70
|
+
}
|
|
71
|
+
async function readVerifiedArtifact(artifact, port) {
|
|
72
|
+
const bytes = "content" in artifact ? Buffer.from(artifact.content, "base64") : await port.read(artifact);
|
|
73
|
+
verifyBytes(bytes, artifact.sha256, artifact.byteLength, "candidate artifact");
|
|
74
|
+
return Uint8Array.from(bytes);
|
|
75
|
+
}
|
|
76
|
+
function verifyBytes(bytes, digest, byteLength, label) {
|
|
77
|
+
if (bytes.byteLength !== byteLength) throw new Error(`${label} byte length ${bytes.byteLength} does not match ${byteLength}`);
|
|
78
|
+
const actual = sha256Bytes(bytes);
|
|
79
|
+
if (actual !== digest) throw new Error(`${label} digest ${actual} does not match ${digest}`);
|
|
80
|
+
}
|
|
81
|
+
async function verifyWorkspaceSnapshotArtifacts(snapshot, port) {
|
|
82
|
+
const [manifest, archive] = await Promise.all([readVerifiedArtifact(snapshot.manifest, port), readVerifiedArtifact(snapshot.archive, port)]);
|
|
83
|
+
const canonicalManifest = canonicalCandidateBytes(snapshot.material);
|
|
84
|
+
if (!Buffer.from(manifest).equals(Buffer.from(canonicalManifest))) throw new Error("workspace manifest artifact is not the exact canonical manifest material");
|
|
85
|
+
if (sha256Bytes(canonicalManifest) !== snapshot.digest) throw new Error("workspace snapshot digest does not match its canonical manifest material");
|
|
86
|
+
return {
|
|
87
|
+
manifest,
|
|
88
|
+
archive
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Refuse a materialized workspace whose files, modes, or bytes are not the signed manifest.
|
|
93
|
+
*
|
|
94
|
+
* The scan streams, so the size of the largest file does not decide whether the check can run, and
|
|
95
|
+
* the refusal names the one mismatch a caller can produce on its own: a capture and a verify that
|
|
96
|
+
* disagree about `portableTree`.
|
|
97
|
+
*/
|
|
98
|
+
async function verifyMaterializedWorkspace(root, expected, options = {}) {
|
|
99
|
+
assertWorkspaceManifest(await scanMaterializedWorkspaceManifest(root, options), expected);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The canonical manifest of one materialized workspace, read without holding any file.
|
|
103
|
+
*
|
|
104
|
+
* Every file is digested by streaming, so the size of the largest file does not decide whether the
|
|
105
|
+
* workspace can be described. `FileHandle.readFile` refuses anything above 2 GiB with
|
|
106
|
+
* `ERR_FS_FILE_TOO_LARGE`, which made a workspace holding one such artifact impossible to verify
|
|
107
|
+
* against a manifest it already matched. The digest is sha-256 over the same bytes either way, so
|
|
108
|
+
* a manifest a buffered read produced is reproduced exactly.
|
|
109
|
+
*/
|
|
110
|
+
async function scanMaterializedWorkspaceManifest(root, options = {}) {
|
|
111
|
+
return workspaceManifestFromEntries(await walkWorkspace(root, options, false));
|
|
112
|
+
}
|
|
113
|
+
async function captureMaterializedWorkspace(root, options = {}) {
|
|
114
|
+
const entries = await walkWorkspace(root, options, true);
|
|
115
|
+
return {
|
|
116
|
+
manifest: workspaceManifestFromEntries(entries),
|
|
117
|
+
files: entries.map((entry) => ({
|
|
118
|
+
path: entry.path,
|
|
119
|
+
mode: entry.mode,
|
|
120
|
+
bytes: Uint8Array.from(capturedBytes(entry))
|
|
121
|
+
}))
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Capture exact verified regular-file bytes for fresh isolated materialization. */
|
|
125
|
+
async function readMaterializedWorkspaceFiles(root, expected, options = {}) {
|
|
126
|
+
const observed = await captureMaterializedWorkspace(root, options);
|
|
127
|
+
assertWorkspaceManifest(observed.manifest, expected);
|
|
128
|
+
return observed.files.map((file) => Object.freeze({
|
|
129
|
+
path: file.path,
|
|
130
|
+
mode: file.mode,
|
|
131
|
+
bytes: Uint8Array.from(file.bytes)
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Build the canonical manifest for files a caller already holds — the shape a remote executor
|
|
136
|
+
* returns. Pass `portableTree` to record Git's two file modes instead of exact permission bits, and
|
|
137
|
+
* pass the same flag to every verify that reads the result.
|
|
138
|
+
*/
|
|
139
|
+
function candidateWorkspaceManifest(files, options = {}) {
|
|
140
|
+
return workspaceManifestFromEntries(files.map((file) => ({
|
|
141
|
+
path: file.path,
|
|
142
|
+
mode: options.portableTree === true ? portableFileMode(file.mode) : file.mode,
|
|
143
|
+
sha256: sha256Bytes(file.bytes),
|
|
144
|
+
byteLength: file.bytes.byteLength
|
|
145
|
+
})));
|
|
146
|
+
}
|
|
147
|
+
function workspaceManifestFromEntries(entries) {
|
|
148
|
+
return {
|
|
149
|
+
kind: "agent-candidate-workspace-manifest",
|
|
150
|
+
files: entries.map((entry) => ({
|
|
151
|
+
path: entry.path,
|
|
152
|
+
mode: entry.mode,
|
|
153
|
+
sha256: entry.sha256,
|
|
154
|
+
byteLength: entry.byteLength
|
|
155
|
+
})).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** Git records exactly two file modes, and a portable-tree scan records the same two. */
|
|
159
|
+
function portableFileMode(mode) {
|
|
160
|
+
return (mode & 73) !== 0 ? 493 : 420;
|
|
161
|
+
}
|
|
162
|
+
function capturedBytes(entry) {
|
|
163
|
+
if (!entry.bytes) throw new Error(`workspace file was digested without its bytes: ${entry.path}`);
|
|
164
|
+
return entry.bytes;
|
|
165
|
+
}
|
|
166
|
+
function assertWorkspaceManifest(observed, expected) {
|
|
167
|
+
if (Buffer.from(canonicalCandidateBytes(observed)).equals(canonicalCandidateBytes(expected))) return;
|
|
168
|
+
if (Buffer.from(canonicalCandidateBytes(withPortableModes(observed))).equals(canonicalCandidateBytes(withPortableModes(expected)))) throw new Error("materialized workspace files do not match the signed manifest: only the file modes differ, so the capture and this verify disagree about portableTree");
|
|
169
|
+
throw new Error("materialized workspace files, modes, or bytes do not match the signed manifest");
|
|
170
|
+
}
|
|
171
|
+
function withPortableModes(material) {
|
|
172
|
+
return {
|
|
173
|
+
...material,
|
|
174
|
+
files: material.files.map((file) => ({
|
|
175
|
+
...file,
|
|
176
|
+
mode: portableFileMode(file.mode)
|
|
177
|
+
}))
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
async function verifyMaterializedProfileWorkspace(root, expected, fileRoot = "workspace") {
|
|
181
|
+
const observedProfile = (await scanMaterializedWorkspaceManifest(root)).files.map(({ path, mode, sha256 }) => ({
|
|
182
|
+
relPath: path,
|
|
183
|
+
mode,
|
|
184
|
+
contentSha256: sha256
|
|
185
|
+
}));
|
|
186
|
+
const expectedFiles = expected.files.filter((file) => (file.root ?? "workspace") === fileRoot).map(({ root: _root, ...file }) => file);
|
|
187
|
+
if (!Buffer.from(canonicalCandidateBytes(observedProfile)).equals(canonicalCandidateBytes(expectedFiles))) throw new Error(`profile ${fileRoot} staging files, modes, or bytes do not match the signed profile plan`);
|
|
188
|
+
}
|
|
189
|
+
async function walkWorkspace(root, options, keepBytes) {
|
|
190
|
+
const ignoredProtectedRootEntries = new Set(options.ignoredProtectedRootEntries ?? []);
|
|
191
|
+
const limits = options.limits;
|
|
192
|
+
const requestedRoot = resolve(root);
|
|
193
|
+
const rootStats = await lstat(requestedRoot);
|
|
194
|
+
if (!rootStats.isDirectory() || rootStats.isSymbolicLink()) throw new Error("workspace root must be a real directory");
|
|
195
|
+
const absoluteRoot = await realpath(requestedRoot);
|
|
196
|
+
const resolvedStats = await lstat(absoluteRoot);
|
|
197
|
+
if (!resolvedStats.isDirectory() || resolvedStats.isSymbolicLink()) throw new Error("workspace root must resolve to a real directory");
|
|
198
|
+
const scanned = [];
|
|
199
|
+
let totalBytes = 0;
|
|
200
|
+
async function visit(directory) {
|
|
201
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
202
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
if (directory === absoluteRoot && ignoredProtectedRootEntries.has(entry.name)) continue;
|
|
205
|
+
const absolute = resolve(directory, entry.name);
|
|
206
|
+
const relPath = relative(absoluteRoot, absolute).split(sep).join("/");
|
|
207
|
+
if (!relPath || relPath.startsWith("../") || relPath.includes("/../")) throw new Error(`workspace entry escapes root: ${relPath}`);
|
|
208
|
+
const stats = await lstat(absolute);
|
|
209
|
+
if (stats.isSymbolicLink()) throw new Error(await symlinkRefusal(absolute, relPath, absoluteRoot));
|
|
210
|
+
if (stats.isDirectory()) {
|
|
211
|
+
await visit(absolute);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (!stats.isFile()) throw new Error(`workspace contains a non-regular entry: ${relPath}`);
|
|
215
|
+
if (limits && scanned.length >= limits.maxFiles) throw new Error("workspace exceeds maxFiles");
|
|
216
|
+
const descriptor = await open(absolute, constants.O_RDONLY | (typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0));
|
|
217
|
+
try {
|
|
218
|
+
const openedStats = await descriptor.stat();
|
|
219
|
+
if (!openedStats.isFile()) throw new Error(`workspace contains a non-regular entry: ${relPath}`);
|
|
220
|
+
if (openedStats.nlink !== 1) throw new Error(`workspace contains a hard-linked file: ${relPath}`);
|
|
221
|
+
const mode = options.portableTree === true ? portableFileMode(openedStats.mode) : openedStats.mode & 511;
|
|
222
|
+
if (limits && openedStats.size > limits.maxFileBytes) throw new Error(`workspace file exceeds maxFileBytes: ${relPath}`);
|
|
223
|
+
const remainingBytes = limits ? limits.maxTotalFileBytes - totalBytes : void 0;
|
|
224
|
+
if (remainingBytes !== void 0 && openedStats.size > remainingBytes) throw new Error("workspace exceeds maxTotalFileBytes");
|
|
225
|
+
const read = await readWorkspaceFile(descriptor, limits ? Math.min(limits.maxFileBytes, remainingBytes ?? limits.maxFileBytes) : void 0, keepBytes, relPath);
|
|
226
|
+
totalBytes += read.byteLength;
|
|
227
|
+
scanned.push({
|
|
228
|
+
path: relPath,
|
|
229
|
+
mode,
|
|
230
|
+
sha256: read.sha256,
|
|
231
|
+
byteLength: read.byteLength,
|
|
232
|
+
...read.bytes === void 0 ? {} : { bytes: read.bytes }
|
|
233
|
+
});
|
|
234
|
+
} finally {
|
|
235
|
+
await descriptor.close();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
await visit(absoluteRoot);
|
|
240
|
+
return scanned;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Why one symbolic link is refused, in the terms that separate a portable link from a link that
|
|
244
|
+
* reaches outside the tree.
|
|
245
|
+
*
|
|
246
|
+
* Every link is refused, in both mode policies. `AgentCandidateWorkspaceManifestMaterial` has no
|
|
247
|
+
* representation for one — `mode`, `sha256` and `byteLength` describe a regular file — so a link
|
|
248
|
+
* recorded as a file would let two different trees share one digest. The reason says which kind of
|
|
249
|
+
* link it was, so a caller can tell a link it can rewrite from one it cannot.
|
|
250
|
+
*/
|
|
251
|
+
async function symlinkRefusal(absolute, relPath, absoluteRoot) {
|
|
252
|
+
const target = await readlink(absolute);
|
|
253
|
+
if (isAbsolute(target)) return `workspace contains an absolute symlink: ${relPath} -> ${target}`;
|
|
254
|
+
if (!isWithin(absoluteRoot, resolve(dirname(absolute), target))) return `workspace contains a symlink that escapes its tree: ${relPath} -> ${target}`;
|
|
255
|
+
let physical;
|
|
256
|
+
try {
|
|
257
|
+
physical = await realpath(absolute);
|
|
258
|
+
} catch {
|
|
259
|
+
return `workspace contains an unresolved symlink: ${relPath} -> ${target}`;
|
|
260
|
+
}
|
|
261
|
+
if (!isWithin(absoluteRoot, physical)) return `workspace contains a symlink that resolves outside its tree: ${relPath} -> ${target}`;
|
|
262
|
+
return `workspace contains a symlink: ${relPath} -> ${target}`;
|
|
263
|
+
}
|
|
264
|
+
function isWithin(root, path) {
|
|
265
|
+
return path === root || path.startsWith(`${root}${sep}`);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Read one open regular file into its digest, keeping the bytes only when the caller needs them.
|
|
269
|
+
*
|
|
270
|
+
* The content reaches the hash in 1 MiB chunks and is never held whole for a digest-only walk, so
|
|
271
|
+
* the size of the largest file does not decide whether a workspace can be described.
|
|
272
|
+
* `FileHandle.readFile` refuses anything above 2 GiB with `ERR_FS_FILE_TOO_LARGE`. Hashing the
|
|
273
|
+
* chunks in order equals hashing the whole buffer, so a manifest a buffered read produced is
|
|
274
|
+
* reproduced byte for byte.
|
|
275
|
+
*/
|
|
276
|
+
async function readWorkspaceFile(descriptor, maxBytes, keepBytes, path) {
|
|
277
|
+
const hash = createHash("sha256");
|
|
278
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
279
|
+
const chunks = [];
|
|
280
|
+
let total = 0;
|
|
281
|
+
while (true) {
|
|
282
|
+
const { bytesRead } = await descriptor.read(buffer, 0, buffer.byteLength, null);
|
|
283
|
+
if (bytesRead === 0) break;
|
|
284
|
+
total += bytesRead;
|
|
285
|
+
if (maxBytes !== void 0 && total > maxBytes) throw new Error(`workspace file exceeds its capture limit: ${path}`);
|
|
286
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
287
|
+
hash.update(chunk);
|
|
288
|
+
if (keepBytes) chunks.push(Buffer.from(chunk));
|
|
289
|
+
}
|
|
290
|
+
const sha256 = `sha256:${hash.digest("hex")}`;
|
|
291
|
+
return keepBytes ? {
|
|
292
|
+
sha256,
|
|
293
|
+
byteLength: total,
|
|
294
|
+
bytes: Buffer.concat(chunks, total)
|
|
295
|
+
} : {
|
|
296
|
+
sha256,
|
|
297
|
+
byteLength: total
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
67
301
|
//#region src/candidate-execution/system-prompt.ts
|
|
68
302
|
const SYSTEM_PROMPT_FILE = ".tangle/system-prompt.md";
|
|
69
303
|
const APPEND_SYSTEM_PROMPT_FILE = ".tangle/append-system-prompt.md";
|
|
@@ -900,228 +1134,6 @@ function candidateTerminalWindowMs(cleanupTimeoutMs) {
|
|
|
900
1134
|
return windowMs;
|
|
901
1135
|
}
|
|
902
1136
|
//#endregion
|
|
903
|
-
//#region src/candidate-execution/artifacts.ts
|
|
904
|
-
function artifactCacheKey(artifact) {
|
|
905
|
-
return `${artifact.sha256}:${artifact.byteLength}`;
|
|
906
|
-
}
|
|
907
|
-
async function readVerifiedArtifact(artifact, port) {
|
|
908
|
-
const bytes = "content" in artifact ? Buffer.from(artifact.content, "base64") : await port.read(artifact);
|
|
909
|
-
verifyBytes(bytes, artifact.sha256, artifact.byteLength, "candidate artifact");
|
|
910
|
-
return Uint8Array.from(bytes);
|
|
911
|
-
}
|
|
912
|
-
function verifyBytes(bytes, digest, byteLength, label) {
|
|
913
|
-
if (bytes.byteLength !== byteLength) throw new Error(`${label} byte length ${bytes.byteLength} does not match ${byteLength}`);
|
|
914
|
-
const actual = sha256Bytes(bytes);
|
|
915
|
-
if (actual !== digest) throw new Error(`${label} digest ${actual} does not match ${digest}`);
|
|
916
|
-
}
|
|
917
|
-
async function verifyWorkspaceSnapshotArtifacts(snapshot, port) {
|
|
918
|
-
const [manifest, archive] = await Promise.all([readVerifiedArtifact(snapshot.manifest, port), readVerifiedArtifact(snapshot.archive, port)]);
|
|
919
|
-
const canonicalManifest = canonicalCandidateBytes(snapshot.material);
|
|
920
|
-
if (!Buffer.from(manifest).equals(Buffer.from(canonicalManifest))) throw new Error("workspace manifest artifact is not the exact canonical manifest material");
|
|
921
|
-
if (sha256Bytes(canonicalManifest) !== snapshot.digest) throw new Error("workspace snapshot digest does not match its canonical manifest material");
|
|
922
|
-
return {
|
|
923
|
-
manifest,
|
|
924
|
-
archive
|
|
925
|
-
};
|
|
926
|
-
}
|
|
927
|
-
async function verifyMaterializedWorkspace(root, expected, options = {}) {
|
|
928
|
-
assertWorkspaceManifest(await scanMaterializedWorkspaceManifest(root, options), expected);
|
|
929
|
-
}
|
|
930
|
-
/**
|
|
931
|
-
* The canonical manifest of one materialized workspace, read without holding any file.
|
|
932
|
-
*
|
|
933
|
-
* Every file is digested by streaming, so the size of the largest file does not decide whether the
|
|
934
|
-
* workspace can be described. `FileHandle.readFile` refuses anything above 2 GiB with
|
|
935
|
-
* `ERR_FS_FILE_TOO_LARGE`, which made a workspace holding one such artifact impossible to verify
|
|
936
|
-
* against a manifest it already matched. The digest is sha-256 over the same bytes either way, so
|
|
937
|
-
* a manifest a buffered read produced is reproduced exactly.
|
|
938
|
-
*/
|
|
939
|
-
async function scanMaterializedWorkspaceManifest(root, options = {}) {
|
|
940
|
-
return workspaceManifestFromEntries(await walkWorkspace(root, options, false));
|
|
941
|
-
}
|
|
942
|
-
async function captureMaterializedWorkspace(root, options = {}) {
|
|
943
|
-
const entries = await walkWorkspace(root, options, true);
|
|
944
|
-
return {
|
|
945
|
-
manifest: workspaceManifestFromEntries(entries),
|
|
946
|
-
files: entries.map((entry) => ({
|
|
947
|
-
path: entry.path,
|
|
948
|
-
mode: entry.mode,
|
|
949
|
-
bytes: Uint8Array.from(capturedBytes(entry))
|
|
950
|
-
}))
|
|
951
|
-
};
|
|
952
|
-
}
|
|
953
|
-
/** Capture exact verified regular-file bytes for fresh isolated materialization. */
|
|
954
|
-
async function readMaterializedWorkspaceFiles(root, expected, options = {}) {
|
|
955
|
-
const observed = await captureMaterializedWorkspace(root, options);
|
|
956
|
-
assertWorkspaceManifest(observed.manifest, expected);
|
|
957
|
-
return observed.files.map((file) => Object.freeze({
|
|
958
|
-
path: file.path,
|
|
959
|
-
mode: file.mode,
|
|
960
|
-
bytes: Uint8Array.from(file.bytes)
|
|
961
|
-
}));
|
|
962
|
-
}
|
|
963
|
-
function candidateWorkspaceManifest(files, options = {}) {
|
|
964
|
-
return workspaceManifestFromEntries(files.map((file) => ({
|
|
965
|
-
path: file.path,
|
|
966
|
-
mode: options.portableTree === true ? portableFileMode(file.mode) : file.mode,
|
|
967
|
-
sha256: sha256Bytes(file.bytes),
|
|
968
|
-
byteLength: file.bytes.byteLength
|
|
969
|
-
})));
|
|
970
|
-
}
|
|
971
|
-
function workspaceManifestFromEntries(entries) {
|
|
972
|
-
return {
|
|
973
|
-
kind: "agent-candidate-workspace-manifest",
|
|
974
|
-
files: entries.map((entry) => ({
|
|
975
|
-
path: entry.path,
|
|
976
|
-
mode: entry.mode,
|
|
977
|
-
sha256: entry.sha256,
|
|
978
|
-
byteLength: entry.byteLength
|
|
979
|
-
})).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
/** Git records exactly two file modes, and a portable-tree scan records the same two. */
|
|
983
|
-
function portableFileMode(mode) {
|
|
984
|
-
return (mode & 73) !== 0 ? 493 : 420;
|
|
985
|
-
}
|
|
986
|
-
function capturedBytes(entry) {
|
|
987
|
-
if (!entry.bytes) throw new Error(`workspace file was digested without its bytes: ${entry.path}`);
|
|
988
|
-
return entry.bytes;
|
|
989
|
-
}
|
|
990
|
-
function assertWorkspaceManifest(observed, expected) {
|
|
991
|
-
if (Buffer.from(canonicalCandidateBytes(observed)).equals(canonicalCandidateBytes(expected))) return;
|
|
992
|
-
if (Buffer.from(canonicalCandidateBytes(withPortableModes(observed))).equals(canonicalCandidateBytes(withPortableModes(expected)))) throw new Error("materialized workspace files do not match the signed manifest: only the file modes differ, so the capture and this verify disagree about portableTree");
|
|
993
|
-
throw new Error("materialized workspace files, modes, or bytes do not match the signed manifest");
|
|
994
|
-
}
|
|
995
|
-
function withPortableModes(material) {
|
|
996
|
-
return {
|
|
997
|
-
...material,
|
|
998
|
-
files: material.files.map((file) => ({
|
|
999
|
-
...file,
|
|
1000
|
-
mode: portableFileMode(file.mode)
|
|
1001
|
-
}))
|
|
1002
|
-
};
|
|
1003
|
-
}
|
|
1004
|
-
async function verifyMaterializedProfileWorkspace(root, expected, fileRoot = "workspace") {
|
|
1005
|
-
const observedProfile = (await scanMaterializedWorkspaceManifest(root)).files.map(({ path, mode, sha256 }) => ({
|
|
1006
|
-
relPath: path,
|
|
1007
|
-
mode,
|
|
1008
|
-
contentSha256: sha256
|
|
1009
|
-
}));
|
|
1010
|
-
const expectedFiles = expected.files.filter((file) => (file.root ?? "workspace") === fileRoot).map(({ root: _root, ...file }) => file);
|
|
1011
|
-
if (!Buffer.from(canonicalCandidateBytes(observedProfile)).equals(canonicalCandidateBytes(expectedFiles))) throw new Error(`profile ${fileRoot} staging files, modes, or bytes do not match the signed profile plan`);
|
|
1012
|
-
}
|
|
1013
|
-
async function walkWorkspace(root, options, keepBytes) {
|
|
1014
|
-
const ignoredProtectedRootEntries = new Set(options.ignoredProtectedRootEntries ?? []);
|
|
1015
|
-
const limits = options.limits;
|
|
1016
|
-
const requestedRoot = resolve(root);
|
|
1017
|
-
const rootStats = await lstat(requestedRoot);
|
|
1018
|
-
if (!rootStats.isDirectory() || rootStats.isSymbolicLink()) throw new Error("workspace root must be a real directory");
|
|
1019
|
-
const absoluteRoot = await realpath(requestedRoot);
|
|
1020
|
-
const resolvedStats = await lstat(absoluteRoot);
|
|
1021
|
-
if (!resolvedStats.isDirectory() || resolvedStats.isSymbolicLink()) throw new Error("workspace root must resolve to a real directory");
|
|
1022
|
-
const scanned = [];
|
|
1023
|
-
let totalBytes = 0;
|
|
1024
|
-
async function visit(directory) {
|
|
1025
|
-
const entries = await readdir(directory, { withFileTypes: true });
|
|
1026
|
-
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
1027
|
-
for (const entry of entries) {
|
|
1028
|
-
if (directory === absoluteRoot && ignoredProtectedRootEntries.has(entry.name)) continue;
|
|
1029
|
-
const absolute = resolve(directory, entry.name);
|
|
1030
|
-
const relPath = relative(absoluteRoot, absolute).split(sep).join("/");
|
|
1031
|
-
if (!relPath || relPath.startsWith("../") || relPath.includes("/../")) throw new Error(`workspace entry escapes root: ${relPath}`);
|
|
1032
|
-
const stats = await lstat(absolute);
|
|
1033
|
-
if (stats.isSymbolicLink()) throw new Error(await symlinkRefusal(absolute, relPath, absoluteRoot));
|
|
1034
|
-
if (stats.isDirectory()) {
|
|
1035
|
-
await visit(absolute);
|
|
1036
|
-
continue;
|
|
1037
|
-
}
|
|
1038
|
-
if (!stats.isFile()) throw new Error(`workspace contains a non-regular entry: ${relPath}`);
|
|
1039
|
-
if (limits && scanned.length >= limits.maxFiles) throw new Error("workspace exceeds maxFiles");
|
|
1040
|
-
const descriptor = await open(absolute, constants.O_RDONLY | (typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0));
|
|
1041
|
-
try {
|
|
1042
|
-
const openedStats = await descriptor.stat();
|
|
1043
|
-
if (!openedStats.isFile()) throw new Error(`workspace contains a non-regular entry: ${relPath}`);
|
|
1044
|
-
if (openedStats.nlink !== 1) throw new Error(`workspace contains a hard-linked file: ${relPath}`);
|
|
1045
|
-
const mode = options.portableTree === true ? portableFileMode(openedStats.mode) : openedStats.mode & 511;
|
|
1046
|
-
if (limits && openedStats.size > limits.maxFileBytes) throw new Error(`workspace file exceeds maxFileBytes: ${relPath}`);
|
|
1047
|
-
const remainingBytes = limits ? limits.maxTotalFileBytes - totalBytes : void 0;
|
|
1048
|
-
if (remainingBytes !== void 0 && openedStats.size > remainingBytes) throw new Error("workspace exceeds maxTotalFileBytes");
|
|
1049
|
-
const read = await readWorkspaceFile(descriptor, limits ? Math.min(limits.maxFileBytes, remainingBytes ?? limits.maxFileBytes) : void 0, keepBytes, relPath);
|
|
1050
|
-
totalBytes += read.byteLength;
|
|
1051
|
-
scanned.push({
|
|
1052
|
-
path: relPath,
|
|
1053
|
-
mode,
|
|
1054
|
-
sha256: read.sha256,
|
|
1055
|
-
byteLength: read.byteLength,
|
|
1056
|
-
...read.bytes === void 0 ? {} : { bytes: read.bytes }
|
|
1057
|
-
});
|
|
1058
|
-
} finally {
|
|
1059
|
-
await descriptor.close();
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
await visit(absoluteRoot);
|
|
1064
|
-
return scanned;
|
|
1065
|
-
}
|
|
1066
|
-
/**
|
|
1067
|
-
* Why one symbolic link is refused, in the terms that separate a portable link from a link that
|
|
1068
|
-
* reaches outside the tree.
|
|
1069
|
-
*
|
|
1070
|
-
* Every link is refused, in both mode policies. `AgentCandidateWorkspaceManifestMaterial` has no
|
|
1071
|
-
* representation for one — `mode`, `sha256` and `byteLength` describe a regular file — so a link
|
|
1072
|
-
* recorded as a file would let two different trees share one digest. The reason says which kind of
|
|
1073
|
-
* link it was, so a caller can tell a link it can rewrite from one it cannot.
|
|
1074
|
-
*/
|
|
1075
|
-
async function symlinkRefusal(absolute, relPath, absoluteRoot) {
|
|
1076
|
-
const target = await readlink(absolute);
|
|
1077
|
-
if (isAbsolute(target)) return `workspace contains an absolute symlink: ${relPath} -> ${target}`;
|
|
1078
|
-
if (!isWithin(absoluteRoot, resolve(dirname(absolute), target))) return `workspace contains a symlink that escapes its tree: ${relPath} -> ${target}`;
|
|
1079
|
-
let physical;
|
|
1080
|
-
try {
|
|
1081
|
-
physical = await realpath(absolute);
|
|
1082
|
-
} catch {
|
|
1083
|
-
return `workspace contains an unresolved symlink: ${relPath} -> ${target}`;
|
|
1084
|
-
}
|
|
1085
|
-
if (!isWithin(absoluteRoot, physical)) return `workspace contains a symlink that resolves outside its tree: ${relPath} -> ${target}`;
|
|
1086
|
-
return `workspace contains a symlink: ${relPath} -> ${target}`;
|
|
1087
|
-
}
|
|
1088
|
-
function isWithin(root, path) {
|
|
1089
|
-
return path === root || path.startsWith(`${root}${sep}`);
|
|
1090
|
-
}
|
|
1091
|
-
/**
|
|
1092
|
-
* Read one open regular file into its digest, keeping the bytes only when the caller needs them.
|
|
1093
|
-
*
|
|
1094
|
-
* The content reaches the hash in 1 MiB chunks and is never held whole for a digest-only walk, so
|
|
1095
|
-
* the size of the largest file does not decide whether a workspace can be described.
|
|
1096
|
-
* `FileHandle.readFile` refuses anything above 2 GiB with `ERR_FS_FILE_TOO_LARGE`. Hashing the
|
|
1097
|
-
* chunks in order equals hashing the whole buffer, so a manifest a buffered read produced is
|
|
1098
|
-
* reproduced byte for byte.
|
|
1099
|
-
*/
|
|
1100
|
-
async function readWorkspaceFile(descriptor, maxBytes, keepBytes, path) {
|
|
1101
|
-
const hash = createHash("sha256");
|
|
1102
|
-
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
1103
|
-
const chunks = [];
|
|
1104
|
-
let total = 0;
|
|
1105
|
-
while (true) {
|
|
1106
|
-
const { bytesRead } = await descriptor.read(buffer, 0, buffer.byteLength, null);
|
|
1107
|
-
if (bytesRead === 0) break;
|
|
1108
|
-
total += bytesRead;
|
|
1109
|
-
if (maxBytes !== void 0 && total > maxBytes) throw new Error(`workspace file exceeds its capture limit: ${path}`);
|
|
1110
|
-
const chunk = buffer.subarray(0, bytesRead);
|
|
1111
|
-
hash.update(chunk);
|
|
1112
|
-
if (keepBytes) chunks.push(Buffer.from(chunk));
|
|
1113
|
-
}
|
|
1114
|
-
const sha256 = `sha256:${hash.digest("hex")}`;
|
|
1115
|
-
return keepBytes ? {
|
|
1116
|
-
sha256,
|
|
1117
|
-
byteLength: total,
|
|
1118
|
-
bytes: Buffer.concat(chunks, total)
|
|
1119
|
-
} : {
|
|
1120
|
-
sha256,
|
|
1121
|
-
byteLength: total
|
|
1122
|
-
};
|
|
1123
|
-
}
|
|
1124
|
-
//#endregion
|
|
1125
1137
|
//#region src/candidate-execution/execution-roots.ts
|
|
1126
1138
|
/** Validate the canonical identity and isolation boundary for execution roots. */
|
|
1127
1139
|
function assertAgentCandidateExecutionRoots(roots) {
|
|
@@ -4645,6 +4657,6 @@ function errorMessage(error) {
|
|
|
4645
4657
|
return error instanceof Error ? error.message : String(error);
|
|
4646
4658
|
}
|
|
4647
4659
|
//#endregion
|
|
4648
|
-
export {
|
|
4660
|
+
export { parseExactAgentProfile as $, assertRecoveryMatchesStaged as A, assertExactObjectKeys as B, assertNoGitIndirection as C, candidateCleanupDeadline as D, assertAgentCandidateExecutionRoots as E, rejectedFinish as F, applyExactAgentProfileDiff as G, PI_CANDIDATE_AGENT_DIR_ENV as H, rejectedStage as I, createAgentCandidateProfileActivation as J, assertCandidateProfileBinding as K, requireStagedTerminal as L, assertTerminalMatchesClaim as M, assertTerminalMatchesStaged as N, candidateCleanupTimeout as O, recoveredTerminalRecord as P, parseAgentCandidateProfileActivation as Q, sealTerminalDigest as R, CANDIDATE_TRACE_TAGS as S, runCandidateGit as T, PI_CANDIDATE_SESSION_DIR_ENV as U, isWellFormedUnicode as V, agentCandidateProfileAsAgentProfile as W, materializeAgentCandidateProfilePlan as X, freezeGenericAgentCandidateProfile as Y, omitUndefinedObjectFields as Z, candidateExecutionClaim as _, persistCandidateOutputArtifact as a, scanMaterializedWorkspaceManifest as at, consumePreparedCandidateExecution as b, CANDIDATE_KNOWLEDGE_ROOT_ENV as c, canonicalCandidateBytes as ct, verifiedResourceTextByDigest as d, deepFreezeCandidate as dt, parseExactAgentProfileDiff as et, verifyAgentCandidateBundle as f, embeddedCandidateArtifact as ft, candidateClaimFileInternals as g, verifyCanonicalCandidateDocument as gt, InMemoryAgentCandidateExecutionClaimStore as h, sha256Bytes as ht, persistCandidateModelSettlementEvidence as i, readVerifiedArtifact as it, assertTerminalAllowedInPhase as j, withinCandidateCleanupDeadline as k, candidateKnowledgeExecutionPaths as l, canonicalCandidateDigest$1 as lt, usdToNanos as m, omitTopLevelDigest as mt, executePreparedAgentCandidate as n, candidateWorkspaceManifest as nt, sealAgentCandidateExecutorStopAcknowledgement as o, verifyBytes as ot, sealAgentCandidateModelSettlement as p, immutableCandidateValue as pt, candidateProfileAgentPaths as q, RecoveryAgentCandidateTraceStore as r, captureMaterializedWorkspace as rt, CANDIDATE_KNOWLEDGE_RETRIEVAL_CONFIG_ENV as s, verifyMaterializedWorkspace as st, prepareAgentCandidateExecution as t, parseExactCandidateProfile as tt, AGENT_CANDIDATE_EXECUTION_SUPPORT as u, canonicalCandidateDocument as ut, assertPreparedCandidateIntegrity as v, readCandidateGitTreeFiles as w, CANDIDATE_TRACE_ENV as x, beginPreparedCandidateDisposal as y, terminalRecord as z };
|
|
4649
4661
|
|
|
4650
|
-
//# sourceMappingURL=prepare-
|
|
4662
|
+
//# sourceMappingURL=prepare-B5fJ7mLS.js.map
|