@tangle-network/agent-profile-materialize 0.27.1 → 0.27.3

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.
@@ -1,4 +1,8 @@
1
- export function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDir, options, publicPlanDigest = hashWorkspacePlan(publicPlan)) {
1
+ const receiptFile = (file) => ({
2
+ root: file.root,
3
+ relPath: file.relPath,
4
+ });
5
+ function readApplyInput(publicPlan, workspaceDir, options) {
2
6
  const workspaceRoot = resolve(workspaceDir);
3
7
  if (options.agentDir !== undefined && !isAbsolute(options.agentDir)) {
4
8
  throw new Error("ApplyWorkspacePlanOptions.agentDir must be absolute");
@@ -9,11 +13,6 @@ export function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDi
9
13
  throw new Error("Workspace plan contains agent-root files but ApplyWorkspacePlanOptions.agentDir is missing");
10
14
  }
11
15
  const rootFor = (root) => root === "workspace" ? workspaceRoot : agentRoot;
12
- const fileIdentity = (file) => `${file.root}\0${file.relPath}`;
13
- const receiptFile = (file) => ({
14
- root: file.root,
15
- relPath: file.relPath,
16
- });
17
16
  const runtimeOnlyFiles = new Set((options.runtimeOnlyFiles ?? []).map(normalizeWorkspaceRelativePath));
18
17
  const ownership = validateWorkspaceOwnership(options.ownership);
19
18
  const privateFilePaths = new Set(publicFiles.filter((file) => file.secretSlots.length > 0).map(fileIdentity));
@@ -21,6 +20,43 @@ export function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDi
21
20
  if (existingFiles !== "allow-identical" && existingFiles !== "reject") {
22
21
  throw new Error(`Unsupported existing workspace file policy: ${existingFiles}`);
23
22
  }
23
+ return {
24
+ rootFor,
25
+ runtimeOnlyFiles,
26
+ ownership,
27
+ privateFilePaths,
28
+ existingFiles,
29
+ };
30
+ }
31
+ // A prior application of this exact plan makes the workspace live agent
32
+ // state. Preserve changed and removed files. Refresh private generated files
33
+ // because the public digest excludes secret values, and repair unchanged
34
+ // materializer files that still have the writer's ownership.
35
+ function selectResumeFiles(files, ownership, privateFilePaths) {
36
+ return files.filter((file) => {
37
+ if (file.ownership !== "materializer")
38
+ return false;
39
+ assertNoSymlinkPathComponents(file.rootDir, file.absPath);
40
+ if (!workspaceFileExists(file.absPath))
41
+ return false;
42
+ const matches = workspaceFileMatches(file);
43
+ const hasCurrentOwnership = holdsCurrentOwnership(file.absPath, ownership);
44
+ if (privateFilePaths.has(fileIdentity(file))) {
45
+ return !matches || !hasCurrentOwnership;
46
+ }
47
+ return matches && !hasCurrentOwnership;
48
+ });
49
+ }
50
+ function spawnFor(resolvedPlan) {
51
+ return {
52
+ env: { ...resolvedPlan.env },
53
+ flags: [...resolvedPlan.flags],
54
+ ...promptIntentFields(resolvedPlan, samePromptIntentValue),
55
+ ...systemPromptBindingField(resolvedPlan),
56
+ };
57
+ }
58
+ export function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDir, options, publicPlanDigest = hashWorkspacePlan(publicPlan)) {
59
+ const { rootFor, runtimeOnlyFiles, ownership, privateFilePaths, existingFiles, } = readApplyInput(publicPlan, workspaceDir, options);
24
60
  const resolvedFiles = resolvedPlan.files;
25
61
  const privateRuntimeFiles = Object.fromEntries(resolvedFiles
26
62
  .filter((file) => file.root === "workspace" && runtimeOnlyFiles.has(file.relPath))
@@ -32,78 +68,28 @@ export function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDi
32
68
  rootDir: rootFor(file.root),
33
69
  absPath: resolveWorkspaceFilePath(rootFor(file.root), file.relPath),
34
70
  }));
35
- // A prior application of this exact plan makes the workspace live agent
36
- // state. Preserve changed and removed files. Refresh private generated files
37
- // because the public digest excludes secret values, and repair unchanged
38
- // materializer files that still have the writer's ownership. `reject` does
39
- // not use this path because it requires an empty target.
71
+ // `reject` does not resume because it requires an empty target.
40
72
  const resumeSamePlan = existingFiles === "allow-identical" &&
