@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 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
- /** cwd-relative native skill dir, or null if the harness has no cwd skill primitive. */
63
- const SKILL_DIR = {
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) && !workspaceFileMatches(file)) {
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 written = [];
976
- for (const file of files) {
977
- if (!workspaceFileExists(file.absPath)) {
978
- writeWorkspaceFile(workspaceRoot, file);
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
- return {
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
- const OPENCODE_TOOL_NAMES = new Set([
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
- "list",
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
- function openCodeToolName(name) {
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
- return OPENCODE_TOOL_NAMES.has(normalized) ? normalized : undefined;
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" && OPENCODE_TOOL_NAMES.has(normalized)
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.5.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.30.0"
19
+ "@tangle-network/agent-interface": "0.31.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "25.6.0",