@tangle-network/agent-profile-materialize 0.27.0 → 0.27.2
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/execution-controls.js +119 -111
- package/dist/harness.d.ts +0 -25
- package/dist/harness.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/managed-workspace-bind.js +4 -5
- package/dist/managed-workspace-stage.d.ts +2 -1
- package/dist/mcp-config-ownership.d.ts +0 -18
- package/dist/mcp-config-ownership.js +4 -4
- package/dist/mcp-opencode.d.ts +25 -0
- package/dist/mcp-opencode.js +52 -0
- package/dist/mcp-timeouts.d.ts +7 -0
- package/dist/mcp-timeouts.js +48 -0
- package/dist/mcp.d.ts +1 -36
- package/dist/mcp.js +119 -201
- package/dist/plan-json.d.ts +2 -7
- package/dist/plan-json.js +3 -3
- package/dist/plan-normalize.d.ts +0 -3
- package/dist/plan-types.d.ts +1 -1
- package/dist/prime-profile.js +124 -111
- package/dist/profile-bundle-cache.js +2 -2
- package/dist/profile-bundle-lru.d.ts +3 -2
- package/dist/profile-bundle-lru.js +2 -2
- package/dist/secret-concurrent-revision.js +45 -29
- package/dist/workspace-apply-publish.d.ts +26 -0
- package/dist/workspace-apply-publish.js +178 -0
- package/dist/workspace-apply.js +60 -196
- package/package.json +4 -4
package/dist/workspace-apply.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
268
|
-
import {
|
|
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 {
|
|
275
|
-
import {
|
|
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.
|
|
3
|
+
"version": "0.27.2",
|
|
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": "
|
|
39
|
-
"mergedAt": "2026-10-
|
|
38
|
+
"sourceSha": "2d91c4f5a94e21acebf0525b7e3644b172cb368f",
|
|
39
|
+
"mergedAt": "2026-10-22T19:47:47-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",
|