@tangle-network/agent-profile-materialize 0.5.0 → 0.5.1
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 +8 -0
- package/dist/index.js +147 -43
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -60,9 +60,17 @@ export interface WorkspacePlanReceipt {
|
|
|
60
60
|
unsupported: Unsupported[];
|
|
61
61
|
written: string[];
|
|
62
62
|
}
|
|
63
|
+
/** The plan was applied, but one or more private staging files remain. */
|
|
64
|
+
export declare class WorkspacePlanCleanupError extends AggregateError {
|
|
65
|
+
readonly receipt: WorkspacePlanReceipt;
|
|
66
|
+
readonly temporaryPaths: readonly string[];
|
|
67
|
+
constructor(errors: readonly unknown[], receipt: WorkspacePlanReceipt, temporaryPaths: readonly string[]);
|
|
68
|
+
}
|
|
63
69
|
export interface ApplyWorkspacePlanOptions {
|
|
64
70
|
/** Plan files consumed in memory by the runtime and never persisted to cwd. */
|
|
65
71
|
runtimeOnlyFiles?: readonly string[];
|
|
72
|
+
/** Existing workspace paths are either exact idempotent inputs or forbidden. */
|
|
73
|
+
existingFiles?: "allow-identical" | "reject";
|
|
66
74
|
}
|
|
67
75
|
/** Fail when a profile asks for controls the chosen harness cannot execute. */
|
|
68
76
|
export declare function assertWorkspacePlanSupported(plan: Pick<WorkspacePlan, "unsupported">): void;
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* cli-bridge and agent-dev-container — this is the single source those re-export.
|
|
23
23
|
*/
|
|
24
24
|
import { createHash, randomUUID } from "node:crypto";
|
|
25
|
-
import { closeSync, fchmodSync, constants as fsConstants, linkSync, lstatSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
25
|
+
import { closeSync, fchmodSync, constants as fsConstants, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
26
26
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32, } from "node:path";
|
|
27
27
|
import { agentCandidateProfileActivationSchema, agentCandidateProfilePlanEvidenceSchema, agentCandidateProfilePlanMaterialSchema, agentCandidateProfileSchema, agentProfileSchema, canonicalCandidateBytes, canonicalCandidateDigest, resolveNativeWebTools, } from "@tangle-network/agent-interface";
|
|
28
28
|
/** Resolve inline bytes already present on the profile. Remote resources must
|
|
@@ -37,6 +37,17 @@ function resolveInlineRef(ref) {
|
|
|
37
37
|
}
|
|
38
38
|
return null; // github / unresolved
|
|
39
39
|
}
|
|
40
|
+
/** The plan was applied, but one or more private staging files remain. */
|
|
41
|
+
export class WorkspacePlanCleanupError extends AggregateError {
|
|
42
|
+
receipt;
|
|
43
|
+
temporaryPaths;
|
|
44
|
+
constructor(errors, receipt, temporaryPaths) {
|
|
45
|
+
super(errors, "Applied workspace plan but failed to remove temporary files");
|
|
46
|
+
this.name = "WorkspacePlanCleanupError";
|
|
47
|
+
this.receipt = receipt;
|
|
48
|
+
this.temporaryPaths = Object.freeze([...temporaryPaths]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
40
51
|
/** Fail when a profile asks for controls the chosen harness cannot execute. */
|
|
41
52
|
export function assertWorkspacePlanSupported(plan) {
|
|
42
53
|
if (plan.unsupported.length > 0) {
|
|
@@ -952,6 +963,10 @@ export function applyAgentCandidateWorkspacePlan(plan, workspaceDir, targetWorks
|
|
|
952
963
|
export function applyWorkspacePlan(plan, workspaceDir, options = {}) {
|
|
953
964
|
const workspaceRoot = resolve(workspaceDir);
|
|
954
965
|
const runtimeOnlyFiles = new Set((options.runtimeOnlyFiles ?? []).map(normalizeWorkspaceRelativePath));
|
|
966
|
+
const existingFiles = options.existingFiles ?? "allow-identical";
|
|
967
|
+
if (existingFiles !== "allow-identical" && existingFiles !== "reject") {
|
|
968
|
+
throw new Error(`Unsupported existing workspace file policy: ${existingFiles}`);
|
|
969
|
+
}
|
|
955
970
|
const files = normalizePlanFiles(plan.files)
|
|
956
971
|
.filter((file) => !runtimeOnlyFiles.has(file.relPath))
|
|
957
972
|
.map((file) => ({
|
|
@@ -962,24 +977,77 @@ export function applyWorkspacePlan(plan, workspaceDir, options = {}) {
|
|
|
962
977
|
// existing symlink cannot leave a partially materialized workspace.
|
|
963
978
|
for (const file of files) {
|
|
964
979
|
assertNoSymlinkPathComponents(workspaceRoot, file.absPath);
|
|
965
|
-
if (workspaceFileExists(file.absPath) &&
|
|
980
|
+
if (workspaceFileExists(file.absPath) &&
|
|
981
|
+
(existingFiles === "reject" || !workspaceFileMatches(file))) {
|
|
966
982
|
throw new Error(`Refusing to replace existing workspace file: ${file.relPath}`);
|
|
967
983
|
}
|
|
968
984
|
}
|
|
985
|
+
// Parent directories remain on failure because they may contain concurrent
|
|
986
|
+
// workspace state that this call does not own.
|
|
969
987
|
for (const file of files) {
|
|
970
988
|
mkdirSync(dirname(file.absPath), { recursive: true });
|
|
971
989
|
}
|
|
972
990
|
for (const file of files) {
|
|
973
991
|
assertNoSymlinkPathComponents(workspaceRoot, file.absPath);
|
|
974
992
|
}
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
993
|
+
const prepared = [];
|
|
994
|
+
// Prepared temp links stay alive through publication and rollback. Their
|
|
995
|
+
// inodes distinguish files created here from concurrent replacements.
|
|
996
|
+
try {
|
|
997
|
+
for (const file of files) {
|
|
998
|
+
if (existingFiles === "allow-identical" &&
|
|
999
|
+
workspaceFileExists(file.absPath) &&
|
|
1000
|
+
workspaceFileMatches(file)) {
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
prepared.push(prepareWorkspaceFile(workspaceRoot, file));
|
|
1004
|
+
}
|
|
1005
|
+
for (const entry of prepared) {
|
|
1006
|
+
assertNoSymlinkPathComponents(workspaceRoot, entry.file.absPath);
|
|
1007
|
+
try {
|
|
1008
|
+
linkSync(entry.temporaryPath, entry.file.absPath);
|
|
1009
|
+
entry.published = true;
|
|
1010
|
+
}
|
|
1011
|
+
catch (error) {
|
|
1012
|
+
if (isFsError(error, "EEXIST") &&
|
|
1013
|
+
existingFiles === "allow-identical" &&
|
|
1014
|
+
workspaceFileMatches(entry.file)) {
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
if (isFsError(error, "EEXIST")) {
|
|
1018
|
+
throw new Error(`Refusing to replace existing workspace file: ${entry.file.relPath}`, { cause: error });
|
|
1019
|
+
}
|
|
1020
|
+
throw error;
|
|
1021
|
+
}
|
|
979
1022
|
}
|
|
980
|
-
written.push(file.relPath);
|
|
981
1023
|
}
|
|
982
|
-
|
|
1024
|
+
catch (error) {
|
|
1025
|
+
const cleanupErrors = [];
|
|
1026
|
+
for (const entry of [...prepared].reverse()) {
|
|
1027
|
+
try {
|
|
1028
|
+
if (entry.published &&
|
|
1029
|
+
workspacePathHasIdentity(entry.file.absPath, entry.identity)) {
|
|
1030
|
+
unlinkSync(entry.file.absPath);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
catch (cleanupError) {
|
|
1034
|
+
cleanupErrors.push(cleanupError);
|
|
1035
|
+
}
|
|
1036
|
+
try {
|
|
1037
|
+
if (workspacePathHasIdentity(entry.temporaryPath, entry.identity)) {
|
|
1038
|
+
unlinkSync(entry.temporaryPath);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
catch (cleanupError) {
|
|
1042
|
+
cleanupErrors.push(cleanupError);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (cleanupErrors.length > 0) {
|
|
1046
|
+
throw new AggregateError([error, ...cleanupErrors], "Failed to apply workspace plan and roll back published files");
|
|
1047
|
+
}
|
|
1048
|
+
throw error;
|
|
1049
|
+
}
|
|
1050
|
+
const receipt = {
|
|
983
1051
|
workspacePlanDigest: hashWorkspacePlan(plan),
|
|
984
1052
|
env: plan.env,
|
|
985
1053
|
flags: plan.flags,
|
|
@@ -987,8 +1055,25 @@ export function applyWorkspacePlan(plan, workspaceDir, options = {}) {
|
|
|
987
1055
|
? {}
|
|
988
1056
|
: { systemPrompt: plan.systemPrompt }),
|
|
989
1057
|
unsupported: plan.unsupported,
|
|
990
|
-
written,
|
|
1058
|
+
written: files.map((file) => file.relPath),
|
|
991
1059
|
};
|
|
1060
|
+
const cleanupErrors = [];
|
|
1061
|
+
const temporaryPaths = [];
|
|
1062
|
+
for (const entry of prepared) {
|
|
1063
|
+
try {
|
|
1064
|
+
unlinkSync(entry.temporaryPath);
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
if (!isFsError(error, "ENOENT")) {
|
|
1068
|
+
cleanupErrors.push(error);
|
|
1069
|
+
temporaryPaths.push(entry.temporaryPath);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
if (cleanupErrors.length > 0) {
|
|
1074
|
+
throw new WorkspacePlanCleanupError(cleanupErrors, receipt, temporaryPaths);
|
|
1075
|
+
}
|
|
1076
|
+
return receipt;
|
|
992
1077
|
}
|
|
993
1078
|
function workspaceFileExists(pathValue) {
|
|
994
1079
|
try {
|
|
@@ -1007,6 +1092,59 @@ function workspaceFileMatches(file) {
|
|
|
1007
1092
|
(stats.mode & 0o777) === file.mode &&
|
|
1008
1093
|
readFileSync(file.absPath, "utf8") === file.content);
|
|
1009
1094
|
}
|
|
1095
|
+
function prepareWorkspaceFile(workspaceRoot, file) {
|
|
1096
|
+
assertNoSymlinkPathComponents(workspaceRoot, file.absPath);
|
|
1097
|
+
const temporaryPath = join(dirname(file.absPath), `.agent-profile-${randomUUID()}.tmp`);
|
|
1098
|
+
let descriptor;
|
|
1099
|
+
try {
|
|
1100
|
+
descriptor = openSync(temporaryPath, SAFE_WRITE_FLAGS, 0o600);
|
|
1101
|
+
writeFileSync(descriptor, file.content, "utf8");
|
|
1102
|
+
fchmodSync(descriptor, file.mode);
|
|
1103
|
+
const stats = fstatSync(descriptor);
|
|
1104
|
+
const descriptorToClose = descriptor;
|
|
1105
|
+
descriptor = undefined;
|
|
1106
|
+
closeSync(descriptorToClose);
|
|
1107
|
+
return {
|
|
1108
|
+
file,
|
|
1109
|
+
temporaryPath,
|
|
1110
|
+
identity: { device: stats.dev, inode: stats.ino },
|
|
1111
|
+
published: false,
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
catch (error) {
|
|
1115
|
+
const cleanupErrors = [];
|
|
1116
|
+
if (descriptor !== undefined) {
|
|
1117
|
+
try {
|
|
1118
|
+
closeSync(descriptor);
|
|
1119
|
+
}
|
|
1120
|
+
catch (cleanupError) {
|
|
1121
|
+
cleanupErrors.push(cleanupError);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
try {
|
|
1125
|
+
unlinkSync(temporaryPath);
|
|
1126
|
+
}
|
|
1127
|
+
catch (cleanupError) {
|
|
1128
|
+
if (!isFsError(cleanupError, "ENOENT"))
|
|
1129
|
+
cleanupErrors.push(cleanupError);
|
|
1130
|
+
}
|
|
1131
|
+
if (cleanupErrors.length > 0) {
|
|
1132
|
+
throw new AggregateError([error, ...cleanupErrors], `Failed to prepare workspace file: ${file.relPath}`);
|
|
1133
|
+
}
|
|
1134
|
+
throw error;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function workspacePathHasIdentity(pathValue, identity) {
|
|
1138
|
+
try {
|
|
1139
|
+
const stats = lstatSync(pathValue);
|
|
1140
|
+
return stats.dev === identity.device && stats.ino === identity.inode;
|
|
1141
|
+
}
|
|
1142
|
+
catch (error) {
|
|
1143
|
+
if (isFsError(error, "ENOENT"))
|
|
1144
|
+
return false;
|
|
1145
|
+
throw error;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1010
1148
|
function resolveWorkspaceFilePath(workspaceRoot, relativePath) {
|
|
1011
1149
|
const targetPath = resolve(workspaceRoot, ...relativePath.split("/"));
|
|
1012
1150
|
const targetRelativePath = relative(workspaceRoot, targetPath);
|
|
@@ -1046,40 +1184,6 @@ function assertNoSymlinkPathComponents(workspaceRoot, targetPath) {
|
|
|
1046
1184
|
}
|
|
1047
1185
|
}
|
|
1048
1186
|
}
|
|
1049
|
-
function writeWorkspaceFile(workspaceRoot, file) {
|
|
1050
|
-
assertNoSymlinkPathComponents(workspaceRoot, file.absPath);
|
|
1051
|
-
const temporaryPath = join(dirname(file.absPath), `.agent-profile-${randomUUID()}.tmp`);
|
|
1052
|
-
let descriptor;
|
|
1053
|
-
try {
|
|
1054
|
-
descriptor = openSync(temporaryPath, SAFE_WRITE_FLAGS, 0o600);
|
|
1055
|
-
writeFileSync(descriptor, file.content, "utf8");
|
|
1056
|
-
fchmodSync(descriptor, file.mode);
|
|
1057
|
-
closeSync(descriptor);
|
|
1058
|
-
descriptor = undefined;
|
|
1059
|
-
assertNoSymlinkPathComponents(workspaceRoot, file.absPath);
|
|
1060
|
-
try {
|
|
1061
|
-
linkSync(temporaryPath, file.absPath);
|
|
1062
|
-
}
|
|
1063
|
-
catch (error) {
|
|
1064
|
-
if (!isFsError(error, "EEXIST") || !workspaceFileMatches(file)) {
|
|
1065
|
-
throw error;
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
unlinkSync(temporaryPath);
|
|
1069
|
-
}
|
|
1070
|
-
catch (error) {
|
|
1071
|
-
if (descriptor !== undefined)
|
|
1072
|
-
closeSync(descriptor);
|
|
1073
|
-
try {
|
|
1074
|
-
unlinkSync(temporaryPath);
|
|
1075
|
-
}
|
|
1076
|
-
catch (cleanupError) {
|
|
1077
|
-
if (!isFsError(cleanupError, "ENOENT"))
|
|
1078
|
-
throw cleanupError;
|
|
1079
|
-
}
|
|
1080
|
-
throw error;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
1187
|
function isFsError(error, code) {
|
|
1084
1188
|
return (typeof error === "object" &&
|
|
1085
1189
|
error !== null &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-profile-materialize",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
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
|
"main": "./dist/index.js",
|