@tangle-network/agent-profile-materialize 1.1.6 → 1.1.8

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.
Files changed (95) hide show
  1. package/dist/index.d.ts +1226 -29
  2. package/dist/index.js +4987 -23
  3. package/package.json +7 -5
  4. package/dist/candidate-types.d.ts +0 -36
  5. package/dist/candidate-types.js +0 -1
  6. package/dist/candidate.d.ts +0 -9
  7. package/dist/candidate.js +0 -283
  8. package/dist/commands.d.ts +0 -2
  9. package/dist/commands.js +0 -32
  10. package/dist/control-maps.d.ts +0 -58
  11. package/dist/control-maps.js +0 -277
  12. package/dist/execution-controls.d.ts +0 -5
  13. package/dist/execution-controls.js +0 -199
  14. package/dist/harness-prompt.d.ts +0 -22
  15. package/dist/harness-prompt.js +0 -64
  16. package/dist/harness.d.ts +0 -131
  17. package/dist/harness.js +0 -416
  18. package/dist/hooks.d.ts +0 -6
  19. package/dist/hooks.js +0 -250
  20. package/dist/managed-workspace-bind.d.ts +0 -4
  21. package/dist/managed-workspace-bind.js +0 -112
  22. package/dist/managed-workspace-stage.d.ts +0 -25
  23. package/dist/managed-workspace-stage.js +0 -199
  24. package/dist/managed-workspace-types.d.ts +0 -38
  25. package/dist/managed-workspace-types.js +0 -1
  26. package/dist/managed-workspace.d.ts +0 -37
  27. package/dist/managed-workspace.js +0 -290
  28. package/dist/mcp-config-ownership.d.ts +0 -13
  29. package/dist/mcp-config-ownership.js +0 -79
  30. package/dist/mcp-opencode.d.ts +0 -25
  31. package/dist/mcp-opencode.js +0 -52
  32. package/dist/mcp-timeouts.d.ts +0 -7
  33. package/dist/mcp-timeouts.js +0 -48
  34. package/dist/mcp.d.ts +0 -4
  35. package/dist/mcp.js +0 -246
  36. package/dist/model-id.d.ts +0 -55
  37. package/dist/model-id.js +0 -67
  38. package/dist/pi-models-config.d.ts +0 -32
  39. package/dist/pi-models-config.js +0 -65
  40. package/dist/plan-application.d.ts +0 -10
  41. package/dist/plan-application.js +0 -190
  42. package/dist/plan-digest.d.ts +0 -18
  43. package/dist/plan-digest.js +0 -116
  44. package/dist/plan-json.d.ts +0 -13
  45. package/dist/plan-json.js +0 -91
  46. package/dist/plan-normalize.d.ts +0 -12
  47. package/dist/plan-normalize.js +0 -168
  48. package/dist/plan-prompt.d.ts +0 -38
  49. package/dist/plan-prompt.js +0 -49
  50. package/dist/plan-support.d.ts +0 -9
  51. package/dist/plan-support.js +0 -22
  52. package/dist/plan-types.d.ts +0 -341
  53. package/dist/plan-types.js +0 -105
  54. package/dist/prime-agent-dir.d.ts +0 -64
  55. package/dist/prime-agent-dir.js +0 -173
  56. package/dist/prime-fork.d.ts +0 -155
  57. package/dist/prime-fork.js +0 -262
  58. package/dist/prime-profile.d.ts +0 -88
  59. package/dist/prime-profile.js +0 -297
  60. package/dist/profile-bundle-cache.d.ts +0 -37
  61. package/dist/profile-bundle-cache.js +0 -320
  62. package/dist/profile-bundle-key.d.ts +0 -69
  63. package/dist/profile-bundle-key.js +0 -62
  64. package/dist/profile-bundle-lru.d.ts +0 -56
  65. package/dist/profile-bundle-lru.js +0 -220
  66. package/dist/profile-materializer.d.ts +0 -24
  67. package/dist/profile-materializer.js +0 -114
  68. package/dist/profile-plan.d.ts +0 -16
  69. package/dist/profile-plan.js +0 -90
  70. package/dist/profile-prompt.d.ts +0 -4
  71. package/dist/profile-prompt.js +0 -107
  72. package/dist/profile-support.d.ts +0 -8
  73. package/dist/profile-support.js +0 -6
  74. package/dist/resource-lowering.d.ts +0 -6
  75. package/dist/resource-lowering.js +0 -103
  76. package/dist/resource-resolution.d.ts +0 -27
  77. package/dist/resource-resolution.js +0 -265
  78. package/dist/secret-concurrent-revision.d.ts +0 -30
  79. package/dist/secret-concurrent-revision.js +0 -291
  80. package/dist/secret-session-view.d.ts +0 -36
  81. package/dist/secret-session-view.js +0 -179
  82. package/dist/subagents.d.ts +0 -6
  83. package/dist/subagents.js +0 -137
  84. package/dist/tool-bin-path.d.ts +0 -23
  85. package/dist/tool-bin-path.js +0 -61
  86. package/dist/workspace-apply-files.d.ts +0 -57
  87. package/dist/workspace-apply-files.js +0 -193
  88. package/dist/workspace-apply-publish.d.ts +0 -22
  89. package/dist/workspace-apply-publish.js +0 -178
  90. package/dist/workspace-apply.d.ts +0 -11
  91. package/dist/workspace-apply.js +0 -124
  92. package/dist/workspace-files.d.ts +0 -8
  93. package/dist/workspace-files.js +0 -46
  94. package/dist/workspace-launch.d.ts +0 -3
  95. package/dist/workspace-launch.js +0 -13