41
73
  options.appliedPlanDigest !== undefined &&
42
74
  options.appliedPlanDigest === publicPlanDigest;
43
75
  const filesToApply = resumeSamePlan
44
- ? files.filter((file) => {
45
- if (file.ownership !== "materializer")
46
- return false;
47
- assertNoSymlinkPathComponents(file.rootDir, file.absPath);
48
- if (!workspaceFileExists(file.absPath))
49
- return false;
50
- const matches = workspaceFileMatches(file);
51
- const hasCurrentOwnership = ownership === undefined ||
52
- workspaceFileHasOwnership(file.absPath, ownership);
53
- if (privateFilePaths.has(fileIdentity(file))) {
54
- return !matches || !hasCurrentOwnership;
55
- }
56
- return matches && !hasCurrentOwnership;
57
- })
76
+ ? selectResumeFiles(files, ownership, privateFilePaths)
58
77
  : files;
59
78
  if (resumeSamePlan && filesToApply.length === 0) {
60
79
  return {
61
80
  receipt: safeWorkspacePlanReceipt(publicPlan, files.map(receiptFile), publicPlanDigest),
62
- spawn: {
63
- env: { ...resolvedPlan.env },
64
- flags: [...resolvedPlan.flags],
65
- ...promptIntentFields(resolvedPlan, samePromptIntentValue),
66
- ...systemPromptBindingField(resolvedPlan),
67
- },
81
+ spawn: spawnFor(resolvedPlan),
68
82
  runtimeOnlyFiles: privateRuntimeFiles,
69
83
  };
70
84
  }
