@tangle-network/agent-profile-materialize 1.1.7 → 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 -106
  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 -31
  27. package/dist/managed-workspace.js +0 -271
  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 -249
  80. package/dist/secret-session-view.d.ts +0 -25
  81. package/dist/secret-session-view.js +0 -166
  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,57 +0,0 @@
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
- */
7
- declare function regularFileContent(pathValue: string): string | undefined;
8
- declare function workspaceFileMatches(file: NormalizedPlanFile & {
9
- absPath: string;
10
- }): boolean;
11
- declare function workspaceFileHasOwnership(pathValue: string, ownership: {
12
- uid: number;
13
- gid: number;
14
- }): boolean;
15
- declare function validateWorkspaceOwnership(ownership: ApplyWorkspacePlanOptions["ownership"]): {
16
- uid: number;
17
- gid: number;
18
- } | undefined;
19
- /**
20
- * The refusal names what differs — planned versus existing content digest,
21
- * mode, or file kind — plus the plan digest, so the operator can tell a
22
- * mid-session profile swap from an agent-edited workspace file without
23
- * reproducing the workspace. Reads here are best-effort diagnosis inside an
24
- * already-failing path; an unreadable existing file is reported, not thrown
25
- * over.
26
- */
27
- declare function workspaceFileConflictMessage(file: NormalizedPlanFile & {
28
- absPath: string;
29
- }, existingFiles: "allow-identical" | "reject", planDigest: string): string;
30
- interface WorkspaceFileIdentity {
31
- device: number;
32
- inode: number;
33
- }
34
- interface PreparedWorkspaceFile {
35
- file: NormalizedPlanFile & {
36
- absPath: string;
37
- };
38
- temporaryPath: string;
39
- identity: WorkspaceFileIdentity;
40
- published: boolean;
41
- /** A link to the replaced file while it holds an agent's text, for rollback. */
42
- superseded?: string;
43
- }
44
- declare function prepareWorkspaceFile(workspaceRoot: string, file: NormalizedPlanFile & {
45
- absPath: string;
46
- }, ownership: {
47
- uid: number;
48
- gid: number;
49
- } | undefined): PreparedWorkspaceFile;
50
- declare function workspacePathHasIdentity(pathValue: string, identity: WorkspaceFileIdentity): boolean;
51
- declare function resolveWorkspaceFilePath(workspaceRoot: string, relativePath: string): string;
52
- declare function assertNoSymlinkPathComponents(workspaceRoot: string, targetPath: string): void;
53
- declare function isFsError(error: unknown, code: string): error is NodeJS.ErrnoException;
54
- export type { PreparedWorkspaceFile };
55
- export { assertNoSymlinkPathComponents, isFsError, prepareWorkspaceFile, regularFileContent, resolveWorkspaceFilePath, validateWorkspaceOwnership, workspaceFileConflictMessage, workspaceFileExists, workspaceFileHasOwnership, workspaceFileMatches, workspacePathHasIdentity, };
56
- import type { NormalizedPlanFile } from "./plan-normalize.js";
57
- import type { ApplyWorkspacePlanOptions } from "./plan-types.js";
@@ -1,193 +0,0 @@
1
- function workspaceFileExists(pathValue) {
2
- try {
3
- lstatSync(pathValue);
4
- return true;
5
- }
6
- catch (error) {
7
- if (isFsError(error, "ENOENT"))
8
- return false;
9
- throw error;
10
- }
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
- */
18
- function regularFileContent(pathValue) {
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
- }
30
- }
31
- function workspaceFileMatches(file) {
32
- const stats = lstatSync(file.absPath);
33
- return (stats.isFile() &&
34
- (stats.mode & 0o777) === file.mode &&
35
- readFileSync(file.absPath, "utf8") === file.content);
36
- }
37
- function workspaceFileHasOwnership(pathValue, ownership) {
38
- const stats = lstatSync(pathValue);
39
- return (stats.isFile() && stats.uid === ownership.uid && stats.gid === ownership.gid);
40
- }
41
- function validateWorkspaceOwnership(ownership) {
42
- if (ownership === undefined)
43
- return undefined;
44
- if (!Number.isInteger(ownership.uid) ||
45
- ownership.uid < 0 ||
46
- !Number.isInteger(ownership.gid) ||
47
- ownership.gid < 0) {
48
- throw new Error("Workspace file ownership must use non-negative integer uid/gid values");
49
- }
50
- return ownership;
51
- }
52
- function sha256Hex(content) {
53
- return createHash("sha256").update(content).digest("hex");
54
- }
55
- /**
56
- * The refusal names what differs — planned versus existing content digest,
57
- * mode, or file kind — plus the plan digest, so the operator can tell a
58
- * mid-session profile swap from an agent-edited workspace file without
59
- * reproducing the workspace. Reads here are best-effort diagnosis inside an
60
- * already-failing path; an unreadable existing file is reported, not thrown
61
- * over.
62
- */
63
- function workspaceFileConflictMessage(file, existingFiles, planDigest) {
64
- const rootLabel = file.root === "workspace" ? "workspace" : "agent-directory";
65
- const prefix = `Refusing to replace existing ${rootLabel} file: ${file.relPath}`;
66
- if (existingFiles === "reject") {
67
- return `${prefix} — the reject policy forbids any pre-existing plan path (plan ${planDigest})`;
68
- }
69
- const planned = `planned sha256:${sha256Hex(file.content)} mode ${file.mode.toString(8)}`;
70
- let existing;
71
- try {
72
- const stats = lstatSync(file.absPath);
73
- if (!stats.isFile()) {
74
- existing = stats.isSymbolicLink()
75
- ? "existing path is a symlink, not a regular file"
76
- : "existing path is not a regular file";
77
- }
78
- else {
79
- const mode = (stats.mode & 0o777).toString(8);
80
- existing = `existing sha256:${sha256Hex(readFileSync(file.absPath))} mode ${mode}`;
81
- }
82
- }
83
- catch (error) {
84
- const message = error instanceof Error ? error.message : String(error);
85
- existing = `existing file unreadable: ${message}`;
86
- }
87
- return `${prefix} — ${planned} != ${existing} (plan ${planDigest})`;
88
- }
89
- function prepareWorkspaceFile(workspaceRoot, file, ownership) {
90
- assertNoSymlinkPathComponents(workspaceRoot, file.absPath);
91
- const temporaryPath = join(dirname(file.absPath), `.agent-profile-${randomUUID()}.tmp`);
92
- let descriptor;
93
- try {
94
- descriptor = openSync(temporaryPath, SAFE_WRITE_FLAGS, 0o600);
95
- writeFileSync(descriptor, file.content, "utf8");
96
- if (ownership)
97
- fchownSync(descriptor, ownership.uid, ownership.gid);
98
- fchmodSync(descriptor, file.mode);
99
- const stats = fstatSync(descriptor);
100
- const descriptorToClose = descriptor;
101
- descriptor = undefined;
102
- closeSync(descriptorToClose);
103
- return {
104
- file,
105
- temporaryPath,
106
- identity: { device: stats.dev, inode: stats.ino },
107
- published: false,
108
- };
109
- }
110
- catch (error) {
111
- const cleanupErrors = [];
112
- if (descriptor !== undefined) {
113
- try {
114
- closeSync(descriptor);
115
- }
116
- catch (cleanupError) {
117
- cleanupErrors.push(cleanupError);
118
- }
119
- }
120
- try {
121
- unlinkSync(temporaryPath);
122
- }
123
- catch (cleanupError) {
124
- if (!isFsError(cleanupError, "ENOENT"))
125
- cleanupErrors.push(cleanupError);
126
- }
127
- if (cleanupErrors.length > 0) {
128
- throw new AggregateError([error, ...cleanupErrors], `Failed to prepare workspace file: ${file.relPath}`);
129
- }
130
- throw error;
131
- }
132
- }
133
- function workspacePathHasIdentity(pathValue, identity) {
134
- try {
135
- const stats = lstatSync(pathValue);
136
- return stats.dev === identity.device && stats.ino === identity.inode;
137
- }
138
- catch (error) {
139
- if (isFsError(error, "ENOENT"))
140
- return false;
141
- throw error;
142
- }
143
- }
144
- function resolveWorkspaceFilePath(workspaceRoot, relativePath) {
145
- const targetPath = resolve(workspaceRoot, ...relativePath.split("/"));
146
- const targetRelativePath = relative(workspaceRoot, targetPath);
147
- if (!targetRelativePath ||
148
- targetRelativePath === ".." ||
149
- targetRelativePath.startsWith(`..${sep}`) ||
150
- isAbsolute(targetRelativePath)) {
151
- throw new Error(`Refusing to materialize profile file outside workspace: ${relativePath}`);
152
- }
153
- return targetPath;
154
- }
155
- function assertNoSymlinkPathComponents(workspaceRoot, targetPath) {
156
- const targetRelativePath = relative(workspaceRoot, targetPath);
157
- let currentPath = workspaceRoot;
158
- const components = ["", ...targetRelativePath.split(sep)];
159
- for (const [index, component] of components.entries()) {
160
- if (component)
161
- currentPath = join(currentPath, component);
162
- let stats;
163
- try {
164
- stats = lstatSync(currentPath);
165
- }
166
- catch (error) {
167
- if (isFsError(error, "ENOENT"))
168
- return;
169
- if (isFsError(error, "ELOOP")) {
170
- throw new Error(`Refusing to materialize profile file through symlink path: ${targetRelativePath}`);
171
- }
172
- throw error;
173
- }
174
- if (stats.isSymbolicLink()) {
175
- throw new Error(`Refusing to materialize profile file through symlink path: ${targetRelativePath}`);
176
- }
177
- const isTarget = index === components.length - 1;
178
- if ((!isTarget && !stats.isDirectory()) || (isTarget && !stats.isFile())) {
179
- throw new Error(`Refusing to materialize profile file through non-file path: ${targetRelativePath}`);
180
- }
181
- }
182
- }
183
- function isFsError(error, code) {
184
- return (typeof error === "object" &&
185
- error !== null &&
186
- "code" in error &&
187
- error.code === code);
188
- }
189
- export { assertNoSymlinkPathComponents, isFsError, prepareWorkspaceFile, regularFileContent, resolveWorkspaceFilePath, validateWorkspaceOwnership, workspaceFileConflictMessage, workspaceFileExists, workspaceFileHasOwnership, workspaceFileMatches, workspacePathHasIdentity, };
190
- import { createHash, randomUUID } from "node:crypto";
191
- import { closeSync, fchmodSync, fchownSync, fstatSync, lstatSync, openSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
192
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
193
- import { SAFE_WRITE_FLAGS } from "./workspace-files.js";
@@ -1,22 +0,0 @@
1
- export type ExistingFilePolicy = "allow-identical" | "reject";
2
- export type ApplyFile = NormalizedPlanFile & {
3
- rootDir: string;
4
- absPath: string;
5
- };
6
- /** What one publication needs once the files to write are chosen. */
7
- export interface PublishContext {
8
- rootFor: (root: NormalizedPlanFile["root"]) => string;
9
- existingFiles: ExistingFilePolicy;
10
- ownership: ApplyWorkspacePlanOptions["ownership"];
11
- publicPlanDigest: string;
12
- plannedContent: Map<string, string>;
13
- }
14
- export declare const fileIdentity: (file: NormalizedPlanFile) => string;
15
- export declare function holdsCurrentOwnership(absPath: string, ownership: ApplyWorkspacePlanOptions["ownership"]): boolean;
16
- export declare function planWrites(filesToApply: ApplyFile[], context: PublishContext): ApplyFile[];
17
- export declare function publishWrites(writes: ApplyFile[], prepared: PreparedWorkspaceFile[], context: PublishContext): void;
18
- export declare function rollbackPublished(prepared: PreparedWorkspaceFile[], error: unknown): never;
19
- export declare function removeTemporaries(prepared: PreparedWorkspaceFile[], receipt: WorkspacePlanReceipt): void;
20
- import type { NormalizedPlanFile } from "./plan-normalize.js";
21
- import type { ApplyWorkspacePlanOptions, WorkspacePlanReceipt } from "./plan-types.js";
22
- import { type PreparedWorkspaceFile } from "./workspace-apply-files.js";
@@ -1,178 +0,0 @@
1
- export const fileIdentity = (file) => `${file.root}\0${file.relPath}`;
2
- export function holdsCurrentOwnership(absPath, ownership) {
3
- return (ownership === undefined || workspaceFileHasOwnership(absPath, ownership));
4
- }
5
- /** Only materializer-owned files are held to the writer's ownership. */
6
- function ownershipCurrent(file, ownership) {
7
- return (file.ownership !== "materializer" ||
8
- holdsCurrentOwnership(file.absPath, ownership));
9
- }
10
- // An owned path holds this package's own earlier output, so re-applying
11
- // into the same workspace replaces it rather than failing the turn — once
12
- // the bytes already there prove that provenance. The replacement keeps what
13
- // an agent appended below a generated context file (its memory), so it can
14
- // differ from the planned bytes. `reject` still refuses: that policy is
15
- // "nothing may pre-exist here", which is a statement about the workspace,
16
- // not authorship.
17
- function replacementContent(file, context) {
18
- const planned = context.plannedContent.get(fileIdentity(file));
19
- if (context.existingFiles !== "allow-identical" ||
20
- file.ownership !== "materializer" ||
21
- planned === undefined) {
22
- return undefined;
23
- }
24
- return materializerReplacement(file.relPath, () => regularFileContent(file.absPath), planned);
25
- }
26
- // Validate the complete plan before the first write so a late traversal or
27
- // existing symlink cannot leave a partially materialized workspace.
28
- export function planWrites(filesToApply, context) {
29
- const writes = [];
30
- for (const file of filesToApply) {
31
- assertNoSymlinkPathComponents(file.rootDir, file.absPath);
32
- if (!workspaceFileExists(file.absPath)) {
33
- writes.push(file);
34
- continue;
35
- }
36
- const content = replacementContent(file, context);
37
- if (content !== undefined) {
38
- writes.push({ ...file, content });
39
- continue;
40
- }
41
- if (context.existingFiles === "reject" || !workspaceFileMatches(file)) {
42
- throw new Error(workspaceFileConflictMessage(file, context.existingFiles, context.publicPlanDigest));
43
- }
44
- writes.push(file);
45
- }
46
- return writes;
47
- }
48
- // rename() replaces in one step, so no window exists in which the path is
49
- // missing and a concurrent spawn reads nothing. The staged inode moves onto the
50
- // path, which is what rollback identifies the file by, so an abort still
51
- // removes exactly what this call published — leaving the path ABSENT rather
52
- // than restored to the superseded bytes. That is the fail-closed direction: the
53
- // harness whose flag names a missing file refuses to start, whereas silently
54
- // restored previous-turn bytes would run the wrong prompt. A replaced file that
55
- // holds an agent's appended text is the exception: it is linked aside first and
56
- // restored, because removing it would delete the agent's own text.
57
- function replacePublishedFile(entry, context) {
58
- assertNoSymlinkPathComponents(context.rootFor(entry.file.root), entry.file.absPath);
59
- if (entry.file.content !== context.plannedContent.get(fileIdentity(entry.file))) {
60
- const superseded = join(dirname(entry.file.absPath), `.agent-profile-${randomUUID()}.superseded`);
61
- linkSync(entry.file.absPath, superseded);
62
- entry.superseded = superseded;
63
- }
64
- renameSync(entry.temporaryPath, entry.file.absPath);
65
- entry.published = true;
66
- }
67
- function publishPreparedFile(entry, context) {
68
- assertNoSymlinkPathComponents(context.rootFor(entry.file.root), entry.file.absPath);
69
- try {
70
- linkSync(entry.temporaryPath, entry.file.absPath);
71
- entry.published = true;
72
- }
73
- catch (error) {
74
- if (isFsError(error, "EEXIST") &&
75
- context.existingFiles === "allow-identical" &&
76
- workspaceFileMatches(entry.file) &&
77
- ownershipCurrent(entry.file, context.ownership)) {
78
- return;
79
- }
80
- if (isFsError(error, "EEXIST") &&
81
- replacementContent(entry.file, context) === entry.file.content) {
82
- // The replacement is recomposed from the bytes there now, so an
83
- // agent append that landed since validation refuses below instead
84
- // of being overwritten; one landing between that read and the
85
- // rename is not detected.
86
- replacePublishedFile(entry, context);
87
- return;
88
- }
89
- if (isFsError(error, "EEXIST")) {
90
- throw new Error(workspaceFileConflictMessage(entry.file, context.existingFiles, context.publicPlanDigest), { cause: error });
91
- }
92
- throw error;
93
- }
94
- }
95
- // Prepared temp links stay alive through publication and rollback. Their
96
- // inodes distinguish files created here from concurrent replacements.
97
- export function publishWrites(writes, prepared, context) {
98
- for (const file of writes) {
99
- if (context.existingFiles === "allow-identical" &&
100
- workspaceFileExists(file.absPath) &&
101
- workspaceFileMatches(file) &&
102
- ownershipCurrent(file, context.ownership)) {
103
- continue;
104
- }
105
- prepared.push(prepareWorkspaceFile(file.rootDir, file, context.ownership));
106
- }
107
- for (const entry of prepared)
108
- publishPreparedFile(entry, context);
109
- }
110
- export function rollbackPublished(prepared,
111
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- rollback rethrows the caught failure unchanged; an exception has no schema to parse at a boundary.
112
- error) {
113
- const cleanupErrors = [];
114
- for (const entry of [...prepared].reverse()) {
115
- try {
116
- if (entry.published &&
117
- workspacePathHasIdentity(entry.file.absPath, entry.identity)) {
118
- if (entry.superseded) {
119
- renameSync(entry.superseded, entry.file.absPath);
120
- }
121
- else {
122
- unlinkSync(entry.file.absPath);
123
- }
124
- }
125
- }
126
- catch (cleanupError) {
127
- cleanupErrors.push(cleanupError);
128
- }
129
- try {
130
- if (entry.superseded)
131
- unlinkSync(entry.superseded);
132
- }
133
- catch (cleanupError) {
134
- if (!isFsError(cleanupError, "ENOENT"))
135
- cleanupErrors.push(cleanupError);
136
- }
137
- try {
138
- if (workspacePathHasIdentity(entry.temporaryPath, entry.identity)) {
139
- unlinkSync(entry.temporaryPath);
140
- }
141
- }
142
- catch (cleanupError) {
143
- cleanupErrors.push(cleanupError);
144
- }
145
- }
146
- if (cleanupErrors.length > 0) {
147
- throw new AggregateError([error, ...cleanupErrors], "Failed to apply workspace plan and roll back published files");
148
- }
149
- throw error;
150
- }
151
- export function removeTemporaries(prepared, receipt) {
152
- const cleanupErrors = [];
153
- const temporaryPaths = [];
154
- for (const entry of prepared) {
155
- for (const path of [entry.temporaryPath, entry.superseded]) {
156
- if (path === undefined)
157
- continue;
158
- try {
159
- unlinkSync(path);
160
- }
161
- catch (error) {
162
- if (!isFsError(error, "ENOENT")) {
163
- cleanupErrors.push(error);
164
- temporaryPaths.push(path);
165
- }
166
- }
167
- }
168
- }
169
- if (cleanupErrors.length > 0) {
170
- throw new WorkspacePlanCleanupError(cleanupErrors, receipt, temporaryPaths);
171
- }
172
- }
173
- import { randomUUID } from "node:crypto";
174
- import { linkSync, renameSync, unlinkSync } from "node:fs";
175
- import { dirname, join } from "node:path";
176
- import { materializerReplacement } from "./harness.js";
177
- import { WorkspacePlanCleanupError } from "./plan-types.js";
178
- import { assertNoSymlinkPathComponents, isFsError, prepareWorkspaceFile, regularFileContent, workspaceFileConflictMessage, workspaceFileExists, workspaceFileHasOwnership, workspaceFileMatches, workspacePathHasIdentity, } from "./workspace-apply-files.js";
@@ -1,11 +0,0 @@
1
- export declare function applyResolvedWorkspacePlan(publicPlan: WorkspacePlan, resolvedPlan: ResolvedWorkspacePlan, workspaceDir: string, options: ApplyWorkspacePlanOptions, publicPlanDigest?: string): WorkspacePlanApplication;
2
- export interface ResolvedWorkspacePlan {
3
- files: NormalizedPlanFile[];
4
- env: Record<string, string>;
5
- flags: string[];
6
- systemPrompt?: string;
7
- appendSystemPrompt?: string;
8
- systemPromptBinding?: WorkspacePlanSystemPromptBinding;
9
- }
10
- import { type NormalizedPlanFile } from "./plan-normalize.js";
11
- import type { ApplyWorkspacePlanOptions, WorkspacePlan, WorkspacePlanApplication, WorkspacePlanSystemPromptBinding } from "./plan-types.js";
@@ -1,124 +0,0 @@
1
- function readApplyInput(publicPlan, workspaceDir, options) {
2
- const workspaceRoot = resolve(workspaceDir);
3
- if (options.agentDir !== undefined && !isAbsolute(options.agentDir)) {
4
- throw new Error("ApplyWorkspacePlanOptions.agentDir must be absolute");
5
- }
6
- const agentRoot = options.agentDir === undefined ? undefined : resolve(options.agentDir);
7
- const publicFiles = normalizePlanFiles(publicPlan.files);
8
- if (publicFiles.some((file) => file.root === "agent") && !agentRoot) {
9
- throw new Error("Workspace plan contains agent-root files but ApplyWorkspacePlanOptions.agentDir is missing");
10
- }
11
- const rootFor = (root) => root === "workspace" ? workspaceRoot : agentRoot;
12
- const runtimeOnlyFiles = new Set((options.runtimeOnlyFiles ?? []).map(normalizeWorkspaceRelativePath));
13
- const ownership = validateWorkspaceOwnership(options.ownership);
14
- const privateFilePaths = new Set(publicFiles.filter((file) => file.secretSlots.length > 0).map(fileIdentity));
15
- const existingFiles = options.existingFiles ?? "allow-identical";
16
- if (existingFiles !== "allow-identical" && existingFiles !== "reject") {
17
- throw new Error(`Unsupported existing workspace file policy: ${existingFiles}`);
18
- }
19
- return {
20
- rootFor,
21
- runtimeOnlyFiles,
22
- ownership,
23
- privateFilePaths,
24
- existingFiles,
25
- };
26
- }
27
- // A prior application of this exact plan makes the workspace live agent
28
- // state. Preserve changed and removed files. Refresh private generated files
29
- // because the public digest excludes secret values, and repair unchanged
30
- // materializer files that still have the writer's ownership.
31
- function selectResumeFiles(files, ownership, privateFilePaths) {
32
- return files.filter((file) => {
33
- if (file.ownership !== "materializer")
34
- return false;
35
- assertNoSymlinkPathComponents(file.rootDir, file.absPath);
36
- if (!workspaceFileExists(file.absPath))
37
- return false;
38
- const matches = workspaceFileMatches(file);
39
- const hasCurrentOwnership = holdsCurrentOwnership(file.absPath, ownership);
40
- if (privateFilePaths.has(fileIdentity(file))) {
41
- return !matches || !hasCurrentOwnership;
42
- }
43
- return matches && !hasCurrentOwnership;
44
- });
45
- }
46
- export function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDir, options, publicPlanDigest = hashWorkspacePlan(publicPlan)) {
47
- const { rootFor, runtimeOnlyFiles, ownership, privateFilePaths, existingFiles, } = readApplyInput(publicPlan, workspaceDir, options);
48
- const privateRuntimeFiles = Object.fromEntries(resolvedPlan.files
49
- .filter((file) => file.root === "workspace" && runtimeOnlyFiles.has(file.relPath))
50
- .map((file) => [file.relPath, file.content]));
51
- const files = resolvedPlan.files
52
- .filter((file) => file.root !== "workspace" || !runtimeOnlyFiles.has(file.relPath))
53
- .map((file) => ({
54
- ...file,
55
- rootDir: rootFor(file.root),
56
- absPath: resolveWorkspaceFilePath(rootFor(file.root), file.relPath),
57
- }));
58
- const application = {
59
- receipt: {
60
- workspacePlanDigest: publicPlanDigest,
61
- env: Object.fromEntries(Object.entries(publicPlan.env).map(([name, value]) => [
62
- name,
63
- typeof value === "string" ? value : { ...value },
64
- ])),
65
- flags: bindWorkspaceLaunchFlags(publicPlan, publicPlan.flags, workspaceDir).map(normalizeWorkspacePlanArgument),
66
- ...promptIntentFields(publicPlan, samePromptIntentValue),
67
- ...systemPromptBindingField(publicPlan),
68
- unsupported: publicPlan.unsupported.map((entry) => ({ ...entry })),
69
- written: files
70
- .filter((file) => file.root === "workspace")
71
- .map((file) => file.relPath),
72
- writtenFiles: files.map(({ root, relPath }) => ({ root, relPath })),
73
- },
74
- spawn: {
75
- env: { ...resolvedPlan.env },
76
- flags: bindWorkspaceLaunchFlags(publicPlan, resolvedPlan.flags, workspaceDir),
77
- ...promptIntentFields(resolvedPlan, samePromptIntentValue),
78
- ...systemPromptBindingField(resolvedPlan),
79
- },
80
- runtimeOnlyFiles: privateRuntimeFiles,
81
- };
82
- // `reject` does not resume because it requires an empty target.
83
- const resumeSamePlan = existingFiles === "allow-identical" &&
84
- options.appliedPlanDigest !== undefined &&
85
- options.appliedPlanDigest === publicPlanDigest;
86
- const filesToApply = resumeSamePlan
87
- ? selectResumeFiles(files, ownership, privateFilePaths)
88
- : files;
89
- if (resumeSamePlan && filesToApply.length === 0) {
90
- return application;
91
- }
92
- const context = {
93
- rootFor,
94
- existingFiles,
95
- ownership,
96
- publicPlanDigest,
97
- plannedContent: new Map(filesToApply.map((file) => [fileIdentity(file), file.content])),
98
- };
99
- const writes = planWrites(filesToApply, context);
100
- // Parent directories remain on failure because they may contain concurrent
101
- // workspace state that this call does not own.
102
- for (const file of writes) {
103
- mkdirSync(dirname(file.absPath), { recursive: true });
104
- assertNoSymlinkPathComponents(file.rootDir, file.absPath);
105
- }
106
- const prepared = [];
107
- try {
108
- publishWrites(writes, prepared, context);
109
- }
110
- catch (error) {
111
- rollbackPublished(prepared, error);
112
- }
113
- removeTemporaries(prepared, application.receipt);
114
- return application;
115
- }
116
- import { mkdirSync } from "node:fs";
117
- import { dirname, isAbsolute, resolve } from "node:path";
118
- import { hashWorkspacePlan } from "./plan-digest.js";
119
- import { normalizePlanFiles, normalizeWorkspacePlanArgument, } from "./plan-normalize.js";
120
- import { promptIntentFields, samePromptIntentValue, systemPromptBindingField, } from "./plan-prompt.js";
121
- import { assertNoSymlinkPathComponents, resolveWorkspaceFilePath, validateWorkspaceOwnership, workspaceFileExists, workspaceFileMatches, } from "./workspace-apply-files.js";
122
- import { fileIdentity, holdsCurrentOwnership, planWrites, publishWrites, removeTemporaries, rollbackPublished, } from "./workspace-apply-publish.js";
123
- import { normalizeWorkspaceRelativePath } from "./workspace-files.js";
124
- import { bindWorkspaceLaunchFlags } from "./workspace-launch.js";
@@ -1,8 +0,0 @@
1
- export declare const compareStrings: (a: string, b: string) => 1 | 0 | -1;
2
- export declare const DEFAULT_FILE_MODE = 420;
3
- export declare const PRIVATE_FILE_MODE = 384;
4
- export declare const EXECUTABLE_FILE_MODE = 493;
5
- export declare const SAFE_WRITE_FLAGS: number;
6
- export declare function normalizeWorkspaceRelativePath(pathValue: string): string;
7
- export declare function hasControlCharacter(value: string): boolean;
8
- export declare function validateFileMode(mode: number | undefined): number;
@@ -1,46 +0,0 @@
1
- export const compareStrings = (a, b) => a < b ? -1 : a > b ? 1 : 0;
2
- export const DEFAULT_FILE_MODE = 0o644;
3
- export const PRIVATE_FILE_MODE = 0o600;
4
- export const EXECUTABLE_FILE_MODE = 0o755;
5
- export const SAFE_WRITE_FLAGS = fsConstants.O_WRONLY |
6
- fsConstants.O_CREAT |
7
- fsConstants.O_EXCL |
8
- (typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0);
9
- export function normalizeWorkspaceRelativePath(pathValue) {
10
- if (typeof pathValue !== "string" ||
11
- !pathValue ||
12
- hasControlCharacter(pathValue)) {
13
- throw new Error("Workspace file path must be non-empty and contain no control characters");
14
- }
15
- if (isAbsolute(pathValue) ||
16
- win32.isAbsolute(pathValue) ||
17
- /^[A-Za-z]:/.test(pathValue)) {
18
- throw new Error(`Workspace file path must be relative: ${pathValue}`);
19
- }
20
- const segments = pathValue.replace(/\\/g, "/").split("/");
21
- if (segments.some((segment) => !segment ||
22
- segment === "." ||
23
- segment === ".." ||
24
- segment === ".git" ||
25
- segment === ".sidecar")) {
26
- throw new Error(`Workspace file path must be canonical: ${pathValue}`);
27
- }
28
- return segments.join("/");
29
- }
30
- export function hasControlCharacter(value) {
31
- for (let index = 0; index < value.length; index++) {
32
- const code = value.charCodeAt(index);
33
- if (code < 0x20 || code === 0x7f)
34
- return true;
35
- }
36
- return false;
37
- }
38
- export function validateFileMode(mode) {
39
- const normalized = mode ?? DEFAULT_FILE_MODE;
40
- if (!Number.isInteger(normalized) || normalized < 0 || normalized > 0o777) {
41
- throw new Error(`Workspace file mode must be between 0o000 and 0o777: ${mode}`);
42
- }
43
- return normalized;
44
- }
45
- import { constants as fsConstants } from "node:fs";
46
- import { isAbsolute, win32 } from "node:path";
@@ -1,3 +0,0 @@
1
- import type { WorkspacePlan } from "./plan-types.js";
2
- /** Bind launch paths; omit the workspace to hash portable content identity. */
3
- export declare function bindWorkspaceLaunchFlags<T>(plan: WorkspacePlan, flags: readonly T[], workspaceDir?: string): (T | string)[];
@@ -1,13 +0,0 @@
1
- import { resolve } from "node:path";
2
- import { CODEX_SYSTEM_PROMPT_FILE } from "./harness.js";
3
- import { tomlStr } from "./plan-json.js";
4
- /** Bind launch paths; omit the workspace to hash portable content identity. */
5
- export function bindWorkspaceLaunchFlags(plan, flags, workspaceDir) {
6
- return flags.map((flag, at) => plan.harness === "codex" &&
7
- plan.flags[at - 1] === "-c" &&
8
- plan.flags[at] === `model_instructions_file=${CODEX_SYSTEM_PROMPT_FILE}`
9
- ? workspaceDir === undefined
10
- ? `model_instructions_file=${CODEX_SYSTEM_PROMPT_FILE}`
11
- : `model_instructions_file=${tomlStr(resolve(workspaceDir, CODEX_SYSTEM_PROMPT_FILE))}`
12
- : flag);
13
- }