@@ -1,297 +0,0 @@
1
- const PRIME_SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2
- const PRIME_MAX_SKILL_NAME_LENGTH = 64;
3
- export const PRIME_PROFILE_DIR_NAME = "profile";
4
- export const PRIME_SKILLS_DIR_NAME = "skills";
5
- export const PRIME_SYSTEM_PROMPT_FILE = "system-prompt.md";
6
- export const PRIME_APPEND_SYSTEM_PROMPT_FILE = "append-system-prompt.md";
7
- /** Agent-dir global context file the fork reads (resource-loader.ts:59,86). */
8
- export const PRIME_CONTEXT_FILE = "AGENTS.md";
9
- /** Fork continual-harness store location under the agent dir (refinement.ts:269-279). */
10
- export const PRIME_HARNESS_DIR_NAME = "harness";
11
- export const PRIME_HARNESS_STATE_FILE = "harness_state.json";
12
- export const PRIME_HARNESS_KINDS = [
13
- "prompt",
14
- "memory",
15
- "skill",
16
- "subagent",
17
- ];
18
- /**
19
- * Provenance marker for entries pinned from the profile. Stale removal keys on
20
- * it, so agent-written (`source: "agent"`) and user-written entries are never
21
- * touched.
22
- */
23
- export const PRIME_PROFILE_ENTRY_SOURCE = "profile";
24
- /**
25
- * Provenance markers written by executors that predate this shared lowering.
26
- *
27
- * Pruning keys on the source tag, so an executor that switches to the shared
28
- * implementation would otherwise leave its own earlier entries behind forever:
29
- * they match no current profile and no current source, and the model's
30
- * spawnable roster silently accumulates duplicates of every subagent. They are
31
- * adopted (pruned or replaced) on the first shared run instead.
32
- */
33
- export const PRIME_LEGACY_PROFILE_ENTRY_SOURCES = [
34
- "cli-bridge-profile",
35
- ];
36
- /** True when the controls materialize anything at all. */
37
- export function hasPrimeProfileMaterial(controls) {
38
- return (controls.systemPrompt !== undefined ||
39
- controls.appendSystemPrompt !== undefined ||
40
- primeProfileNeedsAgentDir(controls) ||
41
- controls.skills.length > 0);
42
- }
43
- /**
44
- * True when the controls must WRITE INTO the agent dir itself.
45
- *
46
- * Prompt files and skill dirs are named by a flag, so they can live anywhere
47
- * the agent can read (`--system-prompt` / `--append-system-prompt` resolve a
48
- * path when it exists, fork resource-loader.ts:41-56; `--skill` takes an
49
- * arbitrary dir, :437-442). The context file and the continual-harness store
50
- * are located BY the agent dir and have no flag, so they cannot move.
51
- *
52
- * An executor pointed at an agent dir it does not own (an operator's shared
53
- * dir) uses this to refuse exactly the controls it would have to write there,
54
- * while still binding the ones it can materialize elsewhere.
55
- */
56
- export function primeProfileNeedsAgentDir(controls) {
57
- return controls.instructions !== undefined || controls.subagents.length > 0;
58
- }
59
- /**
60
- * Read one instruction block, which the profile may carry inline or as a
61
- * resource ref. Only inline refs are accepted: resolving a GitHub-backed ref
62
- * fetches remote content, which is `resolveAgentProfileResources`' job and
63
- * must happen before a profile reaches an executor.
64
- */
65
- function primeInstructionBlock(value, control) {
66
- if (typeof value === "string")
67
- return value;
68
- if (value.kind === "inline")
69
- return value.content;
70
- rejectPrimeControl(control, "only inline resource refs are accepted — resolve GitHub-backed refs " +
71
- "(resolveAgentProfileResources) before handing the profile to this lowering.");
72
- }
73
- /**
74
- * Validate and extract the profile controls the prime agent-dir lowering
75
- * materializes. Throws {@link PrimeProfileError} on any control (or control
76
- * facet) the fork has no verified read path for — partial acceptance would
77
- * silently drop the rest.
78
- */
79
- export function readPrimeProfileControls(profile) {
80
- const controls = { skills: [], subagents: [] };
81
- if (!profile)
82
- return controls;
83
- rejectUnboundPrimeControls(profile);
84
- const instructionBlocks = [];
85
- if (profile.prompt) {
86
- readPrimePromptControls(profile.prompt, controls, instructionBlocks);
87
- }
88
- if (profile.resources) {
89
- readPrimeResourceControls(profile.resources, controls, instructionBlocks);
90
- }
91
- if (instructionBlocks.length > 0) {
92
- controls.instructions = instructionBlocks.join("\n\n");
93
- }
94
- if (profile.subagents) {
95
- readPrimeSubagents(profile.subagents, controls);
96
- }
97
- return controls;
98
- }
99
- /** Refuse every profile control prime-agent has no verified read path for. */
100
- function rejectUnboundPrimeControls(profile) {
101
- if (profile.harness !== undefined && profile.harness !== "prime") {
102
- rejectPrimeDeclaration("harness", `prime cannot run a profile pinned to harness "${profile.harness}" — ` +
103
- 'only "prime" (or an unset harness preference) is accepted.');
104
- }
105
- // An entry with `enabled: false` is OFF, not requested. A fully disabled map
106
- // asks for nothing, so accepting it drops no control — which is the only
107
- // thing this refusal exists to prevent.
108
- const requestedMcp = Object.keys(activeMcpServers(profile.mcp));
109
- if (requestedMcp.length > 0) {
110
- rejectPrimeControl(`mcp: ${requestedMcp.join(", ")}`, PRIME_MCP_UNSUPPORTED_REASON);
111
- }
112
- if (profile.connections !== undefined) {
113
- rejectPrimeControl("connections", `a hub connection grant resolves to MCP tools, and ${PRIME_MCP_UNSUPPORTED_REASON}`);
114
- }
115
- if (profile.permissions !== undefined) {
116
- rejectPrimeControl("permissions", "prime-agent exposes no permission control at all (full flag scan of fork " +
117
- "cli/args.ts:93-295; native tools run unconditionally), so a permission map " +
118
- "would be accepted and never enforced.");
119
- }
120
- if (profile.tools !== undefined) {
121
- rejectPrimeControl("tools", "the only per-run tool control is `--tools` over the single builtin `ipython` " +
122
- "(fork cli/args.ts:63-64,156-167); bash/edit and the rest are extension-provided " +
123
- "and not addressable per run, so a harness-neutral tool map cannot bind to it.");
124
- }
125
- if (profile.hooks !== undefined) {
126
- rejectPrimeControl("hooks", "prime-agent has no hook mechanism (fork cli/args.ts flag scan; extensions are " +
127
- "host-trusted JS modules loaded into the agent process, not caller command hooks).");
128
- }
129
- if (profile.modes !== undefined) {
130
- rejectPrimeControl("modes", "prime-agent has no mode concept (fork cli/args.ts flag scan).");
131
- }
132
- if (profile.confidential !== undefined) {
133
- rejectPrimeControl("confidential", "confidential execution is a property of the executor's isolation boundary, not " +
134
- "of the agent dir; this lowering cannot establish it.");
135
- }
136
- }
137
- function readPrimePromptControls(prompt, controls, instructionBlocks) {
138
- const { systemPrompt, appendSystemPrompt, instructions } = prompt;
139
- const knownPromptFields = new Set([
140
- "systemPrompt",
141
- "appendSystemPrompt",
142
- "instructions",
143
- ]);
144
- const unknown = Object.entries(prompt)
145
- .filter(([key, value]) => !knownPromptFields.has(key) && value !== undefined)
146
- .map(([key]) => key);
147
- if (unknown.length > 0) {
148
- rejectPrimeControl(`prompt fields: ${unknown.join(", ")}`, "no verified prime-agent read path exists for them.");
149
- }
150
- if (systemPrompt !== undefined) {
151
- if (typeof systemPrompt !== "string" || systemPrompt.trim() === "") {
152
- rejectPrimeDeclaration("prompt.systemPrompt", "an empty replacement would delete the built-in system prompt and " +
153
- "install nothing.");
154
- }
155
- controls.systemPrompt = systemPrompt;
156
- }
157
- if (appendSystemPrompt !== undefined) {
158
- if (typeof appendSystemPrompt !== "string" ||
159
- appendSystemPrompt.trim() === "") {
160
- rejectPrimeDeclaration("prompt.appendSystemPrompt", "must be non-empty text.");
161
- }
162
- controls.appendSystemPrompt = appendSystemPrompt;
163
- }
164
- for (const block of instructions ?? []) {
165
- if (typeof block === "string" && block.trim() !== "")
166
- instructionBlocks.push(block);
167
- }
168
- }
169
- function readPrimeResourceControls(resources, controls, instructionBlocks) {
170
- const { skills, instructions, failOnError, ...unsupported } = resources;
171
- const rejectedKeys = Object.entries(unsupported)
172
- .filter(([, value]) => value !== undefined)
173
- .map(([key]) => key);
174
- if (rejectedKeys.length > 0) {
175
- rejectPrimeControl(`resources: ${rejectedKeys.join(", ")}`, "only resources.skills (--skill dirs, fork resource-loader.ts:437-442) and " +
176
- "resources.instructions (agent-dir AGENTS.md, resource-loader.ts:86) have a " +
177
- "verified prime-agent read path. Generic workspace files, tool files, " +
178
- "subagent-definition files and prompt templates either have no headless read " +
179
- "path or would run caller-authored code inside the agent process.");
180
- }
181
- if (failOnError === false) {
182
- rejectPrimeControl("resources.failOnError: false", "this lowering always fails loud on a resource it cannot " +
183
- "materialize; lenient materialization is not implemented.");
184
- }
185
- if (instructions !== undefined) {
186
- const block = primeInstructionBlock(instructions, "resources.instructions");
187
- if (block.trim() !== "")
188
- instructionBlocks.push(block);
189
- }
190
- const seen = new Set();
191
- for (const [index, ref] of (skills ?? []).entries()) {
192
- if (ref.kind !== "inline") {
193
- rejectPrimeControl(`resources.skills[${index}]`, "only inline skill refs are accepted — resolve GitHub-backed refs " +
194
- "(resolveAgentProfileResources) before handing the profile to " +
195
- "this lowering.");
196
- }
197
- const name = ref.name;
198
- if (!PRIME_SKILL_NAME_RE.test(name) ||
199
- name.length > PRIME_MAX_SKILL_NAME_LENGTH) {
200
- rejectPrimeDeclaration(`resources.skills[${index}]`, `name ${JSON.stringify(name)} is not a valid prime skill name — it becomes the ` +
201
- "skill directory name, so it must be lowercase a-z0-9 with single hyphens, at " +
202
- `most ${PRIME_MAX_SKILL_NAME_LENGTH} chars (fork skills.ts:122-147).`);
203
- }
204
- if (seen.has(name)) {
205
- rejectPrimeDeclaration("resources.skills", `declares ${JSON.stringify(name)} twice; duplicate skill names collide in the ` +
206
- "fork's skill registry, which keeps only the first and warns.");
207
- }
208
- seen.add(name);
209
- if (ref.content.trim() === "") {
210
- rejectPrimeDeclaration(`resources.skills[${index}]`, `skill ${JSON.stringify(name)} declares no content.`);
211
- }
212
- controls.skills.push({ name, content: ref.content });
213
- }
214
- }
215
- function readPrimeSubagents(subagents, controls) {
216
- const usedIds = new Map();
217
- for (const [name, spec] of Object.entries(subagents)) {
218
- if (name.trim() === "") {
219
- rejectPrimeDeclaration("subagents", "a subagent name must be non-empty.");
220
- }
221
- const inexpressible = [
222
- ["model", spec.model],
223
- ["tools", spec.tools],
224
- ["permissions", spec.permissions],
225
- ["maxSteps", spec.maxSteps],
226
- ]
227
- .filter(([, value]) => value !== undefined)
228
- .map(([key]) => key);
229
- if (inexpressible.length > 0) {
230
- rejectPrimeControl(`subagents[${JSON.stringify(name)}] fields: ${inexpressible.join(", ")}`, "a prime continual-harness subagent spec is prompt-roster " +
231
- "guidance for `await rlm('<task>')` (fork refinement.ts:483-496) " +
232
- "and pins no model, tool set, permission, or step budget on the " +
233
- "spawned child.");
234
- }
235
- if (!spec.description?.trim() && !spec.prompt?.trim()) {
236
- rejectPrimeDeclaration(`subagents[${JSON.stringify(name)}]`, "declares no behavior — a spec needs a description and/or prompt, since an " +
237
- "empty entry gives the model an empty roster line.");
238
- }
239
- const id = primeSubagentEntryId(name);
240
- const clash = usedIds.get(id);
241
- if (clash !== undefined) {
242
- rejectPrimeDeclaration(`subagents[${JSON.stringify(name)}]`, `collides with subagent ${JSON.stringify(clash)} on harness-state id ` +
243
- `${JSON.stringify(id)}.`);
244
- }
245
- usedIds.set(id, name);
246
- controls.subagents.push({
247
- name,
248
- description: spec.description,
249
- prompt: spec.prompt,
250
- metadata: spec.metadata,
251
- });
252
- }
253
- }
254
- /**
255
- * Refuse a run whose selected model disagrees with the profile's own.
256
- *
257
- * One authority per run: a profile names the model it was written and scored
258
- * against, so running it on a different one silently scores a different agent.
259
- * Both sides go through {@link modelIdsMatch}, so the qualification level a
260
- * caller happened to use is not a conflict.
261
- *
262
- * `provider` is the executor's PROVIDER SELECTION, and is compared only when
263
- * the executor's provider is part of model identity. An executor whose
264
- * provider key is merely a namespace inside the agent dir passes it as
265
- * undefined and gets the model-name comparison alone.
266
- */
267
- export function assertPrimeModelAgreement(profile, selected) {
268
- const declared = profile?.model?.default;
269
- const declaredProvider = profile?.model?.provider;
270
- if (declared !== undefined) {
271
- const selectedId = qualifyModelId(selected.provider, selected.model);
272
- const declaredId = qualifyModelId(declaredProvider, declared);
273
- if (!modelIdsMatch(selectedId, declaredId, selected.provider)) {
274
- rejectPrimeDeclaration("model.default", `the run selects ${JSON.stringify(selectedId)} but the profile names ` +
275
- `${JSON.stringify(declaredId)}; a profile must not be scored against a model it ` +
276
- "does not name.");
277
- }
278
- return;
279
- }
280
- if (selected.provider !== undefined &&
281
- declaredProvider !== undefined &&
282
- declaredProvider !== selected.provider) {
283
- rejectPrimeDeclaration("model.provider", `the run selects provider ${JSON.stringify(selected.provider)} but the profile names ` +
284
- `${JSON.stringify(declaredProvider)}.`);
285
- }
286
- }
287
- /** Deterministic harness-state entry id for a profile subagent name. */
288
- export function primeSubagentEntryId(name) {
289
- const slug = name
290
- .toLowerCase()
291
- .replace(/[^a-z0-9]+/g, "-")
292
- .replace(/^-+|-+$/g, "");
293
- return `profile-${slug || "subagent"}`;
294
- }
295
- import { modelIdsMatch, qualifyModelId } from "./model-id.js";
296
- import { PRIME_MCP_UNSUPPORTED_REASON, rejectPrimeControl, rejectPrimeDeclaration, } from "./prime-fork.js";
297
- import { activeMcpServers } from "./profile-support.js";
@@ -1,37 +0,0 @@
1
- import { type ProfileBundleCache, type ProfileBundleClaim, type ProfileBundleClaimResult, type ProfileBundleManifest } from "./profile-bundle-key.js";
2
- export * from "./profile-bundle-key.js";
3
- export { evictProfileBundles, profileBundleManifestPath, sweepProfileBundleCache, } from "./profile-bundle-lru.js";
4
- /**
5
- * Admit a just-delivered bundle to the host cache, all-or-nothing. Every
6
- * object's bytes are verified against its content hash BEFORE anything is
7
- * renamed into place; a mismatch throws with nothing published. Objects
8
- * already present are re-verified against the incoming bytes, then the temp
9
- * copy is dropped — the existing object was verified when it was admitted.
10
- * The manifest is written LAST as the commit point, followed by lru update
11
- * and eviction down to the default cap.
12
- */
13
- export declare function admitProfileBundle(root: string, manifest: ProfileBundleManifest, readObject: (sha256: string) => Promise<Uint8Array> | Uint8Array): Promise<void>;
14
- /**
15
- * Claim a bundle from a (possibly read-only mounted) cache. Verify-on-read:
16
- * every object's bytes are re-hashed and compared to BOTH the manifest entry
17
- * and the claim's expected sha256 before `linkFile` is called — a read-only
18
- * mount must never serve unverified bytes. `linkFile` returning false sends
19
- * the file to `missing` (the caller delivers it normally). Status is "hit"
20
- * only when nothing is missing; a partial bundle is NEVER a hit. The only
21
- * write is a best-effort lru touch, wrapped so EROFS on a read-only mount is
22
- * harmless.
23
- */
24
- export declare function claimProfileBundle(root: string, claim: ProfileBundleClaim, linkFile: (objectPath: string, entry: {
25
- mountPath: string;
26
- target: string;
27
- sha256: string;
28
- mode: number;
29
- }) => Promise<boolean>): Promise<ProfileBundleClaimResult>;
30
- /**
31
- * Open a host-side cache handle over `root`, creating the layout and sweeping
32
- * stale `incoming/` residue synchronously when the root is idle. A busy root
33
- * leaves cleanup to its serialized admissions and sweeps.
34
- */
35
- export declare function openProfileBundleCache(root: string, options?: {
36
- maxBytes?: number;
37
- }): ProfileBundleCache;
@@ -1,320 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { mkdirSync, readdirSync, rmSync } from "node:fs";
3
- import { mkdir, open, readFile, rename, rm, stat, writeFile, } from "node:fs/promises";
4
- import { join } from "node:path";
5
- import { PROFILE_BUNDLE_DEFAULT_MAX_BYTES, PROFILE_BUNDLE_MAX_BYTES, PROFILE_BUNDLE_MAX_FILE_BYTES, PROFILE_BUNDLE_MAX_FILES, PROFILE_BUNDLE_SCHEMA, } from "./profile-bundle-key.js";
6
- import { evictProfileBundles, evictUnlocked, fsyncDir, hasPendingRootWork, loadLru, pathExistsFollowingLinks, profileBundleManifestPath, readManifestFile, saveLru, sweepProfileBundleCache, sweepUnlocked, withRootLock, } from "./profile-bundle-lru.js";
7
- import { hasControlCharacter, normalizeWorkspaceRelativePath, validateFileMode, } from "./workspace-files.js";
8
- export * from "./profile-bundle-key.js";
9
- export { evictProfileBundles, profileBundleManifestPath, sweepProfileBundleCache, } from "./profile-bundle-lru.js";
10
- const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
11
- const sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
12
- const nowIso = () => new Date().toISOString();
13
- /**
14
- * Content-addressed, per-host cache of delivered profile-file bundles.
15
- *
16
- * A production cold sandbox start spends seconds re-delivering the same inline
17
- * profile file mounts through the box file API on every fresh box. The host
18
- * owns a cache directory (mounted read-only into boxes) of immutable,
19
- * content-addressed file objects plus per-bundle manifests. A box "claims" a
20
- * bundle by verifying every object against its content hash and hardlinking it
21
- * into the workspace, skipping network delivery; after a normal miss-delivery
22
- * the box-side publisher "admits" the just-delivered bytes to the host.
23
- *
24
- * Durability invariants:
25
- * - `objects/<sha256>` bytes are immutable and self-certifying: the filename
26
- * IS the SHA-256 of the bytes. Objects are written to a unique `incoming/`
27
- * dir and renamed into place only after every byte is verified, so a crash
28
- * mid-admit leaves no half-published object behind.
29
- * - `bundles/<key>.json` is the commit point: readers treat a bundle as
30
- * present only if its manifest exists, and verify every object anyway
31
- * (verify-on-read), so a read-only or partially corrupt cache mount can
32
- * never serve unverified bytes.
33
- * - `lru.json` is advisory accounting only; corruption never breaks admit or
34
- * claim (see profile-bundle-lru.ts).
35
- */
36
- function validateManifestShape(manifest) {
37
- if (manifest.schema !== PROFILE_BUNDLE_SCHEMA) {
38
- throw new Error(`Profile bundle manifest schema mismatch: ${String(manifest.schema)}`);
39
- }
40
- if (typeof manifest.bundle !== "string" || manifest.bundle.length === 0) {
41
- throw new Error("Profile bundle manifest requires a non-empty bundle key");
42
- }
43
- if (typeof manifest.createdAt !== "string") {
44
- throw new Error("Profile bundle manifest requires an ISO createdAt");
45
- }
46
- if (!Number.isInteger(manifest.sizeBytes) ||
47
- manifest.sizeBytes < 0 ||
48
- manifest.sizeBytes > PROFILE_BUNDLE_MAX_BYTES) {
49
- throw new Error(`Profile bundle sizeBytes exceeds ${PROFILE_BUNDLE_MAX_BYTES}: ${manifest.sizeBytes}`);
50
- }
51
- if (!Array.isArray(manifest.files)) {
52
- throw new Error("Profile bundle manifest requires a files array");
53
- }
54
- }
55
- /**
56
- * Reject anything that could link outside the cache or the workspace:
57
- * mount paths must already be canonical workspace-relative paths, and targets
58
- * must not contain traversal segments (targets MAY be absolute — the staged
59
- * manifest's `path` can be a file-API absolute prefix — but never `..`).
60
- */
61
- function validateTarget(target) {
62
- if (typeof target !== "string" || target.length === 0) {
63
- throw new Error("Profile bundle file requires a non-empty target");
64
- }
65
- if (hasControlCharacter(target)) {
66
- throw new Error(`Profile bundle target has control characters: ${target}`);
67
- }
68
- const segments = target.replace(/\\/g, "/").split("/");
69
- if (segments.some((segment) => segment === "..")) {
70
- throw new Error(`Profile bundle target must not traverse: ${target}`);
71
- }
72
- return target;
73
- }
74
- function validateManifestFiles(manifest) {
75
- if (manifest.files.length > PROFILE_BUNDLE_MAX_FILES) {
76
- throw new Error(`Profile bundle has ${manifest.files.length} files; limit is ${PROFILE_BUNDLE_MAX_FILES}`);
77
- }
78
- const mountPaths = new Set();
79
- const targets = new Set();
80
- const entries = [];
81
- for (const file of manifest.files) {
82
- if (normalizeWorkspaceRelativePath(file.mountPath) !== file.mountPath) {
83
- throw new Error(`Profile bundle mount path must be canonical: ${file.mountPath}`);
84
- }
85
- const target = validateTarget(file.target);
86
- if (!SHA256_HEX_RE.test(file.sha256)) {
87
- throw new Error(`Profile bundle sha256 must be lowercase hex without prefix: ${file.sha256}`);
88
- }
89
- const mode = validateFileMode(file.mode);
90
- if (mountPaths.has(file.mountPath)) {
91
- throw new Error(`Profile bundle has duplicate mount path: ${file.mountPath}`);
92
- }
93
- if (targets.has(target)) {
94
- throw new Error(`Profile bundle has duplicate target: ${target}`);
95
- }
96
- mountPaths.add(file.mountPath);
97
- targets.add(target);
98
- entries.push({
99
- mountPath: file.mountPath,
100
- sha256: file.sha256,
101
- mode,
102
- target,
103
- });
104
- }
105
- return entries;
106
- }
107
- /**
108
- * Admit a just-delivered bundle to the host cache, all-or-nothing. Every
109
- * object's bytes are verified against its content hash BEFORE anything is
110
- * renamed into place; a mismatch throws with nothing published. Objects
111
- * already present are re-verified against the incoming bytes, then the temp
112
- * copy is dropped — the existing object was verified when it was admitted.
113
- * The manifest is written LAST as the commit point, followed by lru update
114
- * and eviction down to the default cap.
115
- */
116
- export async function admitProfileBundle(root, manifest, readObject) {
117
- validateManifestShape(manifest);
118
- const entries = validateManifestFiles(manifest);
119
- await mkdir(join(root, "objects"), { recursive: true });
120
- await mkdir(join(root, "bundles"), { recursive: true });
121
- await mkdir(join(root, "incoming"), { recursive: true });
122
- await withRootLock(root, async () => {
123
- await sweepUnlocked(root);
124
- const manifestPath = profileBundleManifestPath(root, manifest.bundle);
125
- const existing = await readManifestFile(manifestPath);
126
- if (existing && existing.bundle !== manifest.bundle) {
127
- throw new Error(`Profile bundle key collides with an admitted bundle: ${manifest.bundle}`);
128
- }
129
- const incoming = join(root, "incoming", randomUUID());
130
- await mkdir(incoming, { recursive: true });
131
- try {
132
- const staged = [];
133
- let totalBytes = 0;
134
- for (const entry of entries) {
135
- const bytes = await readObject(entry.sha256);
136
- const buffer = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
137
- if (buffer.byteLength > PROFILE_BUNDLE_MAX_FILE_BYTES) {
138
- throw new Error(`Profile bundle file ${entry.mountPath} exceeds ${PROFILE_BUNDLE_MAX_FILE_BYTES} bytes`);
139
- }
140
- if (sha256Hex(buffer) !== entry.sha256) {
141
- throw new Error(`Profile bundle object hash mismatch for ${entry.mountPath}`);
142
- }
143
- totalBytes += buffer.byteLength;
144
- const object = join(root, "objects", entry.sha256);
145
- if (await pathExistsFollowingLinks(object)) {
146
- // Already admitted; the bytes above were re-verified, so drop the temp.
147
- continue;
148
- }
149
- const temporary = join(incoming, entry.sha256);
150
- const handle = await open(temporary, "w", (entry.mode & 0o100) !== 0 ? 0o755 : 0o644);
151
- try {
152
- await handle.write(buffer);
153
- await handle.sync();
154
- }
155
- finally {
156
- await handle.close();
157
- }
158
- staged.push({ temporary, object });
159
- }
160
- if (totalBytes > PROFILE_BUNDLE_MAX_BYTES) {
161
- throw new Error(`Profile bundle exceeds ${PROFILE_BUNDLE_MAX_BYTES} bytes: ${totalBytes}`);
162
- }
163
- for (const { temporary, object } of staged) {
164
- await rename(temporary, object);
165
- }
166
- await fsyncDir(join(root, "objects"));
167
- const stagedManifest = join(incoming, "manifest.json");
168
- await writeFile(stagedManifest, JSON.stringify(manifest));
169
- const manifestHandle = await open(stagedManifest, "r");
170
- try {
171
- await manifestHandle.sync();
172
- }
173
- finally {
174
- await manifestHandle.close();
175
- }
176
- await rename(stagedManifest, manifestPath);
177
- await fsyncDir(join(root, "bundles"));
178
- const lru = await loadLru(root);
179
- const used = nowIso();
180
- for (const entry of entries) {
181
- lru.objects[entry.sha256] = {
182
- bytes: (await stat(join(root, "objects", entry.sha256))).size,
183
- lastUsed: used,
184
- };
185
- }
186
- lru.bundles[manifest.bundle] = {
187
- bytes: totalBytes,
188
- lastUsed: used,
189
- createdAt: manifest.createdAt,
190
- };
191
- await saveLru(root, lru);
192
- }
193
- finally {
194
- await rm(incoming, { recursive: true, force: true });
195
- }
196
- await evictUnlocked(root, PROFILE_BUNDLE_DEFAULT_MAX_BYTES);
197
- });
198
- }
199
- /**
200
- * Claim a bundle from a (possibly read-only mounted) cache. Verify-on-read:
201
- * every object's bytes are re-hashed and compared to BOTH the manifest entry
202
- * and the claim's expected sha256 before `linkFile` is called — a read-only
203
- * mount must never serve unverified bytes. `linkFile` returning false sends
204
- * the file to `missing` (the caller delivers it normally). Status is "hit"
205
- * only when nothing is missing; a partial bundle is NEVER a hit. The only
206
- * write is a best-effort lru touch, wrapped so EROFS on a read-only mount is
207
- * harmless.
208
- */
209
- export async function claimProfileBundle(root, claim, linkFile) {
210
- const missAll = () => ({
211
- status: "miss",
212
- missing: claim.files.map((file) => file.mountPath),
213
- });
214
- const manifest = await readManifestFile(profileBundleManifestPath(root, claim.bundle));
215
- if (!manifest ||
216
- manifest.schema !== PROFILE_BUNDLE_SCHEMA ||
217
- manifest.bundle !== claim.bundle ||
218
- !Array.isArray(manifest.files)) {
219
- return missAll();
220
- }
221
- let entries;
222
- try {
223
- entries = validateManifestFiles(manifest);
224
- }
225
- catch {
226
- return missAll();
227
- }
228
- const expected = new Map();
229
- for (const file of claim.files) {
230
- // Fail closed: a claim with a non-canonical path is rejected outright so a
231
- // tampered claim can never link outside the workspace.
232
- if (normalizeWorkspaceRelativePath(file.mountPath) !== file.mountPath) {
233
- throw new Error(`Profile bundle claim path is not canonical: ${file.mountPath}`);
234
- }
235
- if (!SHA256_HEX_RE.test(file.sha256)) {
236
- throw new Error(`Profile bundle claim sha256 is not hex: ${file.sha256}`);
237
- }
238
- if (expected.has(file.mountPath)) {
239
- throw new Error(`Profile bundle claim has duplicate mount path: ${file.mountPath}`);
240
- }
241
- expected.set(file.mountPath, file.sha256);
242
- }
243
- const tableMatches = entries.length === expected.size &&
244
- entries.every((entry) => expected.get(entry.mountPath) === entry.sha256);
245
- if (!tableMatches) {
246
- return missAll();
247
- }
248
- const linked = [];
249
- const missing = [];
250
- for (const entry of entries) {
251
- const objectPath = join(root, "objects", entry.sha256);
252
- let bytes;
253
- try {
254
- bytes = await readFile(objectPath);
255
- }
256
- catch {
257
- missing.push(entry.mountPath);
258
- continue;
259
- }
260
- if (sha256Hex(bytes) !== entry.sha256 ||
261
- entry.sha256 !== expected.get(entry.mountPath)) {
262
- missing.push(entry.mountPath);
263
- continue;
264
- }
265
- if (await linkFile(objectPath, entry)) {
266
- linked.push(entry.mountPath);
267
- }
268
- else {
269
- missing.push(entry.mountPath);
270
- }
271
- }
272
- if (missing.length > 0)
273
- return { status: "miss", missing };
274
- try {
275
- await withRootLock(root, async () => {
276
- const lru = await loadLru(root);
277
- const used = nowIso();
278
- for (const entry of entries) {
279
- const object = lru.objects[entry.sha256];
280
- if (object)
281
- object.lastUsed = used;
282
- }
283
- const bundleEntry = lru.bundles[claim.bundle];
284
- if (bundleEntry)
285
- bundleEntry.lastUsed = used;
286
- await saveLru(root, lru);
287
- });
288
- }
289
- catch {
290
- // Best-effort accounting on a possibly read-only mount.
291
- }
292
- return { status: "hit", linked };
293
- }
294
- /**
295
- * Open a host-side cache handle over `root`, creating the layout and sweeping
296
- * stale `incoming/` residue synchronously when the root is idle. A busy root
297
- * leaves cleanup to its serialized admissions and sweeps.
298
- */
299
- export function openProfileBundleCache(root, options) {
300
- const maxBytes = options?.maxBytes ?? PROFILE_BUNDLE_DEFAULT_MAX_BYTES;
301
- if (!Number.isFinite(maxBytes) || maxBytes < 0) {
302
- throw new Error(`Profile bundle cache maxBytes must be >= 0: ${maxBytes}`);
303
- }
304
- mkdirSync(join(root, "objects"), { recursive: true });
305
- mkdirSync(join(root, "bundles"), { recursive: true });
306
- const incoming = join(root, "incoming");
307
- mkdirSync(incoming, { recursive: true });
308
- if (!hasPendingRootWork(root)) {
309
- for (const entry of readdirSync(incoming)) {
310
- rmSync(join(incoming, entry), { recursive: true, force: true });
311
- }
312
- }
313
- return {
314
- root,
315
- maxBytes,
316
- admit: (manifest, readObject) => admitProfileBundle(root, manifest, readObject),
317
- evict: () => evictProfileBundles(root, maxBytes),
318
- sweep: () => sweepProfileBundleCache(root),
319
- };
320
- }