@tangle-network/agent-profile-materialize 0.9.2 → 0.9.4
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 +58 -12
- package/dist/index.js +615 -54
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* `node:fs` IO consumer. It was previously copied byte-for-byte into both
|
|
22
22
|
* cli-bridge and agent-dev-container — this is the single source those re-export.
|
|
23
23
|
*/
|
|
24
|
-
import type { AgentCandidateConfigValue, AgentCandidateProfile, AgentCandidateProfileActivation, AgentCandidateProfileApplication, AgentCandidateProfilePlanEvidence, AgentCandidateProfilePlanMaterial, AgentProfile, AgentProfileMcpServer, Sha256Digest } from "@tangle-network/agent-interface";
|
|
24
|
+
import type { AgentCandidateConfigValue, AgentCandidateProfile, AgentCandidateProfileActivation, AgentCandidateProfileApplication, AgentCandidateProfilePlanEvidence, AgentCandidateProfilePlanMaterial, AgentProfile, AgentProfileConfigValue, AgentProfileMcpServer, AgentProfileSecretProvider, AgentProfileSecretRef, Sha256Digest } from "@tangle-network/agent-interface";
|
|
25
|
+
export declare const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER: AgentProfileSecretProvider;
|
|
25
26
|
export declare const MATERIALIZER_HARNESSES: readonly ["claude-code", "nanoclaw", "codex", "opencode", "kimi-code", "pi", "gemini", "hermes", "openclaw"];
|
|
26
27
|
export type HarnessId = (typeof MATERIALIZER_HARNESSES)[number];
|
|
27
28
|
export declare function isMaterializerHarness(harness: string): harness is HarnessId;
|
|
@@ -50,7 +51,31 @@ export interface PlanFile {
|
|
|
50
51
|
* this on every file it emits.
|
|
51
52
|
*/
|
|
52
53
|
source?: "user" | "generated";
|
|
54
|
+
/**
|
|
55
|
+
* Secret leaves in a generated JSON document. `content` contains `null` at
|
|
56
|
+
* each path until the private executor resolves the named reference. The
|
|
57
|
+
* public plan, its digest, and its receipt retain only these references.
|
|
58
|
+
*/
|
|
59
|
+
secretSlots?: PlanFileSecretSlot[];
|
|
60
|
+
}
|
|
61
|
+
export interface PlanFileSecretSlot {
|
|
62
|
+
path: Array<string | number>;
|
|
63
|
+
ref: AgentProfileSecretRef;
|
|
64
|
+
}
|
|
65
|
+
export interface WorkspacePlanSecretSegment {
|
|
66
|
+
kind: "secret";
|
|
67
|
+
ref: AgentProfileSecretRef;
|
|
68
|
+
/** Encoding applied after private resolution. */
|
|
69
|
+
encoding: "raw" | "json-string";
|
|
53
70
|
}
|
|
71
|
+
/** A launch argument whose private leaves are resolved immediately before spawn. */
|
|
72
|
+
export interface WorkspacePlanArgumentTemplate {
|
|
73
|
+
kind: "workspace-plan-argument-template";
|
|
74
|
+
segments: Array<string | WorkspacePlanSecretSegment>;
|
|
75
|
+
}
|
|
76
|
+
export type WorkspacePlanArgument = string | WorkspacePlanArgumentTemplate;
|
|
77
|
+
/** Public bytes stay strings; secret references stay opaque until application. */
|
|
78
|
+
export type WorkspacePlanConfigValue = string | AgentProfileSecretRef;
|
|
54
79
|
export interface Unsupported {
|
|
55
80
|
dimension: "files" | "skills" | "mcp" | "hooks" | "subagents" | "commands" | "instructions" | "systemPrompt" | "connections" | "confidential" | "permissions" | "tools" | "modes" | "extensions";
|
|
56
81
|
reason: string;
|
|
@@ -61,9 +86,9 @@ export interface WorkspacePlan {
|
|
|
61
86
|
/** Files to write relative to the run workspace (cwd). */
|
|
62
87
|
files: PlanFile[];
|
|
63
88
|
/** Env vars to set on the spawned process (for harnesses needing them). */
|
|
64
|
-
env: Record<string,
|
|
89
|
+
env: Record<string, WorkspacePlanConfigValue>;
|
|
65
90
|
/** Extra CLI flags to pass (e.g. kimi --mcp-config-file <path>). */
|
|
66
|
-
flags:
|
|
91
|
+
flags: WorkspacePlanArgument[];
|
|
67
92
|
/** Exact system-prompt replacement supplied to the harness. */
|
|
68
93
|
systemPrompt?: string;
|
|
69
94
|
/** Dimensions requested but with NO cwd path on this harness — fail-closed record. */
|
|
@@ -76,12 +101,27 @@ export interface WorkspacePlan {
|
|
|
76
101
|
*/
|
|
77
102
|
export interface WorkspacePlanReceipt {
|
|
78
103
|
workspacePlanDigest: string;
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
/** Public launch identity only; secret values never enter this receipt. */
|
|
105
|
+
env: Record<string, WorkspacePlanConfigValue>;
|
|
106
|
+
/** Public launch identity only; templates retain opaque secret references. */
|
|
107
|
+
flags: WorkspacePlanArgument[];
|
|
81
108
|
systemPrompt?: string;
|
|
82
109
|
unsupported: Unsupported[];
|
|
83
110
|
written: string[];
|
|
84
111
|
}
|
|
112
|
+
/** Private values handed directly to the provider spawn, never to receipts. */
|
|
113
|
+
export interface WorkspacePlanSpawnInputs {
|
|
114
|
+
env: Record<string, string>;
|
|
115
|
+
flags: string[];
|
|
116
|
+
systemPrompt?: string;
|
|
117
|
+
}
|
|
118
|
+
/** Result of applying a public plan at the private executor boundary. */
|
|
119
|
+
export interface WorkspacePlanApplication {
|
|
120
|
+
receipt: WorkspacePlanReceipt;
|
|
121
|
+
spawn: WorkspacePlanSpawnInputs;
|
|
122
|
+
/** Resolved generated files intentionally consumed in memory by the runtime. */
|
|
123
|
+
runtimeOnlyFiles: Record<string, string>;
|
|
124
|
+
}
|
|
85
125
|
/** The plan was applied, but one or more private staging files remain. */
|
|
86
126
|
export declare class WorkspacePlanCleanupError extends AggregateError {
|
|
87
127
|
readonly receipt: WorkspacePlanReceipt;
|
|
@@ -218,12 +258,18 @@ export declare function captureAgentCandidateProfilePlan(plan: AgentCandidateWor
|
|
|
218
258
|
*/
|
|
219
259
|
export declare function applyAgentCandidateWorkspacePlan(plan: AgentCandidateWorkspacePlan, workspaceDir: string, targetWorkspace: AgentCandidateProfileApplication["targetWorkspace"]): AgentCandidateWorkspacePlanReceipt;
|
|
220
260
|
/**
|
|
221
|
-
* Apply a WorkspacePlan
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* agent-dev-container box, and VB. Returns `unsupported` so the caller can log/fail.
|
|
261
|
+
* Apply a public-only WorkspacePlan. A plan with any secret reference fails
|
|
262
|
+
* before the first write; private executors use
|
|
263
|
+
* {@link applyWorkspacePlanWithSecretProvider} instead.
|
|
225
264
|
*/
|
|
226
265
|
export declare function applyWorkspacePlan(plan: WorkspacePlan, workspaceDir: string, options?: ApplyWorkspacePlanOptions): WorkspacePlanReceipt;
|
|
266
|
+
/**
|
|
267
|
+
* Resolve opaque profile references through an executor-owned provider, then
|
|
268
|
+
* apply the resulting private native files and launch inputs. Resolution
|
|
269
|
+
* completes before any workspace write, and resolved values never enter the
|
|
270
|
+
* public plan, digest, receipt, logs, or error messages.
|
|
271
|
+
*/
|
|
272
|
+
export declare function applyWorkspacePlanWithSecretProvider(plan: WorkspacePlan, workspaceDir: string, secretProvider: AgentProfileSecretProvider, options?: ApplyWorkspacePlanOptions): Promise<WorkspacePlanApplication>;
|
|
227
273
|
/** Tool IDs reported by the OpenCode binary shipped in the Sandbox image. */
|
|
228
274
|
export declare const OPENCODE_NATIVE_TOOL_IDS: readonly ["apply_patch", "bash", "codesearch", "edit", "glob", "grep", "question", "read", "skill", "task", "todowrite", "webfetch", "websearch", "write"];
|
|
229
275
|
/** Resolve portable and legacy profile names to current OpenCode tool IDs. */
|
|
@@ -233,12 +279,12 @@ export type OpenCodeMcpConfig = Record<string, {
|
|
|
233
279
|
type: "remote";
|
|
234
280
|
url: string;
|
|
235
281
|
enabled: boolean;
|
|
236
|
-
headers?: Record<string,
|
|
282
|
+
headers?: Record<string, AgentProfileConfigValue>;
|
|
237
283
|
} | {
|
|
238
284
|
type: "local";
|
|
239
|
-
command: string
|
|
285
|
+
command: Array<string | AgentProfileConfigValue>;
|
|
240
286
|
enabled: boolean;
|
|
241
|
-
environment?: Record<string,
|
|
287
|
+
environment?: Record<string, AgentProfileConfigValue>;
|
|
242
288
|
}>;
|
|
243
289
|
/** Lower canonical MCP definitions to OpenCode's native startup configuration. */
|
|
244
290
|
export declare function openCodeMcpConfig(mcp: Record<string, AgentProfileMcpServer>): OpenCodeMcpConfig;
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,12 @@
|
|
|
24
24
|
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
|
-
import { agentCandidateProfileActivationSchema, agentCandidateProfilePlanEvidenceSchema, agentCandidateProfilePlanMaterialSchema, agentCandidateProfileSchema, agentProfileSchema, canonicalCandidateBytes, canonicalCandidateDigest, resolveNativeWebTools, } from "@tangle-network/agent-interface";
|
|
27
|
+
import { agentCandidateProfileActivationSchema, agentCandidateProfilePlanEvidenceSchema, agentCandidateProfilePlanMaterialSchema, agentCandidateProfileSchema, agentProfileSchema, agentProfileSecretRefSchema, canonicalCandidateBytes, canonicalCandidateDigest, resolveNativeWebTools, } from "@tangle-network/agent-interface";
|
|
28
|
+
export const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER = Object.freeze({
|
|
29
|
+
async get() {
|
|
30
|
+
return undefined;
|
|
31
|
+
},
|
|
32
|
+
});
|
|
28
33
|
function describeUnresolvedRef(r) {
|
|
29
34
|
const name = typeof r.name === "string" ? r.name : undefined;
|
|
30
35
|
const path = typeof r.path === "string" ? r.path : undefined;
|
|
@@ -187,6 +192,7 @@ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
|
|
|
187
192
|
const collapse = (s) => s.replace(/\s+/g, " ").trim();
|
|
188
193
|
const compareStrings = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
189
194
|
const DEFAULT_FILE_MODE = 0o644;
|
|
195
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
190
196
|
const EXECUTABLE_FILE_MODE = 0o755;
|
|
191
197
|
const DEFAULT_RESOURCE_FETCH_TIMEOUT_MS = 30_000;
|
|
192
198
|
const MAX_RESOURCE_BYTES = 10 * 1024 * 1024;
|
|
@@ -437,6 +443,96 @@ export function normalizeSkillMd(name, raw) {
|
|
|
437
443
|
return `---\nname: ${name}\ndescription: ${JSON.stringify(desc)}\n---\n\n${body}\n`;
|
|
438
444
|
}
|
|
439
445
|
const tomlStr = (s) => JSON.stringify(s); // valid TOML basic string
|
|
446
|
+
function isProfilePublicConfigValue(value) {
|
|
447
|
+
return (typeof value === "object" &&
|
|
448
|
+
value !== null &&
|
|
449
|
+
!Array.isArray(value) &&
|
|
450
|
+
value.kind === "public" &&
|
|
451
|
+
typeof value.value === "string");
|
|
452
|
+
}
|
|
453
|
+
function isProfileSecretRef(value) {
|
|
454
|
+
return (typeof value === "object" &&
|
|
455
|
+
value !== null &&
|
|
456
|
+
!Array.isArray(value) &&
|
|
457
|
+
value.kind === "secret-ref" &&
|
|
458
|
+
typeof value.key === "string");
|
|
459
|
+
}
|
|
460
|
+
function workspacePlanConfigValue(value) {
|
|
461
|
+
return isProfilePublicConfigValue(value) ? value.value : { ...value };
|
|
462
|
+
}
|
|
463
|
+
function serializeWorkspacePlanJson(value) {
|
|
464
|
+
const secretSlots = [];
|
|
465
|
+
const lower = (current, path, arrayEntry = false) => {
|
|
466
|
+
if (isProfilePublicConfigValue(current))
|
|
467
|
+
return current.value;
|
|
468
|
+
if (isProfileSecretRef(current)) {
|
|
469
|
+
secretSlots.push({ path: [...path], ref: { ...current } });
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
if (Array.isArray(current)) {
|
|
473
|
+
return current.map((entry, index) => lower(entry, [...path, index], true));
|
|
474
|
+
}
|
|
475
|
+
if (current && typeof current === "object") {
|
|
476
|
+
const output = Object.create(null);
|
|
477
|
+
for (const [key, entry] of Object.entries(current)) {
|
|
478
|
+
const lowered = lower(entry, [...path, key]);
|
|
479
|
+
if (lowered !== undefined) {
|
|
480
|
+
Object.defineProperty(output, key, {
|
|
481
|
+
value: lowered,
|
|
482
|
+
enumerable: true,
|
|
483
|
+
configurable: true,
|
|
484
|
+
writable: true,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return output;
|
|
489
|
+
}
|
|
490
|
+
return current === undefined && arrayEntry ? null : current;
|
|
491
|
+
};
|
|
492
|
+
const content = JSON.stringify(lower(value, []), null, 2);
|
|
493
|
+
if (content === undefined) {
|
|
494
|
+
throw new Error("Generated profile JSON must have a serializable root");
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
content,
|
|
498
|
+
...(secretSlots.length === 0 ? {} : { secretSlots }),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function workspacePlanArgumentSegments(value) {
|
|
502
|
+
return typeof value === "string" ? [value] : [...value.segments];
|
|
503
|
+
}
|
|
504
|
+
function concatWorkspacePlanArguments(...values) {
|
|
505
|
+
const segments = values.flatMap(workspacePlanArgumentSegments);
|
|
506
|
+
if (segments.every((segment) => typeof segment === "string")) {
|
|
507
|
+
return segments.join("");
|
|
508
|
+
}
|
|
509
|
+
return { kind: "workspace-plan-argument-template", segments };
|
|
510
|
+
}
|
|
511
|
+
function joinWorkspacePlanArguments(values, separator) {
|
|
512
|
+
if (values.length === 0)
|
|
513
|
+
return "";
|
|
514
|
+
const joined = [];
|
|
515
|
+
for (const [index, value] of values.entries()) {
|
|
516
|
+
if (index > 0)
|
|
517
|
+
joined.push(separator);
|
|
518
|
+
joined.push(value);
|
|
519
|
+
}
|
|
520
|
+
return concatWorkspacePlanArguments(...joined);
|
|
521
|
+
}
|
|
522
|
+
function tomlProfileConfigValue(value) {
|
|
523
|
+
if (isProfilePublicConfigValue(value))
|
|
524
|
+
return tomlStr(value.value);
|
|
525
|
+
return {
|
|
526
|
+
kind: "workspace-plan-argument-template",
|
|
527
|
+
segments: [
|
|
528
|
+
{
|
|
529
|
+
kind: "secret",
|
|
530
|
+
ref: { ...value },
|
|
531
|
+
encoding: "json-string",
|
|
532
|
+
},
|
|
533
|
+
],
|
|
534
|
+
};
|
|
535
|
+
}
|
|
440
536
|
// ─── The materializer ─────────────────────────────────────────────────────────
|
|
441
537
|
/**
|
|
442
538
|
* Build the WorkspacePlan for `profile` on `harness`. Pure — no IO. The caller
|
|
@@ -462,13 +558,27 @@ export function materializeProfile(inputProfile, harness, opts = {}) {
|
|
|
462
558
|
// the synthetic native config) must gate on `source === "generated"` —
|
|
463
559
|
// otherwise a user resource mounted at the same conventional path is
|
|
464
560
|
// silently misread as trusted harness config.
|
|
465
|
-
const pushPlanFile = (relPath,
|
|
561
|
+
const pushPlanFile = (relPath, serialized, source, mode) => {
|
|
466
562
|
const normalizedPath = normalizeWorkspaceRelativePath(relPath);
|
|
563
|
+
const content = typeof serialized === "string" ? serialized : serialized.content;
|
|
564
|
+
const secretSlots = typeof serialized === "string" ? undefined : serialized.secretSlots;
|
|
565
|
+
const fileMode = mode ??
|
|
566
|
+
(secretSlots !== undefined && secretSlots.length > 0
|
|
567
|
+
? PRIVATE_FILE_MODE
|
|
568
|
+
: undefined);
|
|
467
569
|
plan.files.push({
|
|
468
570
|
relPath: normalizedPath,
|
|
469
571
|
content,
|
|
470
572
|
source,
|
|
471
|
-
...(
|
|
573
|
+
...(fileMode === undefined ? {} : { mode: validateFileMode(fileMode) }),
|
|
574
|
+
...(secretSlots === undefined
|
|
575
|
+
? {}
|
|
576
|
+
: {
|
|
577
|
+
secretSlots: secretSlots.map((slot) => ({
|
|
578
|
+
path: [...slot.path],
|
|
579
|
+
ref: { ...slot.ref },
|
|
580
|
+
})),
|
|
581
|
+
}),
|
|
472
582
|
});
|
|
473
583
|
};
|
|
474
584
|
const add = (relPath, content, mode) => pushPlanFile(relPath, content, "generated", mode);
|
|
@@ -572,10 +682,10 @@ export function materializeProfile(inputProfile, harness, opts = {}) {
|
|
|
572
682
|
const instructionPath = CONTEXT_FILE[h];
|
|
573
683
|
add(instructionPath, `${instr.join("\n\n")}\n`);
|
|
574
684
|
if (h === "opencode") {
|
|
575
|
-
add("opencode.json",
|
|
685
|
+
add("opencode.json", serializeWorkspacePlanJson({
|
|
576
686
|
$schema: "https://opencode.ai/config.json",
|
|
577
687
|
instructions: [instructionPath],
|
|
578
|
-
}
|
|
688
|
+
}));
|
|
579
689
|
}
|
|
580
690
|
// gemini reads AGENTS.md only via settings; we write GEMINI.md so no extra step.
|
|
581
691
|
}
|
|
@@ -690,7 +800,7 @@ export function materializeCandidateProfile(profile, harness, opts = {}) {
|
|
|
690
800
|
harness: classicPlan.harness,
|
|
691
801
|
files: classicPlan.files,
|
|
692
802
|
env: candidatePublicRecord(classicPlan.env),
|
|
693
|
-
flags: classicPlan.flags.map(
|
|
803
|
+
flags: classicPlan.flags.map(candidatePublicArgument),
|
|
694
804
|
...(classicPlan.systemPrompt === undefined
|
|
695
805
|
? {}
|
|
696
806
|
: { systemPrompt: candidatePublicValue(classicPlan.systemPrompt) }),
|
|
@@ -781,13 +891,15 @@ function candidateMcpForClassicMaterializer(server) {
|
|
|
781
891
|
...(server.transport === undefined ? {} : { transport: server.transport }),
|
|
782
892
|
...(server.args === undefined
|
|
783
893
|
? {}
|
|
784
|
-
: {
|
|
894
|
+
: {
|
|
895
|
+
args: server.args.map((value) => profilePublicValue(candidatePublicString(value))),
|
|
896
|
+
}),
|
|
785
897
|
...(server.env === undefined
|
|
786
898
|
? {}
|
|
787
899
|
: {
|
|
788
900
|
env: Object.fromEntries(Object.entries(server.env).map(([name, value]) => [
|
|
789
901
|
name,
|
|
790
|
-
candidatePublicString(value),
|
|
902
|
+
profilePublicValue(candidatePublicString(value)),
|
|
791
903
|
])),
|
|
792
904
|
}),
|
|
793
905
|
...(server.cwd === undefined ? {} : { cwd: server.cwd }),
|
|
@@ -844,11 +956,22 @@ function candidatePublicString(value) {
|
|
|
844
956
|
function candidatePublicValue(value) {
|
|
845
957
|
return { kind: "public", value };
|
|
846
958
|
}
|
|
959
|
+
function profilePublicValue(value) {
|
|
960
|
+
return { kind: "public", value };
|
|
961
|
+
}
|
|
962
|
+
function candidatePublicArgument(value) {
|
|
963
|
+
if (typeof value !== "string") {
|
|
964
|
+
throw new Error("Candidate materialization cannot emit private launch arguments");
|
|
965
|
+
}
|
|
966
|
+
return candidatePublicValue(value);
|
|
967
|
+
}
|
|
847
968
|
function candidatePublicRecord(values) {
|
|
848
|
-
return Object.fromEntries(Object.entries(values).map(([name, value]) =>
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
969
|
+
return Object.fromEntries(Object.entries(values).map(([name, value]) => {
|
|
970
|
+
if (typeof value !== "string") {
|
|
971
|
+
throw new Error("Candidate materialization cannot emit private environment values");
|
|
972
|
+
}
|
|
973
|
+
return [name, candidatePublicValue(value)];
|
|
974
|
+
}));
|
|
852
975
|
}
|
|
853
976
|
function verifyCandidateInlineResources(profile) {
|
|
854
977
|
const resources = profile.resources;
|
|
@@ -894,13 +1017,23 @@ function mergeJsonFiles(files) {
|
|
|
894
1017
|
}
|
|
895
1018
|
if (f.relPath.endsWith(".json")) {
|
|
896
1019
|
try {
|
|
1020
|
+
const next = JSON.parse(f.content);
|
|
897
1021
|
const merged = {
|
|
898
1022
|
...JSON.parse(prev.content),
|
|
899
|
-
...
|
|
1023
|
+
...next,
|
|
900
1024
|
};
|
|
1025
|
+
const replacedRoots = new Set(Object.keys(next));
|
|
1026
|
+
const secretSlots = [
|
|
1027
|
+
...(prev.secretSlots ?? []).filter((slot) => typeof slot.path[0] === "string" &&
|
|
1028
|
+
!replacedRoots.has(slot.path[0])),
|
|
1029
|
+
...(f.secretSlots ?? []),
|
|
1030
|
+
];
|
|
1031
|
+
const { secretSlots: _previousSecretSlots, ...previousFile } = prev;
|
|
901
1032
|
byPath.set(f.relPath, {
|
|
902
|
-
...
|
|
1033
|
+
...previousFile,
|
|
903
1034
|
content: JSON.stringify(merged, null, 2),
|
|
1035
|
+
...(secretSlots.length > 0 ? { mode: PRIVATE_FILE_MODE } : {}),
|
|
1036
|
+
...(secretSlots.length === 0 ? {} : { secretSlots }),
|
|
904
1037
|
});
|
|
905
1038
|
continue;
|
|
906
1039
|
}
|
|
@@ -912,6 +1045,60 @@ function mergeJsonFiles(files) {
|
|
|
912
1045
|
}
|
|
913
1046
|
return [...byPath.values()];
|
|
914
1047
|
}
|
|
1048
|
+
function valueAtJsonPath(root, path) {
|
|
1049
|
+
let current = root;
|
|
1050
|
+
for (const segment of path) {
|
|
1051
|
+
if (current === null ||
|
|
1052
|
+
typeof current !== "object" ||
|
|
1053
|
+
!Object.hasOwn(current, segment)) {
|
|
1054
|
+
return undefined;
|
|
1055
|
+
}
|
|
1056
|
+
current = current[segment];
|
|
1057
|
+
}
|
|
1058
|
+
return current;
|
|
1059
|
+
}
|
|
1060
|
+
function normalizePlanFileSecretSlots(file) {
|
|
1061
|
+
if (file.secretSlots === undefined)
|
|
1062
|
+
return [];
|
|
1063
|
+
if (file.source !== "generated" ||
|
|
1064
|
+
!file.relPath.endsWith(".json") ||
|
|
1065
|
+
file.secretSlots.length === 0) {
|
|
1066
|
+
throw new Error(`Workspace secret slots require a generated JSON file: ${file.relPath}`);
|
|
1067
|
+
}
|
|
1068
|
+
let parsed;
|
|
1069
|
+
try {
|
|
1070
|
+
parsed = JSON.parse(file.content);
|
|
1071
|
+
}
|
|
1072
|
+
catch {
|
|
1073
|
+
throw new Error(`Workspace secret slots require valid JSON content: ${file.relPath}`);
|
|
1074
|
+
}
|
|
1075
|
+
const seen = new Set();
|
|
1076
|
+
return file.secretSlots.map((slot) => {
|
|
1077
|
+
if (!Array.isArray(slot.path) || slot.path.length === 0) {
|
|
1078
|
+
throw new Error(`Workspace secret slot path must be non-empty: ${file.relPath}`);
|
|
1079
|
+
}
|
|
1080
|
+
const path = slot.path.map((segment) => {
|
|
1081
|
+
if ((typeof segment !== "string" && typeof segment !== "number") ||
|
|
1082
|
+
(typeof segment === "number" &&
|
|
1083
|
+
(!Number.isSafeInteger(segment) || segment < 0))) {
|
|
1084
|
+
throw new Error(`Workspace secret slot path is invalid: ${file.relPath}`);
|
|
1085
|
+
}
|
|
1086
|
+
return segment;
|
|
1087
|
+
});
|
|
1088
|
+
const identity = JSON.stringify(path);
|
|
1089
|
+
if (seen.has(identity)) {
|
|
1090
|
+
throw new Error(`Duplicate workspace secret slot path: ${file.relPath} ${identity}`);
|
|
1091
|
+
}
|
|
1092
|
+
seen.add(identity);
|
|
1093
|
+
if (valueAtJsonPath(parsed, path) !== null) {
|
|
1094
|
+
throw new Error(`Workspace secret slot must point at a null placeholder: ${file.relPath} ${identity}`);
|
|
1095
|
+
}
|
|
1096
|
+
return {
|
|
1097
|
+
path,
|
|
1098
|
+
ref: agentProfileSecretRefSchema.parse(slot.ref),
|
|
1099
|
+
};
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
915
1102
|
function normalizePlanFiles(files) {
|
|
916
1103
|
const seen = new Set();
|
|
917
1104
|
return files
|
|
@@ -924,15 +1111,56 @@ function normalizePlanFiles(files) {
|
|
|
924
1111
|
if (typeof file.content !== "string") {
|
|
925
1112
|
throw new Error(`Workspace file content must be a string: ${relPath}`);
|
|
926
1113
|
}
|
|
1114
|
+
const mode = validateFileMode(file.mode);
|
|
1115
|
+
const secretSlots = normalizePlanFileSecretSlots(file);
|
|
1116
|
+
if (secretSlots.length > 0 && (mode & 0o077) !== 0) {
|
|
1117
|
+
throw new Error(`Workspace secret-bearing file must not grant group or other access: ${relPath}`);
|
|
1118
|
+
}
|
|
927
1119
|
return {
|
|
928
1120
|
relPath,
|
|
929
1121
|
content: file.content,
|
|
930
|
-
mode
|
|
1122
|
+
mode,
|
|
1123
|
+
secretSlots,
|
|
931
1124
|
};
|
|
932
1125
|
})
|
|
933
1126
|
.sort((a, b) => compareStrings(a.relPath, b.relPath));
|
|
934
1127
|
}
|
|
935
1128
|
const sha256 = (value) => createHash("sha256").update(value, "utf8").digest("hex");
|
|
1129
|
+
function normalizeWorkspacePlanArgument(value) {
|
|
1130
|
+
if (typeof value === "string")
|
|
1131
|
+
return value;
|
|
1132
|
+
if (value.kind !== "workspace-plan-argument-template" ||
|
|
1133
|
+
!Array.isArray(value.segments) ||
|
|
1134
|
+
value.segments.length === 0) {
|
|
1135
|
+
throw new Error("Workspace plan argument template is malformed");
|
|
1136
|
+
}
|
|
1137
|
+
const segments = [];
|
|
1138
|
+
for (const segment of value.segments) {
|
|
1139
|
+
if (typeof segment === "string") {
|
|
1140
|
+
const previous = segments.at(-1);
|
|
1141
|
+
if (typeof previous === "string") {
|
|
1142
|
+
segments[segments.length - 1] = previous + segment;
|
|
1143
|
+
}
|
|
1144
|
+
else if (segment.length > 0) {
|
|
1145
|
+
segments.push(segment);
|
|
1146
|
+
}
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
if (segment.kind !== "secret" ||
|
|
1150
|
+
(segment.encoding !== "raw" && segment.encoding !== "json-string")) {
|
|
1151
|
+
throw new Error("Workspace plan secret argument segment is malformed");
|
|
1152
|
+
}
|
|
1153
|
+
segments.push({
|
|
1154
|
+
kind: "secret",
|
|
1155
|
+
ref: agentProfileSecretRefSchema.parse(segment.ref),
|
|
1156
|
+
encoding: segment.encoding,
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
if (segments.every((segment) => typeof segment === "string")) {
|
|
1160
|
+
return segments.join("");
|
|
1161
|
+
}
|
|
1162
|
+
return { kind: "workspace-plan-argument-template", segments };
|
|
1163
|
+
}
|
|
936
1164
|
/**
|
|
937
1165
|
* Hash the exact files, modes, flags, environment, and unsupported dimensions
|
|
938
1166
|
* represented by a plan. Array order is preserved only where it changes spawn
|
|
@@ -943,14 +1171,26 @@ export function hashWorkspacePlan(plan) {
|
|
|
943
1171
|
relPath: file.relPath,
|
|
944
1172
|
mode: file.mode,
|
|
945
1173
|
contentSha256: sha256(file.content),
|
|
1174
|
+
...(file.secretSlots.length === 0
|
|
1175
|
+
? {}
|
|
1176
|
+
: {
|
|
1177
|
+
secretSlots: [...file.secretSlots].sort((left, right) => compareStrings(JSON.stringify(left.path), JSON.stringify(right.path))),
|
|
1178
|
+
}),
|
|
946
1179
|
}));
|
|
947
|
-
const env = Object.fromEntries(Object.entries(plan.env)
|
|
1180
|
+
const env = Object.fromEntries(Object.entries(plan.env)
|
|
1181
|
+
.sort(([a], [b]) => compareStrings(a, b))
|
|
1182
|
+
.map(([name, value]) => [
|
|
1183
|
+
name,
|
|
1184
|
+
typeof value === "string"
|
|
1185
|
+
? value
|
|
1186
|
+
: agentProfileSecretRefSchema.parse(value),
|
|
1187
|
+
]));
|
|
948
1188
|
const unsupported = [...plan.unsupported].sort((a, b) => compareStrings(`${a.dimension}\0${a.reason}`, `${b.dimension}\0${b.reason}`));
|
|
949
1189
|
const canonical = JSON.stringify({
|
|
950
1190
|
harness: plan.harness,
|
|
951
1191
|
files,
|
|
952
1192
|
env,
|
|
953
|
-
flags:
|
|
1193
|
+
flags: plan.flags.map(normalizeWorkspacePlanArgument),
|
|
954
1194
|
...(plan.systemPrompt === undefined
|
|
955
1195
|
? {}
|
|
956
1196
|
: { systemPrompt: plan.systemPrompt }),
|
|
@@ -1053,19 +1293,186 @@ export function applyAgentCandidateWorkspacePlan(plan, workspaceDir, targetWorks
|
|
|
1053
1293
|
};
|
|
1054
1294
|
}
|
|
1055
1295
|
/**
|
|
1056
|
-
* Apply a WorkspacePlan
|
|
1057
|
-
*
|
|
1058
|
-
*
|
|
1059
|
-
* agent-dev-container box, and VB. Returns `unsupported` so the caller can log/fail.
|
|
1296
|
+
* Apply a public-only WorkspacePlan. A plan with any secret reference fails
|
|
1297
|
+
* before the first write; private executors use
|
|
1298
|
+
* {@link applyWorkspacePlanWithSecretProvider} instead.
|
|
1060
1299
|
*/
|
|
1061
1300
|
export function applyWorkspacePlan(plan, workspaceDir, options = {}) {
|
|
1301
|
+
return applyResolvedWorkspacePlan(plan, resolvePublicWorkspacePlan(plan), workspaceDir, options).receipt;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Resolve opaque profile references through an executor-owned provider, then
|
|
1305
|
+
* apply the resulting private native files and launch inputs. Resolution
|
|
1306
|
+
* completes before any workspace write, and resolved values never enter the
|
|
1307
|
+
* public plan, digest, receipt, logs, or error messages.
|
|
1308
|
+
*/
|
|
1309
|
+
export async function applyWorkspacePlanWithSecretProvider(plan, workspaceDir, secretProvider, options = {}) {
|
|
1310
|
+
const planDigest = hashWorkspacePlan(plan);
|
|
1311
|
+
const resolved = await resolvePrivateWorkspacePlan(plan, secretProvider);
|
|
1312
|
+
return applyResolvedWorkspacePlan(plan, resolved, workspaceDir, options, planDigest);
|
|
1313
|
+
}
|
|
1314
|
+
function secretRefLabel(ref) {
|
|
1315
|
+
return JSON.stringify(ref.key);
|
|
1316
|
+
}
|
|
1317
|
+
function requireResolvedSecretValue(ref, value) {
|
|
1318
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1319
|
+
throw new Error(`AgentProfile secret provider returned no value for ${secretRefLabel(ref)}`);
|
|
1320
|
+
}
|
|
1321
|
+
return value;
|
|
1322
|
+
}
|
|
1323
|
+
function setValueAtJsonPath(root, path, value) {
|
|
1324
|
+
let current = root;
|
|
1325
|
+
for (const segment of path.slice(0, -1)) {
|
|
1326
|
+
if (current === null ||
|
|
1327
|
+
typeof current !== "object" ||
|
|
1328
|
+
!Object.hasOwn(current, segment)) {
|
|
1329
|
+
throw new Error("Generated profile JSON secret path no longer exists");
|
|
1330
|
+
}
|
|
1331
|
+
current = current[segment];
|
|
1332
|
+
}
|
|
1333
|
+
const finalSegment = path.at(-1);
|
|
1334
|
+
if (finalSegment === undefined ||
|
|
1335
|
+
current === null ||
|
|
1336
|
+
typeof current !== "object" ||
|
|
1337
|
+
!Object.hasOwn(current, finalSegment)) {
|
|
1338
|
+
throw new Error("Generated profile JSON secret path no longer exists");
|
|
1339
|
+
}
|
|
1340
|
+
Object.defineProperty(current, finalSegment, {
|
|
1341
|
+
value,
|
|
1342
|
+
enumerable: true,
|
|
1343
|
+
configurable: true,
|
|
1344
|
+
writable: true,
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
function resolvePublicWorkspacePlan(plan) {
|
|
1348
|
+
const files = normalizePlanFiles(plan.files);
|
|
1349
|
+
for (const file of files) {
|
|
1350
|
+
const ref = file.secretSlots[0]?.ref;
|
|
1351
|
+
if (ref) {
|
|
1352
|
+
throw new Error(`Workspace plan requires an AgentProfileSecretProvider for ${secretRefLabel(ref)}`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
const env = Object.fromEntries(Object.entries(plan.env).map(([name, value]) => {
|
|
1356
|
+
if (typeof value !== "string") {
|
|
1357
|
+
throw new Error(`Workspace plan requires an AgentProfileSecretProvider for ${secretRefLabel(value)}`);
|
|
1358
|
+
}
|
|
1359
|
+
return [name, value];
|
|
1360
|
+
}));
|
|
1361
|
+
const flags = plan.flags.map((argument) => {
|
|
1362
|
+
const normalized = normalizeWorkspacePlanArgument(argument);
|
|
1363
|
+
if (typeof normalized !== "string") {
|
|
1364
|
+
const segment = normalized.segments.find((value) => typeof value !== "string");
|
|
1365
|
+
if (!segment) {
|
|
1366
|
+
throw new Error("Workspace plan argument template is malformed");
|
|
1367
|
+
}
|
|
1368
|
+
throw new Error(`Workspace plan requires an AgentProfileSecretProvider for ${secretRefLabel(segment.ref)}`);
|
|
1369
|
+
}
|
|
1370
|
+
return normalized;
|
|
1371
|
+
});
|
|
1372
|
+
return {
|
|
1373
|
+
files,
|
|
1374
|
+
env,
|
|
1375
|
+
flags,
|
|
1376
|
+
...(plan.systemPrompt === undefined
|
|
1377
|
+
? {}
|
|
1378
|
+
: { systemPrompt: plan.systemPrompt }),
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
async function resolvePrivateWorkspacePlan(plan, secretProvider) {
|
|
1382
|
+
if (!secretProvider || typeof secretProvider.get !== "function") {
|
|
1383
|
+
throw new Error("AgentProfileSecretProvider.get is required");
|
|
1384
|
+
}
|
|
1385
|
+
const pending = new Map();
|
|
1386
|
+
const resolveRef = async (input) => {
|
|
1387
|
+
const ref = agentProfileSecretRefSchema.parse(input);
|
|
1388
|
+
let raw = pending.get(ref.key);
|
|
1389
|
+
if (!raw) {
|
|
1390
|
+
raw = (async () => {
|
|
1391
|
+
let value;
|
|
1392
|
+
try {
|
|
1393
|
+
value = await secretProvider.get(ref.key);
|
|
1394
|
+
}
|
|
1395
|
+
catch {
|
|
1396
|
+
throw new Error(`AgentProfile secret provider failed for ${secretRefLabel(ref)}`);
|
|
1397
|
+
}
|
|
1398
|
+
return requireResolvedSecretValue(ref, value);
|
|
1399
|
+
})();
|
|
1400
|
+
pending.set(ref.key, raw);
|
|
1401
|
+
}
|
|
1402
|
+
const value = await raw;
|
|
1403
|
+
return ref.format === "bearer" ? `Bearer ${value}` : value;
|
|
1404
|
+
};
|
|
1405
|
+
const files = await Promise.all(normalizePlanFiles(plan.files).map(async (file) => {
|
|
1406
|
+
if (file.secretSlots.length === 0)
|
|
1407
|
+
return file;
|
|
1408
|
+
const parsed = JSON.parse(file.content);
|
|
1409
|
+
const values = await Promise.all(file.secretSlots.map(async (slot) => ({
|
|
1410
|
+
path: slot.path,
|
|
1411
|
+
value: await resolveRef(slot.ref),
|
|
1412
|
+
})));
|
|
1413
|
+
for (const resolved of values) {
|
|
1414
|
+
setValueAtJsonPath(parsed, resolved.path, resolved.value);
|
|
1415
|
+
}
|
|
1416
|
+
return {
|
|
1417
|
+
...file,
|
|
1418
|
+
content: JSON.stringify(parsed, null, 2),
|
|
1419
|
+
secretSlots: [],
|
|
1420
|
+
};
|
|
1421
|
+
}));
|
|
1422
|
+
const env = Object.fromEntries(await Promise.all(Object.entries(plan.env).map(async ([name, value]) => [
|
|
1423
|
+
name,
|
|
1424
|
+
typeof value === "string" ? value : await resolveRef(value),
|
|
1425
|
+
])));
|
|
1426
|
+
const flags = await Promise.all(plan.flags.map(async (argument) => {
|
|
1427
|
+
const normalized = normalizeWorkspacePlanArgument(argument);
|
|
1428
|
+
if (typeof normalized === "string")
|
|
1429
|
+
return normalized;
|
|
1430
|
+
const segments = await Promise.all(normalized.segments.map(async (segment) => {
|
|
1431
|
+
if (typeof segment === "string")
|
|
1432
|
+
return segment;
|
|
1433
|
+
const value = await resolveRef(segment.ref);
|
|
1434
|
+
return segment.encoding === "json-string"
|
|
1435
|
+
? JSON.stringify(value)
|
|
1436
|
+
: value;
|
|
1437
|
+
}));
|
|
1438
|
+
return segments.join("");
|
|
1439
|
+
}));
|
|
1440
|
+
return {
|
|
1441
|
+
files,
|
|
1442
|
+
env,
|
|
1443
|
+
flags,
|
|
1444
|
+
...(plan.systemPrompt === undefined
|
|
1445
|
+
? {}
|
|
1446
|
+
: { systemPrompt: plan.systemPrompt }),
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
function safeWorkspacePlanReceipt(plan, written, workspacePlanDigest = hashWorkspacePlan(plan)) {
|
|
1450
|
+
return {
|
|
1451
|
+
workspacePlanDigest,
|
|
1452
|
+
env: Object.fromEntries(Object.entries(plan.env).map(([name, value]) => [
|
|
1453
|
+
name,
|
|
1454
|
+
typeof value === "string" ? value : { ...value },
|
|
1455
|
+
])),
|
|
1456
|
+
flags: plan.flags.map(normalizeWorkspacePlanArgument),
|
|
1457
|
+
...(plan.systemPrompt === undefined
|
|
1458
|
+
? {}
|
|
1459
|
+
: { systemPrompt: plan.systemPrompt }),
|
|
1460
|
+
unsupported: plan.unsupported.map((entry) => ({ ...entry })),
|
|
1461
|
+
written: [...written],
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDir, options, publicPlanDigest = hashWorkspacePlan(publicPlan)) {
|
|
1062
1465
|
const workspaceRoot = resolve(workspaceDir);
|
|
1063
1466
|
const runtimeOnlyFiles = new Set((options.runtimeOnlyFiles ?? []).map(normalizeWorkspaceRelativePath));
|
|
1064
1467
|
const existingFiles = options.existingFiles ?? "allow-identical";
|
|
1065
1468
|
if (existingFiles !== "allow-identical" && existingFiles !== "reject") {
|
|
1066
1469
|
throw new Error(`Unsupported existing workspace file policy: ${existingFiles}`);
|
|
1067
1470
|
}
|
|
1068
|
-
const
|
|
1471
|
+
const resolvedFiles = resolvedPlan.files;
|
|
1472
|
+
const privateRuntimeFiles = Object.fromEntries(resolvedFiles
|
|
1473
|
+
.filter((file) => runtimeOnlyFiles.has(file.relPath))
|
|
1474
|
+
.map((file) => [file.relPath, file.content]));
|
|
1475
|
+
const files = resolvedFiles
|
|
1069
1476
|
.filter((file) => !runtimeOnlyFiles.has(file.relPath))
|
|
1070
1477
|
.map((file) => ({
|
|
1071
1478
|
...file,
|
|
@@ -1145,16 +1552,7 @@ export function applyWorkspacePlan(plan, workspaceDir, options = {}) {
|
|
|
1145
1552
|
}
|
|
1146
1553
|
throw error;
|
|
1147
1554
|
}
|
|
1148
|
-
const receipt =
|
|
1149
|
-
workspacePlanDigest: hashWorkspacePlan(plan),
|
|
1150
|
-
env: plan.env,
|
|
1151
|
-
flags: plan.flags,
|
|
1152
|
-
...(plan.systemPrompt === undefined
|
|
1153
|
-
? {}
|
|
1154
|
-
: { systemPrompt: plan.systemPrompt }),
|
|
1155
|
-
unsupported: plan.unsupported,
|
|
1156
|
-
written: files.map((file) => file.relPath),
|
|
1157
|
-
};
|
|
1555
|
+
const receipt = safeWorkspacePlanReceipt(publicPlan, files.map((file) => file.relPath), publicPlanDigest);
|
|
1158
1556
|
const cleanupErrors = [];
|
|
1159
1557
|
const temporaryPaths = [];
|
|
1160
1558
|
for (const entry of prepared) {
|
|
@@ -1171,7 +1569,17 @@ export function applyWorkspacePlan(plan, workspaceDir, options = {}) {
|
|
|
1171
1569
|
if (cleanupErrors.length > 0) {
|
|
1172
1570
|
throw new WorkspacePlanCleanupError(cleanupErrors, receipt, temporaryPaths);
|
|
1173
1571
|
}
|
|
1174
|
-
return
|
|
1572
|
+
return {
|
|
1573
|
+
receipt,
|
|
1574
|
+
spawn: {
|
|
1575
|
+
env: { ...resolvedPlan.env },
|
|
1576
|
+
flags: [...resolvedPlan.flags],
|
|
1577
|
+
...(resolvedPlan.systemPrompt === undefined
|
|
1578
|
+
? {}
|
|
1579
|
+
: { systemPrompt: resolvedPlan.systemPrompt }),
|
|
1580
|
+
},
|
|
1581
|
+
runtimeOnlyFiles: privateRuntimeFiles,
|
|
1582
|
+
};
|
|
1175
1583
|
}
|
|
1176
1584
|
function workspaceFileExists(pathValue) {
|
|
1177
1585
|
try {
|
|
@@ -1301,7 +1709,9 @@ function materializeExecutionControls(h, tools, permissions, modes, plan, add, u
|
|
|
1301
1709
|
for (const [tool, enabled] of nativeTools) {
|
|
1302
1710
|
rules[enabled ? "allow" : "deny"].push(tool);
|
|
1303
1711
|
}
|
|
1304
|
-
add(CLAUDE_SETTINGS_PATH,
|
|
1712
|
+
add(CLAUDE_SETTINGS_PATH, serializeWorkspacePlanJson({
|
|
1713
|
+
permissions: sealClaudePermissionRules(rules),
|
|
1714
|
+
}));
|
|
1305
1715
|
registerClaudeSettings(plan);
|
|
1306
1716
|
}
|
|
1307
1717
|
break;
|
|
@@ -1324,7 +1734,7 @@ function materializeExecutionControls(h, tools, permissions, modes, plan, add, u
|
|
|
1324
1734
|
const nativeTools = mapSupportedBooleanEntries(enforceableTools, openCodeToolName, (name) => unsupported("tools", `opencode: no verified native tool mapping for "${name}"`), "opencode tool");
|
|
1325
1735
|
const nativePermissions = mapSupportedPermissionEntries(enforceablePermissions, openCodePermissionName, (name) => unsupported("permissions", `opencode: no verified native permission mapping for "${name}"`), "opencode permission");
|
|
1326
1736
|
if (enforceableTools.length > 0 || permissionEntries.length > 0) {
|
|
1327
|
-
add("opencode.json",
|
|
1737
|
+
add("opencode.json", serializeWorkspacePlanJson({
|
|
1328
1738
|
$schema: "https://opencode.ai/config.json",
|
|
1329
1739
|
...(nativeTools.length > 0
|
|
1330
1740
|
? { tools: Object.fromEntries(nativeTools) }
|
|
@@ -1332,7 +1742,7 @@ function materializeExecutionControls(h, tools, permissions, modes, plan, add, u
|
|
|
1332
1742
|
...(nativePermissions.length > 0
|
|
1333
1743
|
? { permission: Object.fromEntries(nativePermissions) }
|
|
1334
1744
|
: {}),
|
|
1335
|
-
}
|
|
1745
|
+
}));
|
|
1336
1746
|
}
|
|
1337
1747
|
break;
|
|
1338
1748
|
}
|
|
@@ -1361,6 +1771,46 @@ function materializeExecutionControls(h, tools, permissions, modes, plan, add, u
|
|
|
1361
1771
|
}
|
|
1362
1772
|
break;
|
|
1363
1773
|
}
|
|
1774
|
+
case "pi": {
|
|
1775
|
+
const nativeTools = mapSupportedBooleanEntries(toolEntries, piToolId, (name) => unsupported("tools", `pi: tool name "${name}" is empty after trimming`), "pi tool");
|
|
1776
|
+
const nativePermissions = mapSupportedPermissionEntries(permissionEntries, piToolId, (name) => unsupported("permissions", `pi: permission name "${name}" is empty after trimming`), "pi permission");
|
|
1777
|
+
const enabled = new Set(nativeTools
|
|
1778
|
+
.filter(([, value]) => value)
|
|
1779
|
+
.map(([name]) => name)
|
|
1780
|
+
.sort(compareStrings));
|
|
1781
|
+
const excluded = new Set(nativeTools
|
|
1782
|
+
.filter(([, value]) => !value)
|
|
1783
|
+
.map(([name]) => name)
|
|
1784
|
+
.sort(compareStrings));
|
|
1785
|
+
for (const [name, policy] of nativePermissions) {
|
|
1786
|
+
if (typeof policy !== "string") {
|
|
1787
|
+
unsupported("permissions", `pi: conditional permission policy for "${name}" has no verified headless mapping`);
|
|
1788
|
+
continue;
|
|
1789
|
+
}
|
|
1790
|
+
if (policy === "ask") {
|
|
1791
|
+
unsupported("permissions", `pi: permission policy "ask" for "${name}" requires an interactive decision surface`);
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
if (policy === "allow") {
|
|
1795
|
+
if (excluded.has(name)) {
|
|
1796
|
+
throw new Error(`pi execution control "${name}" conflicts between disabled and allow`);
|
|
1797
|
+
}
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
if (enabled.has(name)) {
|
|
1801
|
+
throw new Error(`pi execution control "${name}" conflicts between enabled and deny`);
|
|
1802
|
+
}
|
|
1803
|
+
excluded.add(name);
|
|
1804
|
+
}
|
|
1805
|
+
if (enabled.size > 0 || excluded.size > 0) {
|
|
1806
|
+
add(PI_EXECUTION_CONTROL_EXTENSION_PATH, piExecutionControlExtensionSource([...enabled], [...excluded]));
|
|
1807
|
+
plan.flags.push("--extension", PI_EXECUTION_CONTROL_EXTENSION_PATH);
|
|
1808
|
+
}
|
|
1809
|
+
if (excluded.size > 0) {
|
|
1810
|
+
plan.flags.push("--exclude-tools", [...excluded].sort(compareStrings).join(","));
|
|
1811
|
+
}
|
|
1812
|
+
break;
|
|
1813
|
+
}
|
|
1364
1814
|
default:
|
|
1365
1815
|
if (toolEntries.length > 0) {
|
|
1366
1816
|
unsupported("tools", `${h}: no verified primary-process tool availability mapping`);
|
|
@@ -1373,6 +1823,56 @@ function materializeExecutionControls(h, tools, permissions, modes, plan, add, u
|
|
|
1373
1823
|
unsupported("modes", `${h}: resolve one named AgentProfile mode before materialization`);
|
|
1374
1824
|
}
|
|
1375
1825
|
}
|
|
1826
|
+
function piExecutionControlExtensionSource(enabledTools, excludedTools) {
|
|
1827
|
+
return [
|
|
1828
|
+
`const enabledTools = Object.freeze(${JSON.stringify(enabledTools)});`,
|
|
1829
|
+
`const excludedTools = Object.freeze(${JSON.stringify(excludedTools)});`,
|
|
1830
|
+
"",
|
|
1831
|
+
"function failProfile(message) {",
|
|
1832
|
+
' process.stderr.write("Pi profile execution controls failed: " + message + "\\n");',
|
|
1833
|
+
" process.exit(78);",
|
|
1834
|
+
"}",
|
|
1835
|
+
"",
|
|
1836
|
+
"function resolveTool(reference, available, required) {",
|
|
1837
|
+
" if (available.includes(reference)) return reference;",
|
|
1838
|
+
" const folded = reference.toLowerCase();",
|
|
1839
|
+
" const matches = available.filter((name) => name.toLowerCase() === folded);",
|
|
1840
|
+
" if (matches.length === 1) return matches[0];",
|
|
1841
|
+
" if (matches.length > 1) {",
|
|
1842
|
+
" failProfile('tool \"' + reference + '\" is ambiguous: ' + matches.join(\", \"));",
|
|
1843
|
+
" }",
|
|
1844
|
+
" if (required) {",
|
|
1845
|
+
" failProfile('requested tool \"' + reference + '\" is unavailable');",
|
|
1846
|
+
" }",
|
|
1847
|
+
" return undefined;",
|
|
1848
|
+
"}",
|
|
1849
|
+
"",
|
|
1850
|
+
"function applyControls(pi) {",
|
|
1851
|
+
" const available = pi.getAllTools().map((tool) => tool.name);",
|
|
1852
|
+
" for (const reference of excludedTools) {",
|
|
1853
|
+
" const remaining = resolveTool(reference, available, false);",
|
|
1854
|
+
" if (remaining !== undefined) {",
|
|
1855
|
+
" failProfile(",
|
|
1856
|
+
" 'excluded tool \"' + reference + '\" remains available as \"' + remaining +",
|
|
1857
|
+
" '\"; pass its exact registered Pi name',",
|
|
1858
|
+
" );",
|
|
1859
|
+
" }",
|
|
1860
|
+
" }",
|
|
1861
|
+
" const enabled = enabledTools.map((name) =>",
|
|
1862
|
+
" resolveTool(name, available, true),",
|
|
1863
|
+
" );",
|
|
1864
|
+
" const active = new Set(pi.getActiveTools());",
|
|
1865
|
+
" for (const name of enabled) active.add(name);",
|
|
1866
|
+
" pi.setActiveTools([...active]);",
|
|
1867
|
+
"}",
|
|
1868
|
+
"",
|
|
1869
|
+
"export default function applyProfileExecutionControls(pi) {",
|
|
1870
|
+
' pi.on("before_agent_start", () => applyControls(pi));',
|
|
1871
|
+
' pi.on("turn_start", () => applyControls(pi));',
|
|
1872
|
+
"}",
|
|
1873
|
+
"",
|
|
1874
|
+
].join("\n");
|
|
1875
|
+
}
|
|
1376
1876
|
function normalizedControlName(name) {
|
|
1377
1877
|
return name.replace(/[^a-zA-Z0-9_]/g, "").toLowerCase();
|
|
1378
1878
|
}
|
|
@@ -1457,6 +1957,24 @@ function openCodePermissionName(name) {
|
|
|
1457
1957
|
? normalized
|
|
1458
1958
|
: undefined;
|
|
1459
1959
|
}
|
|
1960
|
+
const PI_EXECUTION_CONTROL_EXTENSION_PATH = ".tangle/pi-execution-controls.ts";
|
|
1961
|
+
/**
|
|
1962
|
+
* Translate only portable spellings whose Pi equivalent differs.
|
|
1963
|
+
*
|
|
1964
|
+
* Every other exact name is resolved against Pi's live registry, keeping
|
|
1965
|
+
* extension and MCP tools profile-configurable without a static catalog.
|
|
1966
|
+
*/
|
|
1967
|
+
function piToolId(name) {
|
|
1968
|
+
const trimmed = name.trim();
|
|
1969
|
+
if (trimmed.length === 0)
|
|
1970
|
+
return undefined;
|
|
1971
|
+
const portableName = trimmed.toLowerCase();
|
|
1972
|
+
if (portableName === "shell")
|
|
1973
|
+
return "bash";
|
|
1974
|
+
if (portableName === "glob")
|
|
1975
|
+
return "find";
|
|
1976
|
+
return trimmed;
|
|
1977
|
+
}
|
|
1460
1978
|
function mapSupportedBooleanEntries(entries, mapName, onUnsupported, label) {
|
|
1461
1979
|
return mapControlEntries(entries, mapName, onUnsupported, (left, right, nativeName) => {
|
|
1462
1980
|
if (left !== right)
|
|
@@ -1562,7 +2080,7 @@ function materializeMcp(h, mcp, plan, add, unsupported) {
|
|
|
1562
2080
|
};
|
|
1563
2081
|
}
|
|
1564
2082
|
const relPath = ".tangle/claude-mcp.json";
|
|
1565
|
-
add(relPath,
|
|
2083
|
+
add(relPath, serializeWorkspacePlanJson({ mcpServers: servers }));
|
|
1566
2084
|
plan.flags.push("--mcp-config", relPath, "--strict-mcp-config");
|
|
1567
2085
|
break;
|
|
1568
2086
|
}
|
|
@@ -1576,21 +2094,21 @@ function materializeMcp(h, mcp, plan, add, unsupported) {
|
|
|
1576
2094
|
unsupported("mcp", `codex: remote MCP server "${name}" uses unsupported SSE transport`);
|
|
1577
2095
|
continue;
|
|
1578
2096
|
}
|
|
1579
|
-
servers.push(`${tomlStr(name)} =
|
|
2097
|
+
servers.push(concatWorkspacePlanArguments(`${tomlStr(name)} = `, codexMcpServerToml(name, server, plan)));
|
|
1580
2098
|
}
|
|
1581
2099
|
if (servers.length > 0) {
|
|
1582
2100
|
// Runtime overrides do not require persisted project trust. Header values
|
|
1583
2101
|
// ride environment variables so credentials never appear in argv.
|
|
1584
|
-
plan.flags.push("-c",
|
|
2102
|
+
plan.flags.push("-c", concatWorkspacePlanArguments("mcp_servers={ ", joinWorkspacePlanArguments(servers, ", "), " }"));
|
|
1585
2103
|
}
|
|
1586
2104
|
break;
|
|
1587
2105
|
}
|
|
1588
2106
|
case "opencode":
|
|
1589
2107
|
// opencode.json "mcp" block (local|remote).
|
|
1590
|
-
add("opencode.json",
|
|
2108
|
+
add("opencode.json", serializeWorkspacePlanJson({
|
|
1591
2109
|
$schema: "https://opencode.ai/config.json",
|
|
1592
2110
|
mcp: openCodeMcpConfig(mcp),
|
|
1593
|
-
}
|
|
2111
|
+
}));
|
|
1594
2112
|
break;
|
|
1595
2113
|
case "gemini": {
|
|
1596
2114
|
const servers = {};
|
|
@@ -1609,7 +2127,7 @@ function materializeMcp(h, mcp, plan, add, unsupported) {
|
|
|
1609
2127
|
...(server.cwd ? { cwd: server.cwd } : {}),
|
|
1610
2128
|
};
|
|
1611
2129
|
}
|
|
1612
|
-
add(".gemini/settings.json",
|
|
2130
|
+
add(".gemini/settings.json", serializeWorkspacePlanJson({ mcpServers: servers }));
|
|
1613
2131
|
break;
|
|
1614
2132
|
}
|
|
1615
2133
|
case "kimi-code": {
|
|
@@ -1619,7 +2137,7 @@ function materializeMcp(h, mcp, plan, add, unsupported) {
|
|
|
1619
2137
|
name,
|
|
1620
2138
|
kimiMcpServer(server),
|
|
1621
2139
|
]));
|
|
1622
|
-
add(rel,
|
|
2140
|
+
add(rel, serializeWorkspacePlanJson({ mcpServers: servers }));
|
|
1623
2141
|
plan.flags.push("--mcp-config-file", rel);
|
|
1624
2142
|
break;
|
|
1625
2143
|
}
|
|
@@ -1662,7 +2180,7 @@ function codexMcpServerToml(name, server, plan) {
|
|
|
1662
2180
|
}
|
|
1663
2181
|
fields.push(`command = ${tomlStr(server.command)}`);
|
|
1664
2182
|
if (server.args?.length) {
|
|
1665
|
-
fields.push(
|
|
2183
|
+
fields.push(concatWorkspacePlanArguments("args = [", joinWorkspacePlanArguments(server.args.map(tomlProfileConfigValue), ", "), "]"));
|
|
1666
2184
|
}
|
|
1667
2185
|
if (server.cwd)
|
|
1668
2186
|
fields.push(`cwd = ${tomlStr(server.cwd)}`);
|
|
@@ -1674,14 +2192,16 @@ function codexMcpServerToml(name, server, plan) {
|
|
|
1674
2192
|
fields.push(`env_vars = [${envNames.map(tomlStr).join(", ")}]`);
|
|
1675
2193
|
}
|
|
1676
2194
|
}
|
|
1677
|
-
return
|
|
2195
|
+
return concatWorkspacePlanArguments("{ ", joinWorkspacePlanArguments(fields, ", "), " }");
|
|
1678
2196
|
}
|
|
1679
2197
|
function setPlanEnvironment(plan, name, value) {
|
|
2198
|
+
const normalized = typeof value === "string" ? value : workspacePlanConfigValue(value);
|
|
1680
2199
|
const previous = plan.env[name];
|
|
1681
|
-
if (previous !== undefined &&
|
|
2200
|
+
if (previous !== undefined &&
|
|
2201
|
+
JSON.stringify(previous) !== JSON.stringify(normalized)) {
|
|
1682
2202
|
throw new Error(`profile environment values disagree for ${name}`);
|
|
1683
2203
|
}
|
|
1684
|
-
plan.env[name] =
|
|
2204
|
+
plan.env[name] = normalized;
|
|
1685
2205
|
}
|
|
1686
2206
|
function kimiMcpServer(server) {
|
|
1687
2207
|
if (server.url) {
|
|
@@ -1746,14 +2266,14 @@ function materializeHooks(h, hooks, add, unsupported, plan) {
|
|
|
1746
2266
|
hooks: [
|
|
1747
2267
|
{
|
|
1748
2268
|
type: "command",
|
|
1749
|
-
command: materializedHookCommand(event, c, add),
|
|
2269
|
+
command: materializedHookCommand(event, c, add, plan),
|
|
1750
2270
|
...(c.timeoutMs ? { timeout: timeoutSeconds(c.timeoutMs) } : {}),
|
|
1751
2271
|
...(c.blocking === false ? { async: true } : {}),
|
|
1752
2272
|
},
|
|
1753
2273
|
],
|
|
1754
2274
|
}));
|
|
1755
2275
|
}
|
|
1756
|
-
add(CLAUDE_SETTINGS_PATH,
|
|
2276
|
+
add(CLAUDE_SETTINGS_PATH, serializeWorkspacePlanJson({ hooks: settingsHooks }));
|
|
1757
2277
|
registerClaudeSettings(plan);
|
|
1758
2278
|
break;
|
|
1759
2279
|
}
|
|
@@ -1775,13 +2295,13 @@ function materializeHooks(h, hooks, add, unsupported, plan) {
|
|
|
1775
2295
|
hooks: [
|
|
1776
2296
|
{
|
|
1777
2297
|
type: "command",
|
|
1778
|
-
command: materializedHookCommand(event, c, add),
|
|
2298
|
+
command: materializedHookCommand(event, c, add, plan),
|
|
1779
2299
|
...(c.timeoutMs ? { timeout: c.timeoutMs } : {}),
|
|
1780
2300
|
},
|
|
1781
2301
|
],
|
|
1782
2302
|
}));
|
|
1783
2303
|
}
|
|
1784
|
-
add(".gemini/settings.json",
|
|
2304
|
+
add(".gemini/settings.json", serializeWorkspacePlanJson({ hooks: settingsHooks }));
|
|
1785
2305
|
break;
|
|
1786
2306
|
}
|
|
1787
2307
|
case "codex": {
|
|
@@ -1797,7 +2317,7 @@ function materializeHooks(h, hooks, add, unsupported, plan) {
|
|
|
1797
2317
|
const entries = cmds.map((command) => {
|
|
1798
2318
|
const handler = [
|
|
1799
2319
|
`type = ${tomlStr("command")}`,
|
|
1800
|
-
`command = ${tomlStr(materializedHookCommand(event, command, add))}`,
|
|
2320
|
+
`command = ${tomlStr(materializedHookCommand(event, command, add, plan))}`,
|
|
1801
2321
|
...(command.timeoutMs
|
|
1802
2322
|
? [`timeout = ${timeoutSeconds(command.timeoutMs)}`]
|
|
1803
2323
|
: []),
|
|
@@ -1906,7 +2426,7 @@ function hookEventSupported(harness, event) {
|
|
|
1906
2426
|
function timeoutSeconds(timeoutMs) {
|
|
1907
2427
|
return Math.max(1, Math.ceil(timeoutMs / 1000));
|
|
1908
2428
|
}
|
|
1909
|
-
function materializedHookCommand(event, command, add) {
|
|
2429
|
+
function materializedHookCommand(event, command, add, plan) {
|
|
1910
2430
|
if (!command.env || Object.keys(command.env).length === 0) {
|
|
1911
2431
|
return command.command;
|
|
1912
2432
|
}
|
|
@@ -1916,9 +2436,50 @@ function materializedHookCommand(event, command, add) {
|
|
|
1916
2436
|
env: Object.fromEntries(Object.entries(command.env).sort(([left], [right]) => compareStrings(left, right))),
|
|
1917
2437
|
}).slice("sha256:".length);
|
|
1918
2438
|
const path = `.tangle/profile-hooks/${wrapperId}.mjs`;
|
|
1919
|
-
|
|
2439
|
+
const envSources = Object.fromEntries(Object.entries(command.env)
|
|
2440
|
+
.sort(([left], [right]) => compareStrings(left, right))
|
|
2441
|
+
.map(([name, value]) => {
|
|
2442
|
+
const sourceName = `TANGLE_PROFILE_HOOK_${sha256(`${wrapperId}\0${name}`)
|
|
2443
|
+
.slice(0, 24)
|
|
2444
|
+
.toUpperCase()}`;
|
|
2445
|
+
setPlanEnvironment(plan, sourceName, value);
|
|
2446
|
+
return [name, sourceName];
|
|
2447
|
+
}));
|
|
2448
|
+
add(path, hookWrapperSourceFromEnvironment("/bin/sh", ["-c", command.command], envSources));
|
|
1920
2449
|
return `node ${path}`;
|
|
1921
2450
|
}
|
|
2451
|
+
function hookWrapperSourceFromEnvironment(executable, args, envSources) {
|
|
2452
|
+
return [
|
|
2453
|
+
'import { spawnSync } from "node:child_process"',
|
|
2454
|
+
`const executable = ${JSON.stringify(executable)}`,
|
|
2455
|
+
`const args = ${JSON.stringify(args)}`,
|
|
2456
|
+
`const envSources = ${JSON.stringify(envSources)}`,
|
|
2457
|
+
"const extraEnv = {}",
|
|
2458
|
+
"for (const [name, source] of Object.entries(envSources)) {",
|
|
2459
|
+
" const value = process.env[source]",
|
|
2460
|
+
" if (value === undefined) {",
|
|
2461
|
+
' console.error("missing materialized hook environment " + source)',
|
|
2462
|
+
" process.exit(127)",
|
|
2463
|
+
" }",
|
|
2464
|
+
" extraEnv[name] = value",
|
|
2465
|
+
"}",
|
|
2466
|
+
"const result = spawnSync(executable, args, {",
|
|
2467
|
+
' stdio: "inherit",',
|
|
2468
|
+
" env: { ...process.env, ...extraEnv },",
|
|
2469
|
+
" shell: false,",
|
|
2470
|
+
"})",
|
|
2471
|
+
"if (result.error) {",
|
|
2472
|
+
" console.error(result.error.message)",
|
|
2473
|
+
" process.exit(127)",
|
|
2474
|
+
"}",
|
|
2475
|
+
"if (result.signal) {",
|
|
2476
|
+
' console.error("hook terminated by " + result.signal)',
|
|
2477
|
+
" process.exit(1)",
|
|
2478
|
+
"}",
|
|
2479
|
+
"process.exit(result.status ?? 1)",
|
|
2480
|
+
"",
|
|
2481
|
+
].join("\n");
|
|
2482
|
+
}
|
|
1922
2483
|
function hookWrapperSource(executable, args, env) {
|
|
1923
2484
|
return [
|
|
1924
2485
|
'import { spawnSync } from "node:child_process"',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-profile-materialize",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
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",
|
|
@@ -20,16 +20,17 @@
|
|
|
20
20
|
"dist"
|
|
21
21
|
],
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@tangle-network/agent-interface": "0.
|
|
23
|
+
"@tangle-network/agent-interface": "0.38.0",
|
|
24
24
|
"@types/node": "25.6.0",
|
|
25
25
|
"typescript": "^6.0.3",
|
|
26
26
|
"vitest": "^4.1.10"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"@tangle-network/agent-interface": ">=0.
|
|
29
|
+
"@tangle-network/agent-interface": ">=0.38.0 <0.41.0"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsc -p tsconfig.json",
|
|
33
|
+
"check-types": "tsc --noEmit",
|
|
33
34
|
"clean": "rm -rf dist",
|
|
34
35
|
"test": "vitest run",
|
|
35
36
|
"test:watch": "vitest"
|