@tangle-network/agent-profile-materialize 0.5.0 → 0.6.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/index.d.ts +57 -0
- package/dist/index.js +229 -56
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -60,12 +60,29 @@ 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;
|
|
77
|
+
/**
|
|
78
|
+
* Enforce plan supportability under the profile's `resources.failOnError`
|
|
79
|
+
* policy. Strict by default: `failOnError` absent or `true` throws on any
|
|
80
|
+
* unsupported dimension. An explicitly best-effort profile
|
|
81
|
+
* (`failOnError: false`) materializes the supported subset instead; the
|
|
82
|
+
* returned `"dimension: reason"` entries are what the caller must surface
|
|
83
|
+
* as a warning so the skipped dimensions stay visible to operators.
|
|
84
|
+
*/
|
|
85
|
+
export declare function enforceWorkspacePlanSupport(plan: Pick<WorkspacePlan, "unsupported">, failOnError: boolean | undefined): string[];
|
|
69
86
|
/**
|
|
70
87
|
* Candidate-safe workspace plan. Spawn values remain typed until the runtime
|
|
71
88
|
* resolves them; the materializer never joins argv into a shell command.
|
|
@@ -91,6 +108,42 @@ export interface MaterializeCandidateProfileOptions {
|
|
|
91
108
|
/** Exact text fetched from pinned resource refs, keyed by declared SHA-256. */
|
|
92
109
|
resolvedResources?: ReadonlyMap<Sha256Digest, string>;
|
|
93
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Whether the harness LOADS its {@link CONTEXT_FILE} from cwd on its own.
|
|
113
|
+
*
|
|
114
|
+
* True for every harness whose CLI discovers the file by name (CLAUDE.md,
|
|
115
|
+
* AGENTS.md, …). opencode is the exception: it reads only what its
|
|
116
|
+
* `opencode.json` `instructions[]` names, so a caller writing context for
|
|
117
|
+
* opencode must register the path as well as write it — see the `instructions`
|
|
118
|
+
* lowering below, and the interactive TUI's persona fold.
|
|
119
|
+
*/
|
|
120
|
+
export declare function harnessAutoLoadsContextFile(harness: HarnessId): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* The project context file this harness reads from cwd — its stable, documented
|
|
123
|
+
* ALWAYS-LOADED surface. Exported so a caller lowering additive context (e.g. a
|
|
124
|
+
* bare TUI's persona, which has no per-message system channel) targets the same
|
|
125
|
+
* file the materializer writes instructions to, instead of keeping its own copy
|
|
126
|
+
* of this per-harness map.
|
|
127
|
+
*/
|
|
128
|
+
export declare function harnessContextFile(harness: HarnessId): string;
|
|
129
|
+
/**
|
|
130
|
+
* cwd-relative native skill dir per harness, or null if the harness has no
|
|
131
|
+
* cwd skill primitive (skills live in a user/global dir the workspace plan
|
|
132
|
+
* cannot reach). Public so consumers composing a profile can (a) render the
|
|
133
|
+
* exact path the platform will mount a skill at into their prompts instead
|
|
134
|
+
* of hardcoding `~/.claude/skills/...`, and (b) branch on skill support
|
|
135
|
+
* ahead of materialization instead of catching the materialize-time throw.
|
|
136
|
+
*/
|
|
137
|
+
export declare const HARNESS_SKILL_DIRS: Readonly<Record<HarnessId, string | null>>;
|
|
138
|
+
/**
|
|
139
|
+
* Resolve the cwd-relative directory `resources.skills` refs materialize
|
|
140
|
+
* into on `harness` (after alias canonicalization: claude/claudish →
|
|
141
|
+
* claude-code, kimi → kimi-code). `null` means the harness cannot receive
|
|
142
|
+
* cwd skills — the same condition that surfaces as an `unsupported`
|
|
143
|
+
* "skills" entry (and an {@link assertWorkspacePlanSupported} throw) at
|
|
144
|
+
* materialize time, exposed here as branchable data.
|
|
145
|
+
*/
|
|
146
|
+
export declare function skillDirForHarness(harness: HarnessId): string | null;
|
|
94
147
|
export interface ResolveAgentProfileResourcesOptions {
|
|
95
148
|
/** Repository used when a GitHub resource omits its own `repository`. */
|
|
96
149
|
repository?: string;
|
|
@@ -146,6 +199,10 @@ export declare function applyAgentCandidateWorkspacePlan(plan: AgentCandidateWor
|
|
|
146
199
|
* agent-dev-container box, and VB. Returns `unsupported` so the caller can log/fail.
|
|
147
200
|
*/
|
|
148
201
|
export declare function applyWorkspacePlan(plan: WorkspacePlan, workspaceDir: string, options?: ApplyWorkspacePlanOptions): WorkspacePlanReceipt;
|
|
202
|
+
/** Tool IDs reported by the OpenCode binary shipped in the Sandbox image. */
|
|
203
|
+
export declare const OPENCODE_NATIVE_TOOL_IDS: readonly ["apply_patch", "bash", "codesearch", "edit", "glob", "grep", "question", "read", "skill", "task", "todowrite", "webfetch", "websearch", "write"];
|
|
204
|
+
/** Resolve portable and legacy profile names to current OpenCode tool IDs. */
|
|
205
|
+
export declare function openCodeNativeToolId(name: string): string | undefined;
|
|
149
206
|
/** OpenCode-native MCP configuration used by workspace and startup paths. */
|
|
150
207
|
export type OpenCodeMcpConfig = Record<string, {
|
|
151
208
|
type: "remote";
|
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,13 +37,69 @@ 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) {
|
|
43
54
|
throw new Error(formatUnsupported(plan.unsupported));
|
|
44
55
|
}
|
|
45
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Enforce plan supportability under the profile's `resources.failOnError`
|
|
59
|
+
* policy. Strict by default: `failOnError` absent or `true` throws on any
|
|
60
|
+
* unsupported dimension. An explicitly best-effort profile
|
|
61
|
+
* (`failOnError: false`) materializes the supported subset instead; the
|
|
62
|
+
* returned `"dimension: reason"` entries are what the caller must surface
|
|
63
|
+
* as a warning so the skipped dimensions stay visible to operators.
|
|
64
|
+
*/
|
|
65
|
+
export function enforceWorkspacePlanSupport(plan, failOnError) {
|
|
66
|
+
if (failOnError === false) {
|
|
67
|
+
return plan.unsupported.map(({ dimension, reason }) => `${dimension}: ${reason}`);
|
|
68
|
+
}
|
|
69
|
+
assertWorkspacePlanSupported(plan);
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
46
72
|
// ─── Verified per-harness conventions (capability matrix) ───────────────────
|
|
73
|
+
/**
|
|
74
|
+
* Whether the harness LOADS its {@link CONTEXT_FILE} from cwd on its own.
|
|
75
|
+
*
|
|
76
|
+
* True for every harness whose CLI discovers the file by name (CLAUDE.md,
|
|
77
|
+
* AGENTS.md, …). opencode is the exception: it reads only what its
|
|
78
|
+
* `opencode.json` `instructions[]` names, so a caller writing context for
|
|
79
|
+
* opencode must register the path as well as write it — see the `instructions`
|
|
80
|
+
* lowering below, and the interactive TUI's persona fold.
|
|
81
|
+
*/
|
|
82
|
+
export function harnessAutoLoadsContextFile(harness) {
|
|
83
|
+
return canonicalHarness(harness) !== "opencode";
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The project context file this harness reads from cwd — its stable, documented
|
|
87
|
+
* ALWAYS-LOADED surface. Exported so a caller lowering additive context (e.g. a
|
|
88
|
+
* bare TUI's persona, which has no per-message system channel) targets the same
|
|
89
|
+
* file the materializer writes instructions to, instead of keeping its own copy
|
|
90
|
+
* of this per-harness map.
|
|
91
|
+
*/
|
|
92
|
+
export function harnessContextFile(harness) {
|
|
93
|
+
// The map is keyed by HarnessId, but callers reach this from runtime strings
|
|
94
|
+
// (a registry spec name, a request field). A miss would otherwise return
|
|
95
|
+
// undefined behind a `string` return type and surface as an undefined write
|
|
96
|
+
// path much later.
|
|
97
|
+
const path = CONTEXT_FILE[canonicalHarness(harness)];
|
|
98
|
+
if (!path) {
|
|
99
|
+
throw new Error(`No project context file is known for harness "${harness}"`);
|
|
100
|
+
}
|
|
101
|
+
return path;
|
|
102
|
+
}
|
|
47
103
|
/** Project context file each harness actually reads from cwd. NEVER a constant. */
|
|
48
104
|
const CONTEXT_FILE = {
|
|
49
105
|
"claude-code": "CLAUDE.md",
|
|
@@ -59,8 +115,15 @@ const CONTEXT_FILE = {
|
|
|
59
115
|
hermes: "HERMES.md", // highest-priority native name; only ONE context loads (first match)
|
|
60
116
|
openclaw: "AGENTS.md",
|
|
61
117
|
};
|
|
62
|
-
/**
|
|
63
|
-
|
|
118
|
+
/**
|
|
119
|
+
* cwd-relative native skill dir per harness, or null if the harness has no
|
|
120
|
+
* cwd skill primitive (skills live in a user/global dir the workspace plan
|
|
121
|
+
* cannot reach). Public so consumers composing a profile can (a) render the
|
|
122
|
+
* exact path the platform will mount a skill at into their prompts instead
|
|
123
|
+
* of hardcoding `~/.claude/skills/...`, and (b) branch on skill support
|
|
124
|
+
* ahead of materialization instead of catching the materialize-time throw.
|
|
125
|
+
*/
|
|
126
|
+
export const HARNESS_SKILL_DIRS = {
|
|
64
127
|
"claude-code": ".claude/skills",
|
|
65
128
|
claude: ".claude/skills",
|
|
66
129
|
claudish: ".claude/skills",
|
|
@@ -74,6 +137,18 @@ const SKILL_DIR = {
|
|
|
74
137
|
hermes: null, // skills are ~/.hermes/skills (user) only — no cwd discovery
|
|
75
138
|
openclaw: "skills", // <workspace>/skills/<name>/SKILL.md
|
|
76
139
|
};
|
|
140
|
+
const SKILL_DIR = HARNESS_SKILL_DIRS;
|
|
141
|
+
/**
|
|
142
|
+
* Resolve the cwd-relative directory `resources.skills` refs materialize
|
|
143
|
+
* into on `harness` (after alias canonicalization: claude/claudish →
|
|
144
|
+
* claude-code, kimi → kimi-code). `null` means the harness cannot receive
|
|
145
|
+
* cwd skills — the same condition that surfaces as an `unsupported`
|
|
146
|
+
* "skills" entry (and an {@link assertWorkspacePlanSupported} throw) at
|
|
147
|
+
* materialize time, exposed here as branchable data.
|
|
148
|
+
*/
|
|
149
|
+
export function skillDirForHarness(harness) {
|
|
150
|
+
return HARNESS_SKILL_DIRS[canonicalHarness(harness)];
|
|
151
|
+
}
|
|
77
152
|
function canonicalHarness(h) {
|
|
78
153
|
if (h === "claude" || h === "claudish")
|
|
79
154
|
return "claude-code";
|
|
@@ -952,6 +1027,10 @@ export function applyAgentCandidateWorkspacePlan(plan, workspaceDir, targetWorks
|
|
|
952
1027
|
export function applyWorkspacePlan(plan, workspaceDir, options = {}) {
|
|
953
1028
|
const workspaceRoot = resolve(workspaceDir);
|
|
954
1029
|
const runtimeOnlyFiles = new Set((options.runtimeOnlyFiles ?? []).map(normalizeWorkspaceRelativePath));
|
|
1030
|
+
const existingFiles = options.existingFiles ?? "allow-identical";
|
|
1031
|
+
if (existingFiles !== "allow-identical" && existingFiles !== "reject") {
|
|
1032
|
+
throw new Error(`Unsupported existing workspace file policy: ${existingFiles}`);
|
|
1033
|
+
}
|
|
955
1034
|
const files = normalizePlanFiles(plan.files)
|
|
956
1035
|
.filter((file) => !runtimeOnlyFiles.has(file.relPath))
|
|
957
1036
|
.map((file) => ({
|
|
@@ -962,24 +1041,77 @@ export function applyWorkspacePlan(plan, workspaceDir, options = {}) {
|
|
|
962
1041
|
// existing symlink cannot leave a partially materialized workspace.
|
|
963
1042
|
for (const file of files) {
|
|
964
1043
|
assertNoSymlinkPathComponents(workspaceRoot, file.absPath);
|
|
965
|
-
if (workspaceFileExists(file.absPath) &&
|
|
1044
|
+
if (workspaceFileExists(file.absPath) &&
|
|
1045
|
+
(existingFiles === "reject" || !workspaceFileMatches(file))) {
|
|
966
1046
|
throw new Error(`Refusing to replace existing workspace file: ${file.relPath}`);
|
|
967
1047
|
}
|
|
968
1048
|
}
|
|
1049
|
+
// Parent directories remain on failure because they may contain concurrent
|
|
1050
|
+
// workspace state that this call does not own.
|
|
969
1051
|
for (const file of files) {
|
|
970
1052
|
mkdirSync(dirname(file.absPath), { recursive: true });
|
|
971
1053
|
}
|
|
972
1054
|
for (const file of files) {
|
|
973
1055
|
assertNoSymlinkPathComponents(workspaceRoot, file.absPath);
|
|
974
1056
|
}
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1057
|
+
const prepared = [];
|
|
1058
|
+
// Prepared temp links stay alive through publication and rollback. Their
|
|
1059
|
+
// inodes distinguish files created here from concurrent replacements.
|
|
1060
|
+
try {
|
|
1061
|
+
for (const file of files) {
|
|
1062
|
+
if (existingFiles === "allow-identical" &&
|
|
1063
|
+
workspaceFileExists(file.absPath) &&
|
|
1064
|
+
workspaceFileMatches(file)) {
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
prepared.push(prepareWorkspaceFile(workspaceRoot, file));
|
|
1068
|
+
}
|
|
1069
|
+
for (const entry of prepared) {
|
|
1070
|
+
assertNoSymlinkPathComponents(workspaceRoot, entry.file.absPath);
|
|
1071
|
+
try {
|
|
1072
|
+
linkSync(entry.temporaryPath, entry.file.absPath);
|
|
1073
|
+
entry.published = true;
|
|
1074
|
+
}
|
|
1075
|
+
catch (error) {
|
|
1076
|
+
if (isFsError(error, "EEXIST") &&
|
|
1077
|
+
existingFiles === "allow-identical" &&
|
|
1078
|
+
workspaceFileMatches(entry.file)) {
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
if (isFsError(error, "EEXIST")) {
|
|
1082
|
+
throw new Error(`Refusing to replace existing workspace file: ${entry.file.relPath}`, { cause: error });
|
|
1083
|
+
}
|
|
1084
|
+
throw error;
|
|
1085
|
+
}
|
|
979
1086
|
}
|
|
980
|
-
written.push(file.relPath);
|
|
981
1087
|
}
|
|
982
|
-
|
|
1088
|
+
catch (error) {
|
|
1089
|
+
const cleanupErrors = [];
|
|
1090
|
+
for (const entry of [...prepared].reverse()) {
|
|
1091
|
+
try {
|
|
1092
|
+
if (entry.published &&
|
|
1093
|
+
workspacePathHasIdentity(entry.file.absPath, entry.identity)) {
|
|
1094
|
+
unlinkSync(entry.file.absPath);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
catch (cleanupError) {
|
|
1098
|
+
cleanupErrors.push(cleanupError);
|
|
1099
|
+
}
|
|
1100
|
+
try {
|
|
1101
|
+
if (workspacePathHasIdentity(entry.temporaryPath, entry.identity)) {
|
|
1102
|
+
unlinkSync(entry.temporaryPath);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
catch (cleanupError) {
|
|
1106
|
+
cleanupErrors.push(cleanupError);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (cleanupErrors.length > 0) {
|
|
1110
|
+
throw new AggregateError([error, ...cleanupErrors], "Failed to apply workspace plan and roll back published files");
|
|
1111
|
+
}
|
|
1112
|
+
throw error;
|
|
1113
|
+
}
|
|
1114
|
+
const receipt = {
|
|
983
1115
|
workspacePlanDigest: hashWorkspacePlan(plan),
|
|
984
1116
|
env: plan.env,
|
|
985
1117
|
flags: plan.flags,
|
|
@@ -987,8 +1119,25 @@ export function applyWorkspacePlan(plan, workspaceDir, options = {}) {
|
|
|
987
1119
|
? {}
|
|
988
1120
|
: { systemPrompt: plan.systemPrompt }),
|
|
989
1121
|
unsupported: plan.unsupported,
|
|
990
|
-
written,
|
|
1122
|
+
written: files.map((file) => file.relPath),
|
|
991
1123
|
};
|
|
1124
|
+
const cleanupErrors = [];
|
|
1125
|
+
const temporaryPaths = [];
|
|
1126
|
+
for (const entry of prepared) {
|
|
1127
|
+
try {
|
|
1128
|
+
unlinkSync(entry.temporaryPath);
|
|
1129
|
+
}
|
|
1130
|
+
catch (error) {
|
|
1131
|
+
if (!isFsError(error, "ENOENT")) {
|
|
1132
|
+
cleanupErrors.push(error);
|
|
1133
|
+
temporaryPaths.push(entry.temporaryPath);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
if (cleanupErrors.length > 0) {
|
|
1138
|
+
throw new WorkspacePlanCleanupError(cleanupErrors, receipt, temporaryPaths);
|
|
1139
|
+
}
|
|
1140
|
+
return receipt;
|
|
992
1141
|
}
|
|
993
1142
|
function workspaceFileExists(pathValue) {
|
|
994
1143
|
try {
|
|
@@ -1007,6 +1156,59 @@ function workspaceFileMatches(file) {
|
|
|
1007
1156
|
(stats.mode & 0o777) === file.mode &&
|
|
1008
1157
|
readFileSync(file.absPath, "utf8") === file.content);
|
|
1009
1158
|
}
|
|
1159
|
+
function prepareWorkspaceFile(workspaceRoot, file) {
|
|
1160
|
+
assertNoSymlinkPathComponents(workspaceRoot, file.absPath);
|
|
1161
|
+
const temporaryPath = join(dirname(file.absPath), `.agent-profile-${randomUUID()}.tmp`);
|
|
1162
|
+
let descriptor;
|
|
1163
|
+
try {
|
|
1164
|
+
descriptor = openSync(temporaryPath, SAFE_WRITE_FLAGS, 0o600);
|
|
1165
|
+
writeFileSync(descriptor, file.content, "utf8");
|
|
1166
|
+
fchmodSync(descriptor, file.mode);
|
|
1167
|
+
const stats = fstatSync(descriptor);
|
|
1168
|
+
const descriptorToClose = descriptor;
|
|
1169
|
+
descriptor = undefined;
|
|
1170
|
+
closeSync(descriptorToClose);
|
|
1171
|
+
return {
|
|
1172
|
+
file,
|
|
1173
|
+
temporaryPath,
|
|
1174
|
+
identity: { device: stats.dev, inode: stats.ino },
|
|
1175
|
+
published: false,
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
catch (error) {
|
|
1179
|
+
const cleanupErrors = [];
|
|
1180
|
+
if (descriptor !== undefined) {
|
|
1181
|
+
try {
|
|
1182
|
+
closeSync(descriptor);
|
|
1183
|
+
}
|
|
1184
|
+
catch (cleanupError) {
|
|
1185
|
+
cleanupErrors.push(cleanupError);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
try {
|
|
1189
|
+
unlinkSync(temporaryPath);
|
|
1190
|
+
}
|
|
1191
|
+
catch (cleanupError) {
|
|
1192
|
+
if (!isFsError(cleanupError, "ENOENT"))
|
|
1193
|
+
cleanupErrors.push(cleanupError);
|
|
1194
|
+
}
|
|
1195
|
+
if (cleanupErrors.length > 0) {
|
|
1196
|
+
throw new AggregateError([error, ...cleanupErrors], `Failed to prepare workspace file: ${file.relPath}`);
|
|
1197
|
+
}
|
|
1198
|
+
throw error;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
function workspacePathHasIdentity(pathValue, identity) {
|
|
1202
|
+
try {
|
|
1203
|
+
const stats = lstatSync(pathValue);
|
|
1204
|
+
return stats.dev === identity.device && stats.ino === identity.inode;
|
|
1205
|
+
}
|
|
1206
|
+
catch (error) {
|
|
1207
|
+
if (isFsError(error, "ENOENT"))
|
|
1208
|
+
return false;
|
|
1209
|
+
throw error;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1010
1212
|
function resolveWorkspaceFilePath(workspaceRoot, relativePath) {
|
|
1011
1213
|
const targetPath = resolve(workspaceRoot, ...relativePath.split("/"));
|
|
1012
1214
|
const targetRelativePath = relative(workspaceRoot, targetPath);
|
|
@@ -1046,40 +1248,6 @@ function assertNoSymlinkPathComponents(workspaceRoot, targetPath) {
|
|
|
1046
1248
|
}
|
|
1047
1249
|
}
|
|
1048
1250
|
}
|
|
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
1251
|
function isFsError(error, code) {
|
|
1084
1252
|
return (typeof error === "object" &&
|
|
1085
1253
|
error !== null &&
|
|
@@ -1206,33 +1374,38 @@ function claudeToolName(name) {
|
|
|
1206
1374
|
return "WebFetch";
|
|
1207
1375
|
return CLAUDE_TOOL_NAMES[normalizedControlName(name)];
|
|
1208
1376
|
}
|
|
1209
|
-
|
|
1377
|
+
/** Tool IDs reported by the OpenCode binary shipped in the Sandbox image. */
|
|
1378
|
+
export const OPENCODE_NATIVE_TOOL_IDS = [
|
|
1379
|
+
"apply_patch",
|
|
1210
1380
|
"bash",
|
|
1211
|
-
"batch",
|
|
1212
1381
|
"codesearch",
|
|
1213
1382
|
"edit",
|
|
1214
1383
|
"glob",
|
|
1215
1384
|
"grep",
|
|
1216
|
-
"
|
|
1217
|
-
"lsp",
|
|
1218
|
-
"multiedit",
|
|
1219
|
-
"patch",
|
|
1385
|
+
"question",
|
|
1220
1386
|
"read",
|
|
1387
|
+
"skill",
|
|
1221
1388
|
"task",
|
|
1222
|
-
"todoread",
|
|
1223
1389
|
"todowrite",
|
|
1224
1390
|
"webfetch",
|
|
1225
1391
|
"websearch",
|
|
1226
1392
|
"write",
|
|
1227
|
-
]
|
|
1228
|
-
|
|
1393
|
+
];
|
|
1394
|
+
const OPENCODE_NATIVE_TOOL_ID_SET = new Set(OPENCODE_NATIVE_TOOL_IDS);
|
|
1395
|
+
/** Resolve portable and legacy profile names to current OpenCode tool IDs. */
|
|
1396
|
+
export function openCodeNativeToolId(name) {
|
|
1229
1397
|
const web = nativeWebToolKind(name);
|
|
1230
1398
|
if (web === "search")
|
|
1231
1399
|
return "websearch";
|
|
1232
1400
|
if (web === "fetch")
|
|
1233
1401
|
return "webfetch";
|
|
1234
1402
|
const normalized = normalizedControlName(name);
|
|
1235
|
-
|
|
1403
|
+
if (normalized === "patch")
|
|
1404
|
+
return "apply_patch";
|
|
1405
|
+
return OPENCODE_NATIVE_TOOL_ID_SET.has(normalized) ? normalized : undefined;
|
|
1406
|
+
}
|
|
1407
|
+
function openCodeToolName(name) {
|
|
1408
|
+
return openCodeNativeToolId(name);
|
|
1236
1409
|
}
|
|
1237
1410
|
function openCodePermissionName(name) {
|
|
1238
1411
|
const normalized = normalizedControlName(name);
|
|
@@ -1246,7 +1419,7 @@ function openCodePermissionName(name) {
|
|
|
1246
1419
|
if (["bash", "mcp", "doom_loop", "external_directory"].includes(normalized)) {
|
|
1247
1420
|
return normalized;
|
|
1248
1421
|
}
|
|
1249
|
-
return normalized !== "read" &&
|
|
1422
|
+
return normalized !== "read" && OPENCODE_NATIVE_TOOL_ID_SET.has(normalized)
|
|
1250
1423
|
? normalized
|
|
1251
1424
|
: undefined;
|
|
1252
1425
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-profile-materialize",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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
|
"main": "./dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@tangle-network/agent-interface": "0.
|
|
19
|
+
"@tangle-network/agent-interface": "0.31.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "25.6.0",
|