71
- // An owned path holds this package's own earlier output, so re-applying
72
- // into the same workspace replaces it rather than failing the turn — once
73
- // the bytes already there prove that provenance. The replacement keeps what
74
- // an agent appended below a generated context file (its memory), so it can
75
- // differ from the planned bytes. `reject` still refuses: that policy is
76
- // "nothing may pre-exist here", which is a statement about the workspace,
77
- // not authorship.
78
- const plannedContent = new Map(filesToApply.map((file) => [fileIdentity(file), file.content]));
79
- const replacement = (file) => {
80
- const planned = plannedContent.get(fileIdentity(file));
81
- if (existingFiles !== "allow-identical" ||
82
- file.ownership !== "materializer" ||
83
- planned === undefined) {
84
- return undefined;
85
- }
86
- return materializerReplacement(file.relPath, () => regularFileContent(file.absPath), planned);
85
+ const context = {
86
+ rootFor,
87
+ existingFiles,
88
+ ownership,
89
+ publicPlanDigest,
90
+ plannedContent: new Map(filesToApply.map((file) => [fileIdentity(file), file.content])),
87
91
  };
88
- // Validate the complete plan before the first write so a late traversal or
89
- // existing symlink cannot leave a partially materialized workspace.
90
- const writes = [];
91
- for (const file of filesToApply) {
92
- assertNoSymlinkPathComponents(file.rootDir, file.absPath);
93
- if (!workspaceFileExists(file.absPath)) {
94
- writes.push(file);
95
- continue;
96
- }
97
- const content = replacement(file);
98
- if (content !== undefined) {
99
- writes.push({ ...file, content });
100
- continue;
101
- }
102
- if (existingFiles === "reject" || !workspaceFileMatches(file)) {
103
- throw new Error(workspaceFileConflictMessage(file, existingFiles, publicPlanDigest));
104
- }
105
- writes.push(file);
106
- }
92
+ const writes = planWrites(filesToApply, context);
107
93
  // Parent directories remain on failure because they may contain concurrent
108
94
  // workspace state that this call does not own.
109
95
  for (const file of writes) {
@@ -113,137 +99,17 @@ export function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDi
113
99
  assertNoSymlinkPathComponents(file.rootDir, file.absPath);
114
100
  }
115
101
  const prepared = [];
116
- // Prepared temp links stay alive through publication and rollback. Their
117
- // inodes distinguish files created here from concurrent replacements.
118
102
  try {
119
- for (const file of writes) {
120
- if (existingFiles === "allow-identical" &&
121
- workspaceFileExists(file.absPath) &&
122
- workspaceFileMatches(file) &&
123
- (file.ownership !== "materializer" ||
124
- ownership === undefined ||
125
- workspaceFileHasOwnership(file.absPath, ownership))) {
126
- continue;
127
- }
128
- prepared.push(prepareWorkspaceFile(file.rootDir, file, ownership));
129
- }
130
- for (const entry of prepared) {
131
- assertNoSymlinkPathComponents(rootFor(entry.file.root), entry.file.absPath);
132
- try {
133
- linkSync(entry.temporaryPath, entry.file.absPath);
134
- entry.published = true;
135
- }
136
- catch (error) {
137
- if (isFsError(error, "EEXIST") &&
138
- existingFiles === "allow-identical" &&
139
- workspaceFileMatches(entry.file) &&
140
- (entry.file.ownership !== "materializer" ||
141
- ownership === undefined ||
142
- workspaceFileHasOwnership(entry.file.absPath, ownership))) {
143
- continue;
144
- }
145
- if (isFsError(error, "EEXIST") &&
146
- replacement(entry.file) === entry.file.content) {
147
- // The replacement is recomposed from the bytes there now, so an
148
- // agent append that landed since validation refuses below instead
149
- // of being overwritten; one landing between that read and the
150
- // rename is not detected.
151
- // rename() replaces in one step, so no window exists in which the
152
- // path is missing and a concurrent spawn reads nothing. The staged
153
- // inode moves onto the path, which is what rollback identifies the
154
- // file by, so an abort still removes exactly what this call
155
- // published — leaving the path ABSENT rather than restored to the
156
- // superseded bytes. That is the fail-closed direction: the harness
157
- // whose flag names a missing file refuses to start, whereas silently
158
- // restored previous-turn bytes would run the wrong prompt. A replaced
159
- // file that holds an agent's appended text is the exception: it is
160
- // linked aside first and restored, because removing it would delete
161
- // the agent's own text.
162
- assertNoSymlinkPathComponents(rootFor(entry.file.root), entry.file.absPath);
163
- if (entry.file.content !== plannedContent.get(fileIdentity(entry.file))) {
164
- const superseded = join(dirname(entry.file.absPath), `.agent-profile-${randomUUID()}.superseded`);
165
- linkSync(entry.file.absPath, superseded);
166
- entry.superseded = superseded;
167
- }
168
- renameSync(entry.temporaryPath, entry.file.absPath);
169
- entry.published = true;
170
- continue;
171
- }
172
- if (isFsError(error, "EEXIST")) {
173
- throw new Error(workspaceFileConflictMessage(entry.file, existingFiles, publicPlanDigest), { cause: error });
174
- }
175
- throw error;
176
- }
177
- }
103
+ publishWrites(writes, prepared, context);
178
104
  }
179
105
  catch (error) {
180
- const cleanupErrors = [];
181
- for (const entry of [...prepared].reverse()) {
182
- try {
183
- if (entry.published &&
184
- workspacePathHasIdentity(entry.file.absPath, entry.identity)) {
185
- if (entry.superseded) {
186
- renameSync(entry.superseded, entry.file.absPath);
187
- }
188
- else {
189
- unlinkSync(entry.file.absPath);
190
- }
191
- }
192
- }
193
- catch (cleanupError) {
194
- cleanupErrors.push(cleanupError);
195
- }
196
- try {
197
- if (entry.superseded)
198
- unlinkSync(entry.superseded);
199
- }
200
- catch (cleanupError) {
201
- if (!isFsError(cleanupError, "ENOENT"))
202
- cleanupErrors.push(cleanupError);
203
- }
204
- try {
205
- if (workspacePathHasIdentity(entry.temporaryPath, entry.identity)) {
206
- unlinkSync(entry.temporaryPath);
207
- }
208
- }
209
- catch (cleanupError) {
210
- cleanupErrors.push(cleanupError);
211
- }
212
- }
213
- if (cleanupErrors.length > 0) {
214
- throw new AggregateError([error, ...cleanupErrors], "Failed to apply workspace plan and roll back published files");
215
- }
216
- throw error;
106
+ rollbackPublished(prepared, error);
217
107
  }
218
108
  const receipt = safeWorkspacePlanReceipt(publicPlan, files.map(receiptFile), publicPlanDigest);
219
- const cleanupErrors = [];
220
- const temporaryPaths = [];
221
- for (const entry of prepared) {
222
- for (const path of [entry.temporaryPath, entry.superseded]) {
223
- if (path === undefined)
224
- continue;
225
- try {
226
- unlinkSync(path);
227
- }
228
- catch (error) {
229
- if (!isFsError(error, "ENOENT")) {
230
- cleanupErrors.push(error);
231
- temporaryPaths.push(path);
232
- }
233
- }
234
- }
235
- }
236
- if (cleanupErrors.length > 0) {
237
- throw new WorkspacePlanCleanupError(cleanupErrors, receipt, temporaryPaths);
238
- }
109
+ removeTemporaries(prepared, receipt);
239
110
  return {
240
111
  receipt,
241
- spawn: {
242
- env: { ...resolvedPlan.env },
243
- flags: [...resolvedPlan.flags],
244
- ...promptIntentFields(resolvedPlan, samePromptIntentValue),
245
- ...systemPromptBindingField(resolvedPlan),
246
- },
112
+ spawn: spawnFor(resolvedPlan),
247
113
  runtimeOnlyFiles: privateRuntimeFiles,
248
114
  };
249
115
  }
