@tangle-network/agent-profile-materialize 1.1.6 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1226 -29
- package/dist/index.js +4987 -23
- package/package.json +7 -5
- package/dist/candidate-types.d.ts +0 -36
- package/dist/candidate-types.js +0 -1
- package/dist/candidate.d.ts +0 -9
- package/dist/candidate.js +0 -283
- package/dist/commands.d.ts +0 -2
- package/dist/commands.js +0 -32
- package/dist/control-maps.d.ts +0 -58
- package/dist/control-maps.js +0 -277
- package/dist/execution-controls.d.ts +0 -5
- package/dist/execution-controls.js +0 -199
- package/dist/harness-prompt.d.ts +0 -22
- package/dist/harness-prompt.js +0 -64
- package/dist/harness.d.ts +0 -131
- package/dist/harness.js +0 -416
- package/dist/hooks.d.ts +0 -6
- package/dist/hooks.js +0 -250
- package/dist/managed-workspace-bind.d.ts +0 -4
- package/dist/managed-workspace-bind.js +0 -112
- package/dist/managed-workspace-stage.d.ts +0 -25
- package/dist/managed-workspace-stage.js +0 -199
- package/dist/managed-workspace-types.d.ts +0 -38
- package/dist/managed-workspace-types.js +0 -1
- package/dist/managed-workspace.d.ts +0 -37
- package/dist/managed-workspace.js +0 -290
- package/dist/mcp-config-ownership.d.ts +0 -13
- package/dist/mcp-config-ownership.js +0 -79
- package/dist/mcp-opencode.d.ts +0 -25
- package/dist/mcp-opencode.js +0 -52
- package/dist/mcp-timeouts.d.ts +0 -7
- package/dist/mcp-timeouts.js +0 -48
- package/dist/mcp.d.ts +0 -4
- package/dist/mcp.js +0 -246
- package/dist/model-id.d.ts +0 -55
- package/dist/model-id.js +0 -67
- package/dist/pi-models-config.d.ts +0 -32
- package/dist/pi-models-config.js +0 -65
- package/dist/plan-application.d.ts +0 -10
- package/dist/plan-application.js +0 -190
- package/dist/plan-digest.d.ts +0 -18
- package/dist/plan-digest.js +0 -116
- package/dist/plan-json.d.ts +0 -13
- package/dist/plan-json.js +0 -91
- package/dist/plan-normalize.d.ts +0 -12
- package/dist/plan-normalize.js +0 -168
- package/dist/plan-prompt.d.ts +0 -38
- package/dist/plan-prompt.js +0 -49
- package/dist/plan-support.d.ts +0 -9
- package/dist/plan-support.js +0 -22
- package/dist/plan-types.d.ts +0 -341
- package/dist/plan-types.js +0 -105
- package/dist/prime-agent-dir.d.ts +0 -64
- package/dist/prime-agent-dir.js +0 -173
- package/dist/prime-fork.d.ts +0 -155
- package/dist/prime-fork.js +0 -262
- package/dist/prime-profile.d.ts +0 -88
- package/dist/prime-profile.js +0 -297
- package/dist/profile-bundle-cache.d.ts +0 -37
- package/dist/profile-bundle-cache.js +0 -320
- package/dist/profile-bundle-key.d.ts +0 -69
- package/dist/profile-bundle-key.js +0 -62
- package/dist/profile-bundle-lru.d.ts +0 -56
- package/dist/profile-bundle-lru.js +0 -220
- package/dist/profile-materializer.d.ts +0 -24
- package/dist/profile-materializer.js +0 -114
- package/dist/profile-plan.d.ts +0 -16
- package/dist/profile-plan.js +0 -90
- package/dist/profile-prompt.d.ts +0 -4
- package/dist/profile-prompt.js +0 -107
- package/dist/profile-support.d.ts +0 -8
- package/dist/profile-support.js +0 -6
- package/dist/resource-lowering.d.ts +0 -6
- package/dist/resource-lowering.js +0 -103
- package/dist/resource-resolution.d.ts +0 -27
- package/dist/resource-resolution.js +0 -265
- package/dist/secret-concurrent-revision.d.ts +0 -30
- package/dist/secret-concurrent-revision.js +0 -291
- package/dist/secret-session-view.d.ts +0 -36
- package/dist/secret-session-view.js +0 -179
- package/dist/subagents.d.ts +0 -6
- package/dist/subagents.js +0 -137
- package/dist/tool-bin-path.d.ts +0 -23
- package/dist/tool-bin-path.js +0 -61
- package/dist/workspace-apply-files.d.ts +0 -57
- package/dist/workspace-apply-files.js +0 -193
- package/dist/workspace-apply-publish.d.ts +0 -22
- package/dist/workspace-apply-publish.js +0 -178
- package/dist/workspace-apply.d.ts +0 -11
- package/dist/workspace-apply.js +0 -124
- package/dist/workspace-files.d.ts +0 -8
- package/dist/workspace-files.js +0 -46
- package/dist/workspace-launch.d.ts +0 -3
- package/dist/workspace-launch.js +0 -13
package/dist/tool-bin-path.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { isAbsolute } from "node:path";
|
|
2
|
-
/** The colon-separated, absolute CLI bin dirs a sandbox app mounts for its tools. */
|
|
3
|
-
export const SANDBOX_TOOL_BIN_DIRS_ENV = "SANDBOX_TOOL_BIN_DIRS";
|
|
4
|
-
/**
|
|
5
|
-
* The tool bin dirs a `SANDBOX_TOOL_BIN_DIRS` value names, or `[]` when it is
|
|
6
|
-
* unset or blank.
|
|
7
|
-
*
|
|
8
|
-
* Fail loud, never silently degrade: a malformed value (a relative segment,
|
|
9
|
-
* or an empty segment from a stray/leading/trailing colon) throws rather than
|
|
10
|
-
* silently dropping a tool dir from PATH or admitting a relative path that
|
|
11
|
-
* resolves against whatever the process cwd happens to be.
|
|
12
|
-
*/
|
|
13
|
-
export function parseSandboxToolBinDirs(value) {
|
|
14
|
-
const trimmed = value?.trim();
|
|
15
|
-
if (!trimmed)
|
|
16
|
-
return [];
|
|
17
|
-
const segments = trimmed.split(":");
|
|
18
|
-
for (const segment of segments) {
|
|
19
|
-
if (!segment || !isAbsolute(segment)) {
|
|
20
|
-
throw new Error(`SANDBOX_TOOL_BIN_DIRS must be a colon-separated list of absolute paths with no empty segments, got: ${trimmed}`);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return segments;
|
|
24
|
-
}
|
|
25
|
-
function assertToolBinDir(dir) {
|
|
26
|
-
// A ':' would split one declared dir into several PATH entries, admitting
|
|
27
|
-
// a dir no profile declared.
|
|
28
|
-
if (!dir || !isAbsolute(dir) || dir.includes(":")) {
|
|
29
|
-
throw new Error(`a tool bin dir must be an absolute path without ':', got: ${dir}`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* A launch environment whose tool bin dirs are `toolBinDirs`, at the TAIL of
|
|
34
|
-
* PATH after every Nix, npm, image and system entry, so a tool never shadows
|
|
35
|
-
* a platform-owned binary of the same name.
|
|
36
|
-
*
|
|
37
|
-
* They replace `appToolBinDirs` (by default the dirs the environment's own
|
|
38
|
-
* `SANDBOX_TOOL_BIN_DIRS` names) wherever those sit in PATH, so an app's older
|
|
39
|
-
* copy of the same tool cannot win by PATH order. Relative PATH entries are
|
|
40
|
-
* dropped too: they resolve against whatever cwd the turn moves to.
|
|
41
|
-
*/
|
|
42
|
-
export function bindLaunchToolBinDirs(env, toolBinDirs, appToolBinDirs = parseSandboxToolBinDirs(env[SANDBOX_TOOL_BIN_DIRS_ENV])) {
|
|
43
|
-
for (const dir of toolBinDirs)
|
|
44
|
-
assertToolBinDir(dir);
|
|
45
|
-
const dirs = [...new Set(toolBinDirs)];
|
|
46
|
-
// No PATH to bind: the spawner appends SANDBOX_TOOL_BIN_DIRS to its default.
|
|
47
|
-
if (env.PATH === undefined) {
|
|
48
|
-
return { ...env, [SANDBOX_TOOL_BIN_DIRS_ENV]: dirs.join(":") };
|
|
49
|
-
}
|
|
50
|
-
const replaced = new Set(appToolBinDirs);
|
|
51
|
-
const entries = env.PATH.split(":").filter((entry) => isAbsolute(entry) && !replaced.has(entry));
|
|
52
|
-
for (const dir of dirs) {
|
|
53
|
-
if (!entries.includes(dir))
|
|
54
|
-
entries.push(dir);
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
...env,
|
|
58
|
-
PATH: entries.join(":"),
|
|
59
|
-
[SANDBOX_TOOL_BIN_DIRS_ENV]: dirs.join(":"),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
@@ -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";
|
package/dist/workspace-apply.js
DELETED
|
@@ -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;
|