@tangle-network/agent-profile-materialize 0.5.1 → 0.7.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 +69 -0
- package/dist/index.js +116 -21
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,26 @@ export interface PlanFile {
|
|
|
28
28
|
content: string;
|
|
29
29
|
/** chmod mode (e.g. 0o755 for hook scripts); default 0o644. */
|
|
30
30
|
mode?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Provenance of this entry. `"generated"` = the materializer synthesized the
|
|
33
|
+
* content itself (native harness config, merged settings, lowered prompt/MCP/
|
|
34
|
+
* hooks/subagents). `"user"` = the exact bytes of a profile-declared resource
|
|
35
|
+
* ref (`resources.files`/`tools`/`agents`/`skills`/`commands`), passed through
|
|
36
|
+
* verbatim at the path the profile author chose.
|
|
37
|
+
*
|
|
38
|
+
* A consumer that needs to reinterpret a SPECIFIC generated file (e.g. parse
|
|
39
|
+
* `opencode.json` as the synthetic native config) MUST gate on
|
|
40
|
+
* `source === "generated"` at that relPath — matching by relPath alone cannot
|
|
41
|
+
* tell a harness-authored config apart from a user resource mount that
|
|
42
|
+
* happens to share the same conventional path, and treating arbitrary user
|
|
43
|
+
* content as trusted generated config is a fail-open bug, not a fallback.
|
|
44
|
+
*
|
|
45
|
+
* Optional: callers that hand-construct a {@link WorkspacePlan} directly for
|
|
46
|
+
* {@link applyWorkspacePlan} (tests, ad hoc writes) are not materializer
|
|
47
|
+
* output and have no provenance to report. `materializeProfile` always sets
|
|
48
|
+
* this on every file it emits.
|
|
49
|
+
*/
|
|
50
|
+
source?: "user" | "generated";
|
|
31
51
|
}
|
|
32
52
|
export interface Unsupported {
|
|
33
53
|
dimension: "files" | "skills" | "mcp" | "hooks" | "subagents" | "commands" | "instructions" | "systemPrompt" | "connections" | "confidential" | "permissions" | "tools" | "modes" | "extensions";
|
|
@@ -74,6 +94,15 @@ export interface ApplyWorkspacePlanOptions {
|
|
|
74
94
|
}
|
|
75
95
|
/** Fail when a profile asks for controls the chosen harness cannot execute. */
|
|
76
96
|
export declare function assertWorkspacePlanSupported(plan: Pick<WorkspacePlan, "unsupported">): void;
|
|
97
|
+
/**
|
|
98
|
+
* Enforce plan supportability under the profile's `resources.failOnError`
|
|
99
|
+
* policy. Strict by default: `failOnError` absent or `true` throws on any
|
|
100
|
+
* unsupported dimension. An explicitly best-effort profile
|
|
101
|
+
* (`failOnError: false`) materializes the supported subset instead; the
|
|
102
|
+
* returned `"dimension: reason"` entries are what the caller must surface
|
|
103
|
+
* as a warning so the skipped dimensions stay visible to operators.
|
|
104
|
+
*/
|
|
105
|
+
export declare function enforceWorkspacePlanSupport(plan: Pick<WorkspacePlan, "unsupported">, failOnError: boolean | undefined): string[];
|
|
77
106
|
/**
|
|
78
107
|
* Candidate-safe workspace plan. Spawn values remain typed until the runtime
|
|
79
108
|
* resolves them; the materializer never joins argv into a shell command.
|
|
@@ -99,6 +128,42 @@ export interface MaterializeCandidateProfileOptions {
|
|
|
99
128
|
/** Exact text fetched from pinned resource refs, keyed by declared SHA-256. */
|
|
100
129
|
resolvedResources?: ReadonlyMap<Sha256Digest, string>;
|
|
101
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Whether the harness LOADS its {@link CONTEXT_FILE} from cwd on its own.
|
|
133
|
+
*
|
|
134
|
+
* True for every harness whose CLI discovers the file by name (CLAUDE.md,
|
|
135
|
+
* AGENTS.md, …). opencode is the exception: it reads only what its
|
|
136
|
+
* `opencode.json` `instructions[]` names, so a caller writing context for
|
|
137
|
+
* opencode must register the path as well as write it — see the `instructions`
|
|
138
|
+
* lowering below, and the interactive TUI's persona fold.
|
|
139
|
+
*/
|
|
140
|
+
export declare function harnessAutoLoadsContextFile(harness: HarnessId): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* The project context file this harness reads from cwd — its stable, documented
|
|
143
|
+
* ALWAYS-LOADED surface. Exported so a caller lowering additive context (e.g. a
|
|
144
|
+
* bare TUI's persona, which has no per-message system channel) targets the same
|
|
145
|
+
* file the materializer writes instructions to, instead of keeping its own copy
|
|
146
|
+
* of this per-harness map.
|
|
147
|
+
*/
|
|
148
|
+
export declare function harnessContextFile(harness: HarnessId): string;
|
|
149
|
+
/**
|
|
150
|
+
* cwd-relative native skill dir per harness, or null if the harness has no
|
|
151
|
+
* cwd skill primitive (skills live in a user/global dir the workspace plan
|
|
152
|
+
* cannot reach). Public so consumers composing a profile can (a) render the
|
|
153
|
+
* exact path the platform will mount a skill at into their prompts instead
|
|
154
|
+
* of hardcoding `~/.claude/skills/...`, and (b) branch on skill support
|
|
155
|
+
* ahead of materialization instead of catching the materialize-time throw.
|
|
156
|
+
*/
|
|
157
|
+
export declare const HARNESS_SKILL_DIRS: Readonly<Record<HarnessId, string | null>>;
|
|
158
|
+
/**
|
|
159
|
+
* Resolve the cwd-relative directory `resources.skills` refs materialize
|
|
160
|
+
* into on `harness` (after alias canonicalization: claude/claudish →
|
|
161
|
+
* claude-code, kimi → kimi-code). `null` means the harness cannot receive
|
|
162
|
+
* cwd skills — the same condition that surfaces as an `unsupported`
|
|
163
|
+
* "skills" entry (and an {@link assertWorkspacePlanSupported} throw) at
|
|
164
|
+
* materialize time, exposed here as branchable data.
|
|
165
|
+
*/
|
|
166
|
+
export declare function skillDirForHarness(harness: HarnessId): string | null;
|
|
102
167
|
export interface ResolveAgentProfileResourcesOptions {
|
|
103
168
|
/** Repository used when a GitHub resource omits its own `repository`. */
|
|
104
169
|
repository?: string;
|
|
@@ -154,6 +219,10 @@ export declare function applyAgentCandidateWorkspacePlan(plan: AgentCandidateWor
|
|
|
154
219
|
* agent-dev-container box, and VB. Returns `unsupported` so the caller can log/fail.
|
|
155
220
|
*/
|
|
156
221
|
export declare function applyWorkspacePlan(plan: WorkspacePlan, workspaceDir: string, options?: ApplyWorkspacePlanOptions): WorkspacePlanReceipt;
|
|
222
|
+
/** Tool IDs reported by the OpenCode binary shipped in the Sandbox image. */
|
|
223
|
+
export declare const OPENCODE_NATIVE_TOOL_IDS: readonly ["apply_patch", "bash", "codesearch", "edit", "glob", "grep", "question", "read", "skill", "task", "todowrite", "webfetch", "websearch", "write"];
|
|
224
|
+
/** Resolve portable and legacy profile names to current OpenCode tool IDs. */
|
|
225
|
+
export declare function openCodeNativeToolId(name: string): string | undefined;
|
|
157
226
|
/** OpenCode-native MCP configuration used by workspace and startup paths. */
|
|
158
227
|
export type OpenCodeMcpConfig = Record<string, {
|
|
159
228
|
type: "remote";
|
package/dist/index.js
CHANGED
|
@@ -25,15 +25,32 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
25
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
|
+
function describeUnresolvedRef(r) {
|
|
29
|
+
const name = typeof r.name === "string" ? r.name : undefined;
|
|
30
|
+
const path = typeof r.path === "string" ? r.path : undefined;
|
|
31
|
+
if (name && path)
|
|
32
|
+
return `"${name}" (${path})`;
|
|
33
|
+
if (name)
|
|
34
|
+
return `"${name}"`;
|
|
35
|
+
if (path)
|
|
36
|
+
return `"${path}"`;
|
|
37
|
+
return "resource";
|
|
38
|
+
}
|
|
28
39
|
/** Resolve inline bytes already present on the profile. Remote resources must
|
|
29
|
-
* be resolved by {@link resolveAgentProfileResources} before materialization.
|
|
40
|
+
* be resolved by {@link resolveAgentProfileResources} before materialization.
|
|
41
|
+
*
|
|
42
|
+
* FAIL-CLOSED: a ref that declares `kind: "inline"` is asserting its bytes are
|
|
43
|
+
* already resolved, so missing/non-string `content` is a caller bug, not an
|
|
44
|
+
* empty file — silently coercing it (`String(content ?? "")`) would produce a
|
|
45
|
+
* file that "looks provisioned, loads nothing", exactly the trap this module's
|
|
46
|
+
* docstring warns against. */
|
|
30
47
|
function resolveInlineRef(ref) {
|
|
31
48
|
const r = ref;
|
|
32
|
-
if (
|
|
33
|
-
return {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
};
|
|
49
|
+
if (typeof r.content === "string") {
|
|
50
|
+
return { name: String(r.name ?? r.path ?? "resource"), content: r.content };
|
|
51
|
+
}
|
|
52
|
+
if (r.kind === "inline") {
|
|
53
|
+
throw new Error(`Profile resource ${describeUnresolvedRef(r)} declares kind "inline" but has no string content`);
|
|
37
54
|
}
|
|
38
55
|
return null; // github / unresolved
|
|
39
56
|
}
|
|
@@ -54,7 +71,52 @@ export function assertWorkspacePlanSupported(plan) {
|
|
|
54
71
|
throw new Error(formatUnsupported(plan.unsupported));
|
|
55
72
|
}
|
|
56
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Enforce plan supportability under the profile's `resources.failOnError`
|
|
76
|
+
* policy. Strict by default: `failOnError` absent or `true` throws on any
|
|
77
|
+
* unsupported dimension. An explicitly best-effort profile
|
|
78
|
+
* (`failOnError: false`) materializes the supported subset instead; the
|
|
79
|
+
* returned `"dimension: reason"` entries are what the caller must surface
|
|
80
|
+
* as a warning so the skipped dimensions stay visible to operators.
|
|
81
|
+
*/
|
|
82
|
+
export function enforceWorkspacePlanSupport(plan, failOnError) {
|
|
83
|
+
if (failOnError === false) {
|
|
84
|
+
return plan.unsupported.map(({ dimension, reason }) => `${dimension}: ${reason}`);
|
|
85
|
+
}
|
|
86
|
+
assertWorkspacePlanSupported(plan);
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
57
89
|
// ─── Verified per-harness conventions (capability matrix) ───────────────────
|
|
90
|
+
/**
|
|
91
|
+
* Whether the harness LOADS its {@link CONTEXT_FILE} from cwd on its own.
|
|
92
|
+
*
|
|
93
|
+
* True for every harness whose CLI discovers the file by name (CLAUDE.md,
|
|
94
|
+
* AGENTS.md, …). opencode is the exception: it reads only what its
|
|
95
|
+
* `opencode.json` `instructions[]` names, so a caller writing context for
|
|
96
|
+
* opencode must register the path as well as write it — see the `instructions`
|
|
97
|
+
* lowering below, and the interactive TUI's persona fold.
|
|
98
|
+
*/
|
|
99
|
+
export function harnessAutoLoadsContextFile(harness) {
|
|
100
|
+
return canonicalHarness(harness) !== "opencode";
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The project context file this harness reads from cwd — its stable, documented
|
|
104
|
+
* ALWAYS-LOADED surface. Exported so a caller lowering additive context (e.g. a
|
|
105
|
+
* bare TUI's persona, which has no per-message system channel) targets the same
|
|
106
|
+
* file the materializer writes instructions to, instead of keeping its own copy
|
|
107
|
+
* of this per-harness map.
|
|
108
|
+
*/
|
|
109
|
+
export function harnessContextFile(harness) {
|
|
110
|
+
// The map is keyed by HarnessId, but callers reach this from runtime strings
|
|
111
|
+
// (a registry spec name, a request field). A miss would otherwise return
|
|
112
|
+
// undefined behind a `string` return type and surface as an undefined write
|
|
113
|
+
// path much later.
|
|
114
|
+
const path = CONTEXT_FILE[canonicalHarness(harness)];
|
|
115
|
+
if (!path) {
|
|
116
|
+
throw new Error(`No project context file is known for harness "${harness}"`);
|
|
117
|
+
}
|
|
118
|
+
return path;
|
|
119
|
+
}
|
|
58
120
|
/** Project context file each harness actually reads from cwd. NEVER a constant. */
|
|
59
121
|
const CONTEXT_FILE = {
|
|
60
122
|
"claude-code": "CLAUDE.md",
|
|
@@ -70,8 +132,15 @@ const CONTEXT_FILE = {
|
|
|
70
132
|
hermes: "HERMES.md", // highest-priority native name; only ONE context loads (first match)
|
|
71
133
|
openclaw: "AGENTS.md",
|
|
72
134
|
};
|
|
73
|
-
/**
|
|
74
|
-
|
|
135
|
+
/**
|
|
136
|
+
* cwd-relative native skill dir per harness, or null if the harness has no
|
|
137
|
+
* cwd skill primitive (skills live in a user/global dir the workspace plan
|
|
138
|
+
* cannot reach). Public so consumers composing a profile can (a) render the
|
|
139
|
+
* exact path the platform will mount a skill at into their prompts instead
|
|
140
|
+
* of hardcoding `~/.claude/skills/...`, and (b) branch on skill support
|
|
141
|
+
* ahead of materialization instead of catching the materialize-time throw.
|
|
142
|
+
*/
|
|
143
|
+
export const HARNESS_SKILL_DIRS = {
|
|
75
144
|
"claude-code": ".claude/skills",
|
|
76
145
|
claude: ".claude/skills",
|
|
77
146
|
claudish: ".claude/skills",
|
|
@@ -85,6 +154,18 @@ const SKILL_DIR = {
|
|
|
85
154
|
hermes: null, // skills are ~/.hermes/skills (user) only — no cwd discovery
|
|
86
155
|
openclaw: "skills", // <workspace>/skills/<name>/SKILL.md
|
|
87
156
|
};
|
|
157
|
+
const SKILL_DIR = HARNESS_SKILL_DIRS;
|
|
158
|
+
/**
|
|
159
|
+
* Resolve the cwd-relative directory `resources.skills` refs materialize
|
|
160
|
+
* into on `harness` (after alias canonicalization: claude/claudish →
|
|
161
|
+
* claude-code, kimi → kimi-code). `null` means the harness cannot receive
|
|
162
|
+
* cwd skills — the same condition that surfaces as an `unsupported`
|
|
163
|
+
* "skills" entry (and an {@link assertWorkspacePlanSupported} throw) at
|
|
164
|
+
* materialize time, exposed here as branchable data.
|
|
165
|
+
*/
|
|
166
|
+
export function skillDirForHarness(harness) {
|
|
167
|
+
return HARNESS_SKILL_DIRS[canonicalHarness(harness)];
|
|
168
|
+
}
|
|
88
169
|
function canonicalHarness(h) {
|
|
89
170
|
if (h === "claude" || h === "claudish")
|
|
90
171
|
return "claude-code";
|
|
@@ -360,14 +441,23 @@ export function materializeProfile(inputProfile, harness, opts = {}) {
|
|
|
360
441
|
flags: [],
|
|
361
442
|
unsupported: [],
|
|
362
443
|
};
|
|
363
|
-
|
|
444
|
+
// Every plan file is tagged by provenance at the single push site: `add`
|
|
445
|
+
// (harness-generated native config) vs `addExplicitResource` (a profile
|
|
446
|
+
// resource ref's bytes, passed through verbatim). Downstream consumers that
|
|
447
|
+
// reinterpret a specific generated file by relPath (e.g. opencode.json as
|
|
448
|
+
// the synthetic native config) must gate on `source === "generated"` —
|
|
449
|
+
// otherwise a user resource mounted at the same conventional path is
|
|
450
|
+
// silently misread as trusted harness config.
|
|
451
|
+
const pushPlanFile = (relPath, content, source, mode) => {
|
|
364
452
|
const normalizedPath = normalizeWorkspaceRelativePath(relPath);
|
|
365
453
|
plan.files.push({
|
|
366
454
|
relPath: normalizedPath,
|
|
367
455
|
content,
|
|
456
|
+
source,
|
|
368
457
|
...(mode === undefined ? {} : { mode: validateFileMode(mode) }),
|
|
369
458
|
});
|
|
370
459
|
};
|
|
460
|
+
const add = (relPath, content, mode) => pushPlanFile(relPath, content, "generated", mode);
|
|
371
461
|
const unsupported = (dimension, reason) => plan.unsupported.push({ dimension, reason });
|
|
372
462
|
const explicitResourcePaths = new Map();
|
|
373
463
|
const addExplicitResource = (dimension, relPath, content, mode) => {
|
|
@@ -377,7 +467,7 @@ export function materializeProfile(inputProfile, harness, opts = {}) {
|
|
|
377
467
|
throw new Error(`Duplicate profile resource path: ${normalizedPath} (${previousDimension}, ${dimension})`);
|
|
378
468
|
}
|
|
379
469
|
explicitResourcePaths.set(normalizedPath, dimension);
|
|
380
|
-
|
|
470
|
+
pushPlanFile(normalizedPath, content, "user", mode);
|
|
381
471
|
};
|
|
382
472
|
// 1) GENERIC FILES → exact workspace-relative paths. Inline refs are
|
|
383
473
|
// deterministic; remote refs must be resolved by an async caller first.
|
|
@@ -1310,33 +1400,38 @@ function claudeToolName(name) {
|
|
|
1310
1400
|
return "WebFetch";
|
|
1311
1401
|
return CLAUDE_TOOL_NAMES[normalizedControlName(name)];
|
|
1312
1402
|
}
|
|
1313
|
-
|
|
1403
|
+
/** Tool IDs reported by the OpenCode binary shipped in the Sandbox image. */
|
|
1404
|
+
export const OPENCODE_NATIVE_TOOL_IDS = [
|
|
1405
|
+
"apply_patch",
|
|
1314
1406
|
"bash",
|
|
1315
|
-
"batch",
|
|
1316
1407
|
"codesearch",
|
|
1317
1408
|
"edit",
|
|
1318
1409
|
"glob",
|
|
1319
1410
|
"grep",
|
|
1320
|
-
"
|
|
1321
|
-
"lsp",
|
|
1322
|
-
"multiedit",
|
|
1323
|
-
"patch",
|
|
1411
|
+
"question",
|
|
1324
1412
|
"read",
|
|
1413
|
+
"skill",
|
|
1325
1414
|
"task",
|
|
1326
|
-
"todoread",
|
|
1327
1415
|
"todowrite",
|
|
1328
1416
|
"webfetch",
|
|
1329
1417
|
"websearch",
|
|
1330
1418
|
"write",
|
|
1331
|
-
]
|
|
1332
|
-
|
|
1419
|
+
];
|
|
1420
|
+
const OPENCODE_NATIVE_TOOL_ID_SET = new Set(OPENCODE_NATIVE_TOOL_IDS);
|
|
1421
|
+
/** Resolve portable and legacy profile names to current OpenCode tool IDs. */
|
|
1422
|
+
export function openCodeNativeToolId(name) {
|
|
1333
1423
|
const web = nativeWebToolKind(name);
|
|
1334
1424
|
if (web === "search")
|
|
1335
1425
|
return "websearch";
|
|
1336
1426
|
if (web === "fetch")
|
|
1337
1427
|
return "webfetch";
|
|
1338
1428
|
const normalized = normalizedControlName(name);
|
|
1339
|
-
|
|
1429
|
+
if (normalized === "patch")
|
|
1430
|
+
return "apply_patch";
|
|
1431
|
+
return OPENCODE_NATIVE_TOOL_ID_SET.has(normalized) ? normalized : undefined;
|
|
1432
|
+
}
|
|
1433
|
+
function openCodeToolName(name) {
|
|
1434
|
+
return openCodeNativeToolId(name);
|
|
1340
1435
|
}
|
|
1341
1436
|
function openCodePermissionName(name) {
|
|
1342
1437
|
const normalized = normalizedControlName(name);
|
|
@@ -1350,7 +1445,7 @@ function openCodePermissionName(name) {
|
|
|
1350
1445
|
if (["bash", "mcp", "doom_loop", "external_directory"].includes(normalized)) {
|
|
1351
1446
|
return normalized;
|
|
1352
1447
|
}
|
|
1353
|
-
return normalized !== "read" &&
|
|
1448
|
+
return normalized !== "read" && OPENCODE_NATIVE_TOOL_ID_SET.has(normalized)
|
|
1354
1449
|
? normalized
|
|
1355
1450
|
: undefined;
|
|
1356
1451
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-profile-materialize",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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.32.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "25.6.0",
|