@tangle-network/agent-profile-materialize 0.23.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,5 +20,5 @@ export declare function assertPrivateManagedRoot(root: string): void;
20
20
  * The caller must keep the returned tree while an admitted turn uses it and
21
21
  * dispose it after that turn, so a later binding cannot expose old secrets.
22
22
  */
23
- export declare function prepareManagedWorkspacePlan(plan: WorkspacePlan, managedRoot: string, sharedWorkspaceDir: string, secretProvider: AgentProfileSecretProvider, options?: Pick<ApplyWorkspacePlanOptions, "ownership" | "runtimeOnlyFiles">): Promise<PreparedManagedWorkspace>;
23
+ export declare function prepareManagedWorkspacePlan(plan: WorkspacePlan, managedRoot: string, sharedWorkspaceDir: string, secretProvider: AgentProfileSecretProvider, options?: Pick<ApplyWorkspacePlanOptions, "ownership" | "runtimeOnlyFiles" | "secretFileSlotEnvironment">): Promise<PreparedManagedWorkspace>;
24
24
  export declare function lstatIfPresent(path: string): ReturnType<typeof lstatSync> | null;
@@ -9,7 +9,7 @@ export function applyWorkspacePlan(plan, workspaceDir, options = {}) {
9
9
  */
10
10
  export async function applyWorkspacePlanWithSecretProvider(plan, workspaceDir, secretProvider, options = {}) {
11
11
  const planDigest = hashWorkspacePlan(plan);
12
- const resolved = await resolvePrivateWorkspacePlan(plan, secretProvider);
12
+ const resolved = await resolvePrivateWorkspacePlan(plan, secretProvider, options.secretFileSlotEnvironment);
13
13
  return applyResolvedWorkspacePlan(plan, resolved, workspaceDir, options, planDigest);
14
14
  }
15
15
  function secretRefLabel(ref) {
@@ -78,13 +78,27 @@ function resolvePublicWorkspacePlan(plan) {
78
78
  ...systemPromptBindingField(plan),
79
79
  };
80
80
  }
81
- async function resolvePrivateWorkspacePlan(plan, secretProvider) {
81
+ /**
82
+ * OpenCode substitutes `{env:NAME}` textually into its config before parsing
83
+ * it, so only a value that cannot break out of a JSON string or start another
84
+ * substitution may be referenced; any other value is written in place.
85
+ */
86
+ const SUBSTITUTABLE_SECRET_VALUE = /^[\x20-\x7e]+$/;
87
+ const UNSUBSTITUTABLE_SECRET_CHARACTERS = /["\\{}]/;
88
+ function secretSlotEnvironmentName(ref) {
89
+ const digest = createHash("sha256")
90
+ .update(ref.key, "utf8")
91
+ .digest("hex")
92
+ .slice(0, 24)
93
+ .toUpperCase();
94
+ return `TANGLE_PROFILE_SECRET_${digest}`;
95
+ }
96
+ async function resolvePrivateWorkspacePlan(plan, secretProvider, fileSlotEnvironment) {
82
97
  if (!secretProvider || typeof secretProvider.get !== "function") {
83
98
  throw new Error("AgentProfileSecretProvider.get is required");
84
99
  }
85
100
  const pending = new Map();
86
- const resolveRef = async (input) => {
87
- const ref = agentProfileSecretRefSchema.parse(input);
101
+ const resolveRawRef = (ref) => {
88
102
  let raw = pending.get(ref.key);
89
103
  if (!raw) {
90
104
  raw = (async () => {
@@ -99,16 +113,33 @@ async function resolvePrivateWorkspacePlan(plan, secretProvider) {
99
113
  })();
100
114
  pending.set(ref.key, raw);
101
115
  }
102
- const value = await raw;
116
+ return raw;
117
+ };
118
+ const resolveRef = async (input) => {
119
+ const ref = agentProfileSecretRefSchema.parse(input);
120
+ const value = await resolveRawRef(ref);
103
121
  return ref.format === "bearer" ? `Bearer ${value}` : value;
104
122
  };
123
+ const resolveFileSlot = async (input) => {
124
+ const ref = agentProfileSecretRefSchema.parse(input);
125
+ const value = await resolveRawRef(ref);
126
+ if (!fileSlotEnvironment ||
127
+ !SUBSTITUTABLE_SECRET_VALUE.test(value) ||
128
+ UNSUBSTITUTABLE_SECRET_CHARACTERS.test(value)) {
129
+ return ref.format === "bearer" ? `Bearer ${value}` : value;
130
+ }
131
+ const name = secretSlotEnvironmentName(ref);
132
+ fileSlotEnvironment[name] = value;
133
+ return ref.format === "bearer" ? `Bearer {env:${name}}` : `{env:${name}}`;
134
+ };
105
135
  const files = await Promise.all(normalizePlanFiles(plan.files).map(async (file) => {
106
136
  if (file.secretSlots.length === 0)
107
137
  return file;
138
+ // Generated native config files with secret slots are JSON objects.
108
139
  const parsed = JSON.parse(file.content);
109
140
  const values = await Promise.all(file.secretSlots.map(async (slot) => ({
110
141
  path: slot.path,
111
- value: await resolveRef(slot.ref),
142
+ value: await resolveFileSlot(slot.ref),
112
143
  })));
113
144
  for (const resolved of values) {
114
145
  setValueAtJsonPath(parsed, resolved.path, resolved.value);
@@ -145,6 +176,7 @@ async function resolvePrivateWorkspacePlan(plan, secretProvider) {
145
176
  ...systemPromptBindingField(plan),
146
177
  };
147
178
  }
179
+ import { createHash } from "node:crypto";
148
180
  import { agentProfileSecretRefSchema } from "@tangle-network/agent-interface";
149
181
  import { hashWorkspacePlan } from "./plan-digest.js";
150
182
  import { normalizePlanFiles, normalizeWorkspacePlanArgument, } from "./plan-normalize.js";
@@ -320,6 +320,15 @@ export interface ApplyWorkspacePlanOptions {
320
320
  * refusal.
321
321
  */
322
322
  appliedPlanDigest?: string;
323
+ /**
324
+ * Receives the values of secrets that JSON file slots reference instead of
325
+ * containing. When set, a slot whose resolved value is safe to substitute
326
+ * is written as `{env:NAME}` (OpenCode config substitution) and the value
327
+ * lands here under NAME, so file bytes and every digest over them stay
328
+ * identical when only a secret's value changes. The caller must add these
329
+ * entries to the launch environment after digesting the launch.
330
+ */
331
+ secretFileSlotEnvironment?: Record<string, string>;
323
332
  }
324
333
  import type { AgentProfileSecretRef } from "@tangle-network/agent-interface";
325
334
  import type { HarnessId } from "./harness.js";
@@ -42,7 +42,10 @@ export async function prepareSecretConcurrentProfileRevision(input) {
42
42
  }
43
43
  assertWorkspacePlanSupported(plan);
44
44
  assertLaunchRoot(input.launchRoot);
45
- const prepared = await prepareManagedWorkspacePlan(plan, input.managedRoot, input.sharedWorkspaceDir, input.secretProvider, input.options);
45
+ // Secret values stay out of the revision: file slots reference them, so an
46
+ // unchanged profile with a rotated per-turn secret reuses this revision.
47
+ const secretEnvironment = {};
48
+ const prepared = await prepareManagedWorkspacePlan(plan, input.managedRoot, input.sharedWorkspaceDir, input.secretProvider, { ...input.options, secretFileSlotEnvironment: secretEnvironment });
46
49
  try {
47
50
  // XDG_CONFIG_HOME is bound to this executor-owned tree at native launch.
48
51
  // An agent-owned HOME may hold SQLite state, never native authority.
@@ -82,10 +85,14 @@ export async function prepareSecretConcurrentProfileRevision(input) {
82
85
  prepared.dispose();
83
86
  }
84
87
  }
88
+ // Added after launchDigest so the digest and receipt never cover a value.
85
89
  return {
86
90
  directory,
87
91
  manifest: prepared.manifest,
88
- application: { ...prepared.application, spawn },
92
+ application: {
93
+ ...prepared.application,
94
+ spawn: { ...spawn, env: { ...spawn.env, ...secretEnvironment } },
95
+ },
89
96
  };
90
97
  }
91
98
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.23.0",
3
+ "version": "0.23.1",
4
4
  "description": "Materialize an AgentProfile into the native per-harness workspace files (the one shared materializer for cli-bridge, agent-dev-container, and VB).",
5
5
  "type": "module",
6
6
  "repository": {