@@ -264,13 +130,11 @@ function safeWorkspacePlanReceipt(plan, writtenFiles, workspacePlanDigest = hash
264
130
  writtenFiles: writtenFiles.map((file) => ({ ...file })),
265
131
  };
266
132
  }
267
- import { randomUUID } from "node:crypto";
268
- import { linkSync, mkdirSync, renameSync, unlinkSync } from "node:fs";
269
- import { dirname, isAbsolute, join, resolve } from "node:path";
270
- import { materializerReplacement } from "./harness.js";
133
+ import { mkdirSync } from "node:fs";
134
+ import { dirname, isAbsolute, resolve } from "node:path";
271
135
  import { hashWorkspacePlan } from "./plan-digest.js";
272
136
  import { normalizePlanFiles, normalizeWorkspacePlanArgument, } from "./plan-normalize.js";
273
137
  import { promptIntentFields, samePromptIntentValue, systemPromptBindingField, } from "./plan-prompt.js";
274
- import { WorkspacePlanCleanupError } from "./plan-types.js";
275
- import { assertNoSymlinkPathComponents, isFsError, prepareWorkspaceFile, regularFileContent, resolveWorkspaceFilePath, validateWorkspaceOwnership, workspaceFileConflictMessage, workspaceFileExists, workspaceFileHasOwnership, workspaceFileMatches, workspacePathHasIdentity, } from "./workspace-apply-files.js";
138
+ import { assertNoSymlinkPathComponents, resolveWorkspaceFilePath, validateWorkspaceOwnership, workspaceFileExists, workspaceFileMatches, } from "./workspace-apply-files.js";
139
+ import { fileIdentity, holdsCurrentOwnership, planWrites, publishWrites, removeTemporaries, rollbackPublished, } from "./workspace-apply-publish.js";
276
140
  import { normalizeWorkspaceRelativePath } from "./workspace-files.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.27.1",
3
+ "version": "0.27.3",
4
4
  "description": "Materialize an AgentProfile into the native per-harness workspace files (the one shared materializer for cli-bridge, agent-container, and VB).",
5
5
  "type": "module",
6
6
  "repository": {
@@ -35,12 +35,12 @@
35
35
  "@tangle-network/agent-interface": "^3.0.0 || ^4.0.0 || ^5.0.0"
36
36
  },
37
37
  "tangleRelease": {
38
- "sourceSha": "5a536a79fbd83dfa11da194bf5348cf0d73f224d",
39
- "mergedAt": "2026-10-21T05:59:51-06:00"
38
+ "sourceSha": "b66b40689425eb54bcb3c576be0ac54cf93770b1",
39
+ "mergedAt": "2026-10-23T18:31:26-06:00"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsc -p tsconfig.json",
43
- "check-types": "tsc --noEmit",
43
+ "check-types": "node ../../scripts/tsc-native.mjs --noEmit",
44
44
  "clean": "rm -rf dist",
45
45
  "test": "pnpm run test:installed-peer && pnpm run test:current-peer",
46
46
  "test:installed-peer": "vitest run",