@tangle-network/agent-profile-materialize 0.20.8 → 0.21.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/control-maps.d.ts +14 -1
- package/dist/control-maps.js +61 -25
- package/dist/execution-controls.js +19 -15
- package/dist/harness.d.ts +17 -20
- package/dist/harness.js +92 -39
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/profile-materializer.d.ts +10 -0
- package/dist/profile-materializer.js +19 -0
- package/dist/workspace-apply-files.d.ts +7 -0
- package/dist/workspace-apply-files.js +17 -3
- package/dist/workspace-apply.js +73 -24
- package/package.json +4 -4
package/dist/control-maps.d.ts
CHANGED
|
@@ -14,7 +14,15 @@ export declare const PI_EXECUTION_CONTROL_EXTENSION_PATH = ".tangle/pi-execution
|
|
|
14
14
|
* extension and MCP tools profile-configurable without a static catalog.
|
|
15
15
|
*/
|
|
16
16
|
export declare function piToolId(name: string): string | undefined;
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Map tool switches to native ids; spellings of one tool keep its explicit off.
|
|
19
|
+
*
|
|
20
|
+
* Products compose profiles in layers: a shared allowlist, then a product
|
|
21
|
+
* constraint that disables a native tool under its own spelling. Physim adds
|
|
22
|
+
* `webfetch: false` over an allowlist holding `web_fetch: true`. Off is the
|
|
23
|
+
* only value that honours every layer, so it wins regardless of entry order.
|
|
24
|
+
*/
|
|
25
|
+
export declare function mapSupportedBooleanEntries(entries: [string, boolean][], mapName: (name: string) => string | undefined, onUnsupported: (name: string) => void): [string, boolean][];
|
|
18
26
|
export declare function permissionAllowsEverything(policy: AgentProfilePermission): boolean;
|
|
19
27
|
export declare function mapSupportedPermissionEntries(entries: [string, AgentProfilePermission][], mapName: (name: string) => string | undefined, onUnsupported: (name: string) => void, label: string): [string, AgentProfilePermission][];
|
|
20
28
|
export declare function claudePermissionRules(entries: [string, AgentProfilePermission][]): {
|
|
@@ -22,6 +30,11 @@ export declare function claudePermissionRules(entries: [string, AgentProfilePerm
|
|
|
22
30
|
ask: string[];
|
|
23
31
|
deny: string[];
|
|
24
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* Give each Claude rule one decision. A tool switch and a permission can name
|
|
35
|
+
* the same rule; the stricter decision wins, as deny already beats allow
|
|
36
|
+
* inside Claude Code.
|
|
37
|
+
*/
|
|
25
38
|
export declare function sealClaudePermissionRules(rules: {
|
|
26
39
|
allow: string[];
|
|
27
40
|
ask: string[];
|
package/dist/control-maps.js
CHANGED
|
@@ -101,12 +101,16 @@ export function piToolId(name) {
|
|
|
101
101
|
return "find";
|
|
102
102
|
return trimmed;
|
|
103
103
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Map tool switches to native ids; spellings of one tool keep its explicit off.
|
|
106
|
+
*
|
|
107
|
+
* Products compose profiles in layers: a shared allowlist, then a product
|
|
108
|
+
* constraint that disables a native tool under its own spelling. Physim adds
|
|
109
|
+
* `webfetch: false` over an allowlist holding `web_fetch: true`. Off is the
|
|
110
|
+
* only value that honours every layer, so it wins regardless of entry order.
|
|
111
|
+
*/
|
|
112
|
+
export function mapSupportedBooleanEntries(entries, mapName, onUnsupported) {
|
|
113
|
+
return mapControlEntries(entries, mapName, onUnsupported, (left, right) => left && right);
|
|
110
114
|
}
|
|
111
115
|
export function permissionAllowsEverything(policy) {
|
|
112
116
|
return typeof policy === "string"
|
|
@@ -129,21 +133,43 @@ function mapControlEntries(entries, mapName, onUnsupported, merge) {
|
|
|
129
133
|
}
|
|
130
134
|
return [...mapped.entries()].sort(([left], [right]) => compareStrings(left, right));
|
|
131
135
|
}
|
|
136
|
+
const PERMISSION_RESTRICTION = {
|
|
137
|
+
allow: 0,
|
|
138
|
+
ask: 1,
|
|
139
|
+
deny: 2,
|
|
140
|
+
};
|
|
141
|
+
function stricterDecision(left, right) {
|
|
142
|
+
return PERMISSION_RESTRICTION[right] > PERMISSION_RESTRICTION[left]
|
|
143
|
+
? right
|
|
144
|
+
: left;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Merge policies whose names resolve to one native tool; the stricter
|
|
148
|
+
* decision wins, as an explicit off does for tool switches.
|
|
149
|
+
*
|
|
150
|
+
* Pattern rules merge only when both spellings list the same patterns in the
|
|
151
|
+
* same order. OpenCode applies the last matching pattern, so any other pair
|
|
152
|
+
* has no merge at least as strict as each spelling: `{"*": "deny"}` beside
|
|
153
|
+
* `{"https://x/*": "allow"}` would allow x. Those pairs stay contradictions
|
|
154
|
+
* that `executionControlConflict` reports at admission.
|
|
155
|
+
*/
|
|
132
156
|
function mergePermissionPolicies(left, right, label) {
|
|
157
|
+
if (typeof left === "string" && typeof right === "string") {
|
|
158
|
+
return stricterDecision(left, right);
|
|
159
|
+
}
|
|
133
160
|
if (typeof left === "string" || typeof right === "string") {
|
|
134
|
-
|
|
135
|
-
throw new Error(`${label} aliases disagree`);
|
|
136
|
-
return left;
|
|
161
|
+
throw new Error(`${label} aliases mix a whole-tool decision with pattern rules`);
|
|
137
162
|
}
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
merged[specifier] = decision;
|
|
163
|
+
const patterns = Object.keys(left);
|
|
164
|
+
const rightPatterns = Object.keys(right);
|
|
165
|
+
if (patterns.length !== rightPatterns.length ||
|
|
166
|
+
rightPatterns.some((pattern, index) => pattern !== patterns[index])) {
|
|
167
|
+
throw new Error(`${label} aliases set different pattern rules`);
|
|
145
168
|
}
|
|
146
|
-
return Object.fromEntries(Object.entries(
|
|
169
|
+
return Object.fromEntries(Object.entries(left).map(([pattern, decision]) => [
|
|
170
|
+
pattern,
|
|
171
|
+
stricterDecision(decision, right[pattern] ?? decision),
|
|
172
|
+
]));
|
|
147
173
|
}
|
|
148
174
|
export function claudePermissionRules(entries) {
|
|
149
175
|
const rules = {
|
|
@@ -165,22 +191,32 @@ export function claudePermissionRules(entries) {
|
|
|
165
191
|
rules.deny.sort(compareStrings);
|
|
166
192
|
return rules;
|
|
167
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Give each Claude rule one decision. A tool switch and a permission can name
|
|
196
|
+
* the same rule; the stricter decision wins, as deny already beats allow
|
|
197
|
+
* inside Claude Code.
|
|
198
|
+
*/
|
|
168
199
|
export function sealClaudePermissionRules(rules) {
|
|
169
200
|
const owner = new Map();
|
|
170
201
|
for (const decision of ["allow", "ask", "deny"]) {
|
|
171
202
|
for (const rule of rules[decision]) {
|
|
172
203
|
const previous = owner.get(rule);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
owner.set(rule, decision);
|
|
204
|
+
owner.set(rule, previous === undefined
|
|
205
|
+
? decision
|
|
206
|
+
: stricterDecision(previous, decision));
|
|
177
207
|
}
|
|
178
208
|
}
|
|
179
|
-
|
|
180
|
-
allow: [
|
|
181
|
-
ask: [
|
|
182
|
-
deny: [
|
|
209
|
+
const sealed = {
|
|
210
|
+
allow: [],
|
|
211
|
+
ask: [],
|
|
212
|
+
deny: [],
|
|
183
213
|
};
|
|
214
|
+
for (const [rule, decision] of owner)
|
|
215
|
+
sealed[decision].push(rule);
|
|
216
|
+
sealed.allow.sort(compareStrings);
|
|
217
|
+
sealed.ask.sort(compareStrings);
|
|
218
|
+
sealed.deny.sort(compareStrings);
|
|
219
|
+
return sealed;
|
|
184
220
|
}
|
|
185
221
|
import { resolveNativeWebTools } from "@tangle-network/agent-interface";
|
|
186
222
|
import { compareStrings } from "./workspace-files.js";
|
|
@@ -4,7 +4,7 @@ export function materializeExecutionControls(h, tools, permissions, modes, plan,
|
|
|
4
4
|
switch (h) {
|
|
5
5
|
case "claude-code": {
|
|
6
6
|
if (toolEntries.length > 0 || permissionEntries.length > 0) {
|
|
7
|
-
const nativeTools = mapSupportedBooleanEntries(toolEntries, claudeToolName, (name) => unsupported("tools", `claude-code: no verified native tool mapping for "${name}"`)
|
|
7
|
+
const nativeTools = mapSupportedBooleanEntries(toolEntries, claudeToolName, (name) => unsupported("tools", `claude-code: no verified native tool mapping for "${name}"`));
|
|
8
8
|
const nativePermissions = mapSupportedPermissionEntries(permissionEntries, claudeToolName, (name) => unsupported("permissions", `claude-code: no verified native permission mapping for "${name}"`), "claude-code permission");
|
|
9
9
|
const rules = claudePermissionRules(nativePermissions);
|
|
10
10
|
for (const [tool, enabled] of nativeTools) {
|
|
@@ -32,7 +32,7 @@ export function materializeExecutionControls(h, tools, permissions, modes, plan,
|
|
|
32
32
|
}
|
|
33
33
|
return false;
|
|
34
34
|
});
|
|
35
|
-
const nativeTools = mapSupportedBooleanEntries(enforceableTools, openCodeToolName, (name) => unsupported("tools", `opencode: no verified native tool mapping for "${name}"`)
|
|
35
|
+
const nativeTools = mapSupportedBooleanEntries(enforceableTools, openCodeToolName, (name) => unsupported("tools", `opencode: no verified native tool mapping for "${name}"`));
|
|
36
36
|
const nativePermissions = mapSupportedPermissionEntries(enforceablePermissions, openCodePermissionName, (name) => unsupported("permissions", `opencode: no verified native permission mapping for "${name}"`), "opencode permission");
|
|
37
37
|
if (enforceableTools.length > 0 || permissionEntries.length > 0) {
|
|
38
38
|
add(OPENCODE_CONFIG_FILE, serializeWorkspacePlanJson({
|
|
@@ -49,6 +49,7 @@ export function materializeExecutionControls(h, tools, permissions, modes, plan,
|
|
|
49
49
|
}
|
|
50
50
|
case "codex": {
|
|
51
51
|
const searchEntries = [];
|
|
52
|
+
const fetchEntries = [];
|
|
52
53
|
for (const [name, enabled] of toolEntries) {
|
|
53
54
|
const posture = resolveNativeWebTools({ [name]: enabled });
|
|
54
55
|
if (posture.search === enabled && posture.fetch === undefined) {
|
|
@@ -56,14 +57,20 @@ export function materializeExecutionControls(h, tools, permissions, modes, plan,
|
|
|
56
57
|
continue;
|
|
57
58
|
}
|
|
58
59
|
if (posture.fetch === enabled && posture.search === undefined) {
|
|
59
|
-
|
|
60
|
-
unsupported("tools", `codex: native web fetch is unavailable for "${name}"`);
|
|
61
|
-
}
|
|
60
|
+
fetchEntries.push([name, enabled]);
|
|
62
61
|
continue;
|
|
63
62
|
}
|
|
64
63
|
unsupported("tools", `codex: no verified primary-process tool mapping for "${name}"`);
|
|
65
64
|
}
|
|
66
|
-
|
|
65
|
+
// Codex has no native fetch: only a fetch that stays on after its
|
|
66
|
+
// spellings merge asks for something this harness cannot run.
|
|
67
|
+
const nativeFetch = mapSupportedBooleanEntries(fetchEntries, () => "web_fetch", () => undefined);
|
|
68
|
+
if (nativeFetch[0]?.[1]) {
|
|
69
|
+
for (const [name] of fetchEntries) {
|
|
70
|
+
unsupported("tools", `codex: native web fetch is unavailable for "${name}"`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const nativeSearch = mapSupportedBooleanEntries(searchEntries, () => "web_search", () => undefined);
|
|
67
74
|
if (nativeSearch[0]) {
|
|
68
75
|
plan.flags.push("-c", `tools.web_search=${nativeSearch[0][1]}`);
|
|
69
76
|
}
|
|
@@ -73,7 +80,7 @@ export function materializeExecutionControls(h, tools, permissions, modes, plan,
|
|
|
73
80
|
break;
|
|
74
81
|
}
|
|
75
82
|
case "pi": {
|
|
76
|
-
const nativeTools = mapSupportedBooleanEntries(toolEntries, piToolId, (name) => unsupported("tools", `pi: tool name "${name}" is empty after trimming`)
|
|
83
|
+
const nativeTools = mapSupportedBooleanEntries(toolEntries, piToolId, (name) => unsupported("tools", `pi: tool name "${name}" is empty after trimming`));
|
|
77
84
|
const nativePermissions = mapSupportedPermissionEntries(permissionEntries, piToolId, (name) => unsupported("permissions", `pi: permission name "${name}" is empty after trimming`), "pi permission");
|
|
78
85
|
const enabled = new Set(nativeTools
|
|
79
86
|
.filter(([, value]) => value)
|
|
@@ -92,15 +99,12 @@ export function materializeExecutionControls(h, tools, permissions, modes, plan,
|
|
|
92
99
|
unsupported("permissions", `pi: permission policy "ask" for "${name}" requires an interactive decision surface`);
|
|
93
100
|
continue;
|
|
94
101
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
// The stricter control holds, as across spellings of one tool: an
|
|
103
|
+
// allow keeps a disabled tool disabled, and a deny excludes an
|
|
104
|
+
// enabled one.
|
|
105
|
+
if (policy === "allow")
|
|
99
106
|
continue;
|
|
100
|
-
|
|
101
|
-
if (enabled.has(name)) {
|
|
102
|
-
throw new Error(`pi execution control "${name}" conflicts between enabled and deny`);
|
|
103
|
-
}
|
|
107
|
+
enabled.delete(name);
|
|
104
108
|
excluded.add(name);
|
|
105
109
|
}
|
|
106
110
|
if (enabled.size > 0 || excluded.size > 0) {
|
package/dist/harness.d.ts
CHANGED
|
@@ -79,7 +79,7 @@ export declare const MATERIALIZER_CONTEXT_PATHS: ReadonlySet<string>;
|
|
|
79
79
|
* `opencode.json` and every context file is a documented name a user repository
|
|
80
80
|
* can carry, so the name alone proves nothing and a profile resource may still
|
|
81
81
|
* mount one. An existing file there is replaced only when
|
|
82
|
-
* {@link
|
|
82
|
+
* {@link materializerReplacement} recognizes its bytes as this package's own
|
|
83
83
|
* earlier output.
|
|
84
84
|
*
|
|
85
85
|
* Deliberately absent: `.gemini/system.md`, the file `prompt.systemPrompt`
|
|
@@ -110,11 +110,14 @@ export declare function markMaterializerSubagent(relPath: string, content: strin
|
|
|
110
110
|
* (`.gemini/system.md`; `.codex/system-prompt.md` is a reserved name this
|
|
111
111
|
* package invented and needs no marker to be owned).
|
|
112
112
|
*
|
|
113
|
-
* It carries the digest of
|
|
114
|
-
* not need. `CLAUDE.md` is claude-code's own memory file and the
|
|
115
|
-
* to it mid-session, so a bare marker would still sit on line 1
|
|
116
|
-
* agent has since rewritten.
|
|
117
|
-
*
|
|
113
|
+
* It carries the digest of the generated body below it, which the subagent
|
|
114
|
+
* marker does not need. `CLAUDE.md` is claude-code's own memory file and the
|
|
115
|
+
* agent appends to it mid-session, so a bare marker would still sit on line 1
|
|
116
|
+
* of a file the agent has since rewritten. The digest tells which leading bytes
|
|
117
|
+
* are still this package's own: the next materialization replaces exactly
|
|
118
|
+
* those and keeps what the agent appended below them
|
|
119
|
+
* ({@link generatedContextAppendix}). The marker's wording is part of every
|
|
120
|
+
* marker already written into live workspaces, so it stays as it is.
|
|
118
121
|
*
|
|
119
122
|
* A reserved context path — opencode's invented instruction file — is returned
|
|
120
123
|
* unchanged: its NAME already proves the provenance, and nothing else may mount
|
|
@@ -134,21 +137,15 @@ export declare function markMaterializerContextFile(relPath: string, content: st
|
|
|
134
137
|
*/
|
|
135
138
|
export declare function isMaterializerOwnedPath(relPath: string): boolean;
|
|
136
139
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* An `opencode.json` this package wrote holds the opencode schema id, only the
|
|
146
|
-
* keys this package writes, and only instruction paths this package invented.
|
|
147
|
-
* A user config that holds nothing else is indistinguishable from that output
|
|
148
|
-
* and is replaced too; a config with any other key, or with an instruction path
|
|
149
|
-
* of the user's own, is refused.
|
|
140
|
+
* The bytes to publish at owned path `relPath` over the file already there, or
|
|
141
|
+
* `undefined` when that file is not this package's earlier output and must be
|
|
142
|
+
* refused (`undefined` content: no regular file there). For a context file the
|
|
143
|
+
* replacement is the planned file followed by whatever the agent appended
|
|
144
|
+
* below the generated body (claude-code appends its memory to `CLAUDE.md`). A
|
|
145
|
+
* body written before the marker existed is adopted once, only when it is
|
|
146
|
+
* byte-identical to the body this plan writes.
|
|
150
147
|
*/
|
|
151
|
-
export declare function
|
|
148
|
+
export declare function materializerReplacement(relPath: string, content: () => string | undefined, plannedContent: string): string | undefined;
|
|
152
149
|
/**
|
|
153
150
|
* cwd-relative native skill dir per harness, or null if the harness has no
|
|
154
151
|
* cwd skill primitive (skills live in a user/global dir the workspace plan
|
package/dist/harness.js
CHANGED
|
@@ -153,7 +153,7 @@ export const MATERIALIZER_CONTEXT_PATHS = new Set([...CONTEXT_FILE_PATHS].filter
|
|
|
153
153
|
* `opencode.json` and every context file is a documented name a user repository
|
|
154
154
|
* can carry, so the name alone proves nothing and a profile resource may still
|
|
155
155
|
* mount one. An existing file there is replaced only when
|
|
156
|
-
* {@link
|
|
156
|
+
* {@link materializerReplacement} recognizes its bytes as this package's own
|
|
157
157
|
* earlier output.
|
|
158
158
|
*
|
|
159
159
|
* Deliberately absent: `.gemini/system.md`, the file `prompt.systemPrompt`
|
|
@@ -214,11 +214,14 @@ function materializerContextMarker(body) {
|
|
|
214
214
|
* (`.gemini/system.md`; `.codex/system-prompt.md` is a reserved name this
|
|
215
215
|
* package invented and needs no marker to be owned).
|
|
216
216
|
*
|
|
217
|
-
* It carries the digest of
|
|
218
|
-
* not need. `CLAUDE.md` is claude-code's own memory file and the
|
|
219
|
-
* to it mid-session, so a bare marker would still sit on line 1
|
|
220
|
-
* agent has since rewritten.
|
|
221
|
-
*
|
|
217
|
+
* It carries the digest of the generated body below it, which the subagent
|
|
218
|
+
* marker does not need. `CLAUDE.md` is claude-code's own memory file and the
|
|
219
|
+
* agent appends to it mid-session, so a bare marker would still sit on line 1
|
|
220
|
+
* of a file the agent has since rewritten. The digest tells which leading bytes
|
|
221
|
+
* are still this package's own: the next materialization replaces exactly
|
|
222
|
+
* those and keeps what the agent appended below them
|
|
223
|
+
* ({@link generatedContextAppendix}). The marker's wording is part of every
|
|
224
|
+
* marker already written into live workspaces, so it stays as it is.
|
|
222
225
|
*
|
|
223
226
|
* A reserved context path — opencode's invented instruction file — is returned
|
|
224
227
|
* unchanged: its NAME already proves the provenance, and nothing else may mount
|
|
@@ -232,16 +235,55 @@ export function markMaterializerContextFile(relPath, content) {
|
|
|
232
235
|
return content;
|
|
233
236
|
return `${materializerContextMarker(content)}\n${content}`;
|
|
234
237
|
}
|
|
238
|
+
const MATERIALIZER_CONTEXT_MARKER_HEAD = `<!-- ${MATERIALIZER_MARKER} sha256:`;
|
|
239
|
+
const MATERIALIZER_CONTEXT_MARKER_TAIL = `; ${MATERIALIZER_CONTEXT_MARKER_NOTE} -->`;
|
|
240
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/u;
|
|
235
241
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
242
|
+
* Text an agent appended below this package's generated context body: `""`
|
|
243
|
+
* for exact output, `undefined` when no marker is on line 1 or no leading part
|
|
244
|
+
* of the body matches its digest (an edit inside the generated part). Each
|
|
245
|
+
* line boundary is a candidate end; one incremental hash walks the body once.
|
|
238
246
|
*/
|
|
239
|
-
function
|
|
247
|
+
function generatedContextAppendix(existing) {
|
|
240
248
|
const lineEnd = existing.indexOf("\n");
|
|
241
249
|
if (lineEnd === -1)
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
|
|
250
|
+
return undefined;
|
|
251
|
+
const marker = existing.slice(0, lineEnd);
|
|
252
|
+
if (!marker.startsWith(MATERIALIZER_CONTEXT_MARKER_HEAD) ||
|
|
253
|
+
!marker.endsWith(MATERIALIZER_CONTEXT_MARKER_TAIL)) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
const digest = marker.slice(MATERIALIZER_CONTEXT_MARKER_HEAD.length, marker.length - MATERIALIZER_CONTEXT_MARKER_TAIL.length);
|
|
257
|
+
if (!SHA256_HEX.test(digest))
|
|
258
|
+
return undefined;
|
|
259
|
+
const body = existing.slice(lineEnd + 1);
|
|
260
|
+
// No cut at 0: a generated body is never empty, so it never adopts a whole file.
|
|
261
|
+
const boundaries = [];
|
|
262
|
+
for (let newline = body.indexOf("\n"); newline !== -1; newline = body.indexOf("\n", newline + 1)) {
|
|
263
|
+
boundaries.push(newline, newline + 1);
|
|
264
|
+
}
|
|
265
|
+
if (boundaries.length === 0 ||
|
|
266
|
+
boundaries[boundaries.length - 1] !== body.length) {
|
|
267
|
+
boundaries.push(body.length);
|
|
268
|
+
}
|
|
269
|
+
const hash = createHash("sha256");
|
|
270
|
+
let hashed = 0;
|
|
271
|
+
for (const end of boundaries) {
|
|
272
|
+
hash.update(body.slice(hashed, end), "utf8");
|
|
273
|
+
hashed = end;
|
|
274
|
+
if (hash.copy().digest("hex") === digest)
|
|
275
|
+
return body.slice(end);
|
|
276
|
+
}
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
/** The planned context file, then the agent's appended text on its own line. */
|
|
280
|
+
function withContextAppendix(plannedContent, appendix) {
|
|
281
|
+
if (appendix === "" ||
|
|
282
|
+
plannedContent.endsWith("\n") ||
|
|
283
|
+
appendix.startsWith("\n")) {
|
|
284
|
+
return `${plannedContent}${appendix}`;
|
|
285
|
+
}
|
|
286
|
+
return `${plannedContent}\n${appendix}`;
|
|
245
287
|
}
|
|
246
288
|
/**
|
|
247
289
|
* Whether a generated file occupies a path that the profile materializer owns.
|
|
@@ -267,13 +309,43 @@ const OPENCODE_CONFIG_KEYS = new Set([
|
|
|
267
309
|
"permission",
|
|
268
310
|
]);
|
|
269
311
|
/**
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
312
|
+
* The bytes to publish at owned path `relPath` over the file already there, or
|
|
313
|
+
* `undefined` when that file is not this package's earlier output and must be
|
|
314
|
+
* refused (`undefined` content: no regular file there). For a context file the
|
|
315
|
+
* replacement is the planned file followed by whatever the agent appended
|
|
316
|
+
* below the generated body (claude-code appends its memory to `CLAUDE.md`). A
|
|
317
|
+
* body written before the marker existed is adopted once, only when it is
|
|
318
|
+
* byte-identical to the body this plan writes.
|
|
319
|
+
*/
|
|
320
|
+
export function materializerReplacement(relPath, content, plannedContent) {
|
|
321
|
+
if (!MATERIALIZER_CONTEXT_PATHS.has(relPath)) {
|
|
322
|
+
return isMaterializerOutput(relPath, content, plannedContent)
|
|
323
|
+
? plannedContent
|
|
324
|
+
: undefined;
|
|
325
|
+
}
|
|
326
|
+
const existing = content();
|
|
327
|
+
if (existing === undefined)
|
|
328
|
+
return undefined;
|
|
329
|
+
const appendix = generatedContextAppendix(existing);
|
|
330
|
+
if (appendix === "")
|
|
331
|
+
return plannedContent;
|
|
332
|
+
// Appended text rides only on a marked plan, which the next version recognizes.
|
|
333
|
+
if (generatedContextAppendix(plannedContent) !== "")
|
|
334
|
+
return undefined;
|
|
335
|
+
if (appendix !== undefined) {
|
|
336
|
+
return withContextAppendix(plannedContent, appendix);
|
|
337
|
+
}
|
|
338
|
+
return existing === plannedContent.slice(plannedContent.indexOf("\n") + 1)
|
|
339
|
+
? plannedContent
|
|
340
|
+
: undefined;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Whether the file already at owned path `relPath`, other than a context file,
|
|
344
|
+
* is this package's own earlier output. A reserved name proves that by itself.
|
|
345
|
+
* Native agent files require the generated marker. An exact unmarked copy of
|
|
346
|
+
* the planned native agent file is adopted once for upgrades from materializer
|
|
347
|
+
* versions without that marker. `undefined` content means no regular file is
|
|
348
|
+
* there.
|
|
277
349
|
*
|
|
278
350
|
* An `opencode.json` this package wrote holds the opencode schema id, only the
|
|
279
351
|
* keys this package writes, and only instruction paths this package invented.
|
|
@@ -281,7 +353,7 @@ const OPENCODE_CONFIG_KEYS = new Set([
|
|
|
281
353
|
* and is replaced too; a config with any other key, or with an instruction path
|
|
282
354
|
* of the user's own, is refused.
|
|
283
355
|
*/
|
|
284
|
-
|
|
356
|
+
function isMaterializerOutput(relPath, content, plannedContent) {
|
|
285
357
|
if (MATERIALIZER_RESERVED_PATHS.has(relPath))
|
|
286
358
|
return true;
|
|
287
359
|
if (MATERIALIZER_SUBAGENT_PATH.test(relPath)) {
|
|
@@ -291,8 +363,6 @@ export function isMaterializerOutput(relPath, content, plannedContent) {
|
|
|
291
363
|
const markerPrefix = materializerSubagentMarkerPrefix(relPath);
|
|
292
364
|
if (existing.startsWith(markerPrefix))
|
|
293
365
|
return true;
|
|
294
|
-
if (plannedContent === undefined)
|
|
295
|
-
return false;
|
|
296
366
|
if (!plannedContent.startsWith(markerPrefix))
|
|
297
367
|
return false;
|
|
298
368
|
const legacyContent = relPath.endsWith(".toml")
|
|
@@ -300,23 +370,6 @@ export function isMaterializerOutput(relPath, content, plannedContent) {
|
|
|
300
370
|
: `---\n${plannedContent.slice(markerPrefix.length)}`;
|
|
301
371
|
return existing === legacyContent;
|
|
302
372
|
}
|
|
303
|
-
if (MATERIALIZER_CONTEXT_PATHS.has(relPath)) {
|
|
304
|
-
const existing = content();
|
|
305
|
-
if (existing === undefined)
|
|
306
|
-
return false;
|
|
307
|
-
if (isMarkedContextOutput(existing))
|
|
308
|
-
return true;
|
|
309
|
-
// A workspace materialized before this marker existed carries the body
|
|
310
|
-
// alone. Adopt it once, and only when it is byte-identical to the body this
|
|
311
|
-
// plan writes, so upgrading the package does not turn every resumed session
|
|
312
|
-
// into a refusal. A body that differs is either a user's file or an earlier
|
|
313
|
-
// profile's, and neither can be told apart without the marker.
|
|
314
|
-
if (plannedContent === undefined)
|
|
315
|
-
return false;
|
|
316
|
-
if (!isMarkedContextOutput(plannedContent))
|
|
317
|
-
return false;
|
|
318
|
-
return existing === plannedContent.slice(plannedContent.indexOf("\n") + 1);
|
|
319
|
-
}
|
|
320
373
|
if (relPath !== OPENCODE_CONFIG_FILE)
|
|
321
374
|
return false;
|
|
322
375
|
const bytes = content();
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,6 @@ export type { PrimeProfileErrorKind, PrimeThinkingLevel, } from "./prime-fork.js
|
|
|
21
21
|
export { matchPrimeKernelDeadMarker, PRIME_AGENT_DIR_ENV_VAR, PRIME_AGENT_DIR_ENV_VARS, PRIME_FORK_INSTALL, PRIME_FORK_PIN, PRIME_HARNESS_STATE_ENV_DENYLIST, PRIME_KERNEL_DEAD_MARKERS, PRIME_KERNEL_ENV_PASSTHROUGH, PRIME_MCP_UNSUPPORTED_REASON, PRIME_THINKING_LEVELS, PrimeHarnessStateError, PrimeProfileError, parsePrimeModelsJson, primeAgentDirEnv, primeApiKeyEnvNames, primeThinkingLevel, stripPrimeJsonComments, } from "./prime-fork.js";
|
|
22
22
|
export type { PrimeProfileControls, PrimeProfileSkill, PrimeProfileSubagent, } from "./prime-profile.js";
|
|
23
23
|
export { assertPrimeModelAgreement, hasPrimeProfileMaterial, primeProfileNeedsAgentDir, primeSubagentEntryId, readPrimeProfileControls, } from "./prime-profile.js";
|
|
24
|
-
export { materializeProfile, UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER, } from "./profile-materializer.js";
|
|
24
|
+
export { executionControlConflict, materializeProfile, UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER, } from "./profile-materializer.js";
|
|
25
25
|
export type { ResolveAgentProfileResourcesOptions } from "./resource-resolution.js";
|
|
26
26
|
export { normalizeSkillMd, resolveAgentProfileResources, } from "./resource-resolution.js";
|
package/dist/index.js
CHANGED
|
@@ -14,5 +14,5 @@ export { BINDABLE_LAUNCH_CONTROLS, EXECUTOR_SYSTEM_PROMPT_BINDINGS, isExecutorSy
|
|
|
14
14
|
export { materializePrimeProfileControls, prunePrimeProfileMaterial, seedPrimeProfileSubagents, writePrimeAgentFile, } from "./prime-agent-dir.js";
|
|
15
15
|
export { matchPrimeKernelDeadMarker, PRIME_AGENT_DIR_ENV_VAR, PRIME_AGENT_DIR_ENV_VARS, PRIME_FORK_INSTALL, PRIME_FORK_PIN, PRIME_HARNESS_STATE_ENV_DENYLIST, PRIME_KERNEL_DEAD_MARKERS, PRIME_KERNEL_ENV_PASSTHROUGH, PRIME_MCP_UNSUPPORTED_REASON, PRIME_THINKING_LEVELS, PrimeHarnessStateError, PrimeProfileError, parsePrimeModelsJson, primeAgentDirEnv, primeApiKeyEnvNames, primeThinkingLevel, stripPrimeJsonComments, } from "./prime-fork.js";
|
|
16
16
|
export { assertPrimeModelAgreement, hasPrimeProfileMaterial, primeProfileNeedsAgentDir, primeSubagentEntryId, readPrimeProfileControls, } from "./prime-profile.js";
|
|
17
|
-
export { materializeProfile, UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER, } from "./profile-materializer.js";
|
|
17
|
+
export { executionControlConflict, materializeProfile, UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER, } from "./profile-materializer.js";
|
|
18
18
|
export { normalizeSkillMd, resolveAgentProfileResources, } from "./resource-resolution.js";
|
|
@@ -2,6 +2,16 @@ import type { AgentProfile, AgentProfileSecretProvider, AgentRuntimeAttachments
|
|
|
2
2
|
import { type HarnessId } from "./harness.js";
|
|
3
3
|
import { type BindableLaunchControl, type PlanFileRoot, type SkippableDimension, type WorkspacePlan } from "./plan-types.js";
|
|
4
4
|
export declare const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER: AgentProfileSecretProvider;
|
|
5
|
+
/**
|
|
6
|
+
* Why a profile's tool and permission controls cannot reach one native policy
|
|
7
|
+
* on this harness, or undefined when they can.
|
|
8
|
+
*
|
|
9
|
+
* Admission boundaries (profile save, Sandbox create) call this so a
|
|
10
|
+
* contradictory control set is refused when it is submitted, not when a
|
|
11
|
+
* user's turn materializes it. Controls a harness cannot enforce are not
|
|
12
|
+
* contradictions: `resources.failOnError` decides those per turn.
|
|
13
|
+
*/
|
|
14
|
+
export declare function executionControlConflict(profile: Pick<AgentProfile, "tools" | "permissions">, harness: HarnessId): string | undefined;
|
|
5
15
|
/**
|
|
6
16
|
* Build a pure workspace plan for one canonical harness.
|
|
7
17
|
* The caller applies files, environment, and flags at its process boundary.
|
|
@@ -14,6 +14,25 @@ export const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER = Object.freeze({
|
|
|
14
14
|
return undefined;
|
|
15
15
|
},
|
|
16
16
|
});
|
|
17
|
+
/**
|
|
18
|
+
* Why a profile's tool and permission controls cannot reach one native policy
|
|
19
|
+
* on this harness, or undefined when they can.
|
|
20
|
+
*
|
|
21
|
+
* Admission boundaries (profile save, Sandbox create) call this so a
|
|
22
|
+
* contradictory control set is refused when it is submitted, not when a
|
|
23
|
+
* user's turn materializes it. Controls a harness cannot enforce are not
|
|
24
|
+
* contradictions: `resources.failOnError` decides those per turn.
|
|
25
|
+
*/
|
|
26
|
+
export function executionControlConflict(profile, harness) {
|
|
27
|
+
const builder = createProfilePlanBuilder(harness);
|
|
28
|
+
try {
|
|
29
|
+
materializeExecutionControls(harness, profile.tools, profile.permissions, undefined, builder.plan, builder.add, builder.unsupported);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return error instanceof Error ? error.message : String(error);
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
17
36
|
/**
|
|
18
37
|
* Build a pure workspace plan for one canonical harness.
|
|
19
38
|
* The caller applies files, environment, and flags at its process boundary.
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
declare function workspaceFileExists(pathValue: string): boolean;
|
|
2
|
+
/**
|
|
3
|
+
* A regular file's text, or `undefined` when no regular file is there or its
|
|
4
|
+
* bytes are not UTF-8. This package writes only UTF-8, so such bytes are not
|
|
5
|
+
* its output, and decoding them lossily would rewrite them on replacement.
|
|
6
|
+
*/
|
|
2
7
|
declare function regularFileContent(pathValue: string): string | undefined;
|
|
3
8
|
declare function workspaceFileMatches(file: NormalizedPlanFile & {
|
|
4
9
|
absPath: string;
|
|
@@ -33,6 +38,8 @@ interface PreparedWorkspaceFile {
|
|
|
33
38
|
temporaryPath: string;
|
|
34
39
|
identity: WorkspaceFileIdentity;
|
|
35
40
|
published: boolean;
|
|
41
|
+
/** A link to the replaced file while it holds an agent's text, for rollback. */
|
|
42
|
+
superseded?: string;
|
|
36
43
|
}
|
|
37
44
|
declare function prepareWorkspaceFile(workspaceRoot: string, file: NormalizedPlanFile & {
|
|
38
45
|
absPath: string;
|
|
@@ -9,10 +9,24 @@ function workspaceFileExists(pathValue) {
|
|
|
9
9
|
throw error;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
const STRICT_UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
13
|
+
/**
|
|
14
|
+
* A regular file's text, or `undefined` when no regular file is there or its
|
|
15
|
+
* bytes are not UTF-8. This package writes only UTF-8, so such bytes are not
|
|
16
|
+
* its output, and decoding them lossily would rewrite them on replacement.
|
|
17
|
+
*/
|
|
12
18
|
function regularFileContent(pathValue) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
if (!lstatSync(pathValue).isFile())
|
|
20
|
+
return undefined;
|
|
21
|
+
const bytes = readFileSync(pathValue);
|
|
22
|
+
try {
|
|
23
|
+
return STRICT_UTF8.decode(bytes);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error instanceof TypeError)
|
|
27
|
+
return undefined;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
16
30
|
}
|
|
17
31
|
function workspaceFileMatches(file) {
|
|
18
32
|
const stats = lstatSync(file.absPath);
|
package/dist/workspace-apply.js
CHANGED
|
@@ -70,35 +70,53 @@ export function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDi
|
|
|
70
70
|
}
|
|
71
71
|
// An owned path holds this package's own earlier output, so re-applying
|
|
72
72
|
// into the same workspace replaces it rather than failing the turn — once
|
|
73
|
-
// the bytes already there prove that provenance.
|
|
74
|
-
//
|
|
75
|
-
// the
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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);
|
|
87
|
+
};
|
|
79
88
|
// Validate the complete plan before the first write so a late traversal or
|
|
80
89
|
// existing symlink cannot leave a partially materialized workspace.
|
|
90
|
+
const writes = [];
|
|
81
91
|
for (const file of filesToApply) {
|
|
82
92
|
assertNoSymlinkPathComponents(file.rootDir, file.absPath);
|
|
83
|
-
if (workspaceFileExists(file.absPath)
|
|
84
|
-
|
|
85
|
-
|
|
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)) {
|
|
86
103
|
throw new Error(workspaceFileConflictMessage(file, existingFiles, publicPlanDigest));
|
|
87
104
|
}
|
|
105
|
+
writes.push(file);
|
|
88
106
|
}
|
|
89
107
|
// Parent directories remain on failure because they may contain concurrent
|
|
90
108
|
// workspace state that this call does not own.
|
|
91
|
-
for (const file of
|
|
109
|
+
for (const file of writes) {
|
|
92
110
|
mkdirSync(dirname(file.absPath), { recursive: true });
|
|
93
111
|
}
|
|
94
|
-
for (const file of
|
|
112
|
+
for (const file of writes) {
|
|
95
113
|
assertNoSymlinkPathComponents(file.rootDir, file.absPath);
|
|
96
114
|
}
|
|
97
115
|
const prepared = [];
|
|
98
116
|
// Prepared temp links stay alive through publication and rollback. Their
|
|
99
117
|
// inodes distinguish files created here from concurrent replacements.
|
|
100
118
|
try {
|
|
101
|
-
for (const file of
|
|
119
|
+
for (const file of writes) {
|
|
102
120
|
if (existingFiles === "allow-identical" &&
|
|
103
121
|
workspaceFileExists(file.absPath) &&
|
|
104
122
|
workspaceFileMatches(file) &&
|
|
@@ -124,7 +142,12 @@ export function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDi
|
|
|
124
142
|
workspaceFileHasOwnership(entry.file.absPath, ownership))) {
|
|
125
143
|
continue;
|
|
126
144
|
}
|
|
127
|
-
if (isFsError(error, "EEXIST") &&
|
|
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.
|
|
128
151
|
// rename() replaces in one step, so no window exists in which the
|
|
129
152
|
// path is missing and a concurrent spawn reads nothing. The staged
|
|
130
153
|
// inode moves onto the path, which is what rollback identifies the
|
|
@@ -132,8 +155,16 @@ export function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDi
|
|
|
132
155
|
// published — leaving the path ABSENT rather than restored to the
|
|
133
156
|
// superseded bytes. That is the fail-closed direction: the harness
|
|
134
157
|
// whose flag names a missing file refuses to start, whereas silently
|
|
135
|
-
// restored previous-turn bytes would run the wrong prompt.
|
|
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.
|
|
136
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
|
+
}
|
|
137
168
|
renameSync(entry.temporaryPath, entry.file.absPath);
|
|
138
169
|
entry.published = true;
|
|
139
170
|
continue;
|
|
@@ -151,12 +182,25 @@ export function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDi
|
|
|
151
182
|
try {
|
|
152
183
|
if (entry.published &&
|
|
153
184
|
workspacePathHasIdentity(entry.file.absPath, entry.identity)) {
|
|
154
|
-
|
|
185
|
+
if (entry.superseded) {
|
|
186
|
+
renameSync(entry.superseded, entry.file.absPath);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
unlinkSync(entry.file.absPath);
|
|
190
|
+
}
|
|
155
191
|
}
|
|
156
192
|
}
|
|
157
193
|
catch (cleanupError) {
|
|
158
194
|
cleanupErrors.push(cleanupError);
|
|
159
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
|
+
}
|
|
160
204
|
try {
|
|
161
205
|
if (workspacePathHasIdentity(entry.temporaryPath, entry.identity)) {
|
|
162
206
|
unlinkSync(entry.temporaryPath);
|
|
@@ -175,13 +219,17 @@ export function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDi
|
|
|
175
219
|
const cleanupErrors = [];
|
|
176
220
|
const temporaryPaths = [];
|
|
177
221
|
for (const entry of prepared) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
+
}
|
|
185
233
|
}
|
|
186
234
|
}
|
|
187
235
|
}
|
|
@@ -216,9 +264,10 @@ function safeWorkspacePlanReceipt(plan, writtenFiles, workspacePlanDigest = hash
|
|
|
216
264
|
writtenFiles: writtenFiles.map((file) => ({ ...file })),
|
|
217
265
|
};
|
|
218
266
|
}
|
|
267
|
+
import { randomUUID } from "node:crypto";
|
|
219
268
|
import { linkSync, mkdirSync, renameSync, unlinkSync } from "node:fs";
|
|
220
|
-
import { dirname, isAbsolute, resolve } from "node:path";
|
|
221
|
-
import {
|
|
269
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
270
|
+
import { materializerReplacement } from "./harness.js";
|
|
222
271
|
import { hashWorkspacePlan } from "./plan-digest.js";
|
|
223
272
|
import { normalizePlanFiles, normalizeWorkspacePlanArgument, } from "./plan-normalize.js";
|
|
224
273
|
import { promptIntentFields, samePromptIntentValue, systemPromptBindingField, } from "./plan-prompt.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-profile-materialize",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Materialize an AgentProfile into the native per-harness workspace files (the one shared materializer for cli-bridge, agent-dev-container, and VB).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
"dist"
|
|
26
26
|
],
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@tangle-network/agent-interface": "
|
|
29
|
-
"@tangle-network/agent-interface-current": "npm:@tangle-network/agent-interface@
|
|
28
|
+
"@tangle-network/agent-interface": "3.0.0",
|
|
29
|
+
"@tangle-network/agent-interface-current": "npm:@tangle-network/agent-interface@3.0.0",
|
|
30
30
|
"@types/node": "25.6.0",
|
|
31
31
|
"typescript": "^6.0.3",
|
|
32
32
|
"vitest": "^4.1.11"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@tangle-network/agent-interface": "^
|
|
35
|
+
"@tangle-network/agent-interface": "^3.0.0"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsc -p tsconfig.json",
|