@tangle-network/agent-profile-materialize 0.26.3 → 0.26.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.
@@ -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" | "secretFileSlotEnvironment">): Promise<PreparedManagedWorkspace>;
23
+ export declare function prepareManagedWorkspacePlan(plan: WorkspacePlan, managedRoot: string, sharedWorkspaceDir: string, secretProvider: AgentProfileSecretProvider, options?: Pick<ApplyWorkspacePlanOptions, "ownership" | "runtimeOnlyFiles" | "secretFileSlotsByReference">): 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, options.secretFileSlotEnvironment);
12
+ const resolved = await resolvePrivateWorkspacePlan(plan, secretProvider, options.secretFileSlotsByReference === true);
13
13
  return applyResolvedWorkspacePlan(plan, resolved, workspaceDir, options, planDigest);
14
14
  }
15
15
  function secretRefLabel(ref) {
@@ -81,7 +81,7 @@ function resolvePublicWorkspacePlan(plan) {
81
81
  /**
82
82
  * OpenCode substitutes `{env:NAME}` textually into its config before parsing
83
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.
84
+ * substitution may stand behind a reference.
85
85
  */
86
86
  const SUBSTITUTABLE_SECRET_VALUE = /^[\x20-\x7e]+$/;
87
87
  const UNSUBSTITUTABLE_SECRET_CHARACTERS = /["\\{}]/;
@@ -93,7 +93,7 @@ function secretSlotEnvironmentName(ref) {
93
93
  .toUpperCase();
94
94
  return `TANGLE_PROFILE_SECRET_${digest}`;
95
95
  }
96
- async function resolvePrivateWorkspacePlan(plan, secretProvider, fileSlotEnvironment) {
96
+ async function resolvePrivateWorkspacePlan(plan, secretProvider, fileSlotsByReference) {
97
97
  if (!secretProvider || typeof secretProvider.get !== "function") {
98
98
  throw new Error("AgentProfileSecretProvider.get is required");
99
99
  }
@@ -123,13 +123,16 @@ async function resolvePrivateWorkspacePlan(plan, secretProvider, fileSlotEnviron
123
123
  const resolveFileSlot = async (input) => {
124
124
  const ref = agentProfileSecretRefSchema.parse(input);
125
125
  const value = await resolveRawRef(ref);
126
- if (!fileSlotEnvironment ||
127
- !SUBSTITUTABLE_SECRET_VALUE.test(value) ||
128
- UNSUBSTITUTABLE_SECRET_CHARACTERS.test(value)) {
126
+ if (!fileSlotsByReference) {
129
127
  return ref.format === "bearer" ? `Bearer ${value}` : value;
130
128
  }
129
+ // Never write the value in place: the file is readable by the process
130
+ // that loads it, and so by every tool that process runs.
131
+ if (!SUBSTITUTABLE_SECRET_VALUE.test(value) ||
132
+ UNSUBSTITUTABLE_SECRET_CHARACTERS.test(value)) {
133
+ throw new Error(`AgentProfile secret ${secretRefLabel(ref)} has a value that cannot be referenced from a secret OpenCode config: it may hold only printable ASCII without quotes, backslashes or braces`);
134
+ }
131
135
  const name = secretSlotEnvironmentName(ref);
132
- fileSlotEnvironment[name] = value;
133
136
  return ref.format === "bearer" ? `Bearer {env:${name}}` : `{env:${name}}`;
134
137
  };
135
138
  const files = await Promise.all(normalizePlanFiles(plan.files).map(async (file) => {
@@ -321,14 +321,13 @@ export interface ApplyWorkspacePlanOptions {
321
321
  */
322
322
  appliedPlanDigest?: string;
323
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.
324
+ * Write every JSON file secret slot as an `{env:NAME}` reference (OpenCode
325
+ * config substitution) and never its value, so file bytes and every digest
326
+ * over them stay identical when only a secret's value changes. The value is
327
+ * not returned: a native process that held it would hand it to every tool
328
+ * it runs. A value that could not be substituted safely is refused.
330
329
  */
331
- secretFileSlotEnvironment?: Record<string, string>;
330
+ secretFileSlotsByReference?: true;
332
331
  }
333
332
  import type { AgentProfileSecretRef } from "@tangle-network/agent-interface";
334
333
  import type { HarnessId } from "./harness.js";
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import { assertLaunchRoot, listRevisionFiles, makeStageWritable, prepareExplicitOpenCodePlan, } from "./managed-workspace.js";
6
6
  import { bindConcurrentLaunch } from "./managed-workspace-bind.js";
7
7
  import { lstatIfPresent, prepareManagedWorkspacePlan, } from "./managed-workspace-stage.js";
8
+ import { normalizeWorkspacePlanArgument } from "./plan-normalize.js";
8
9
  import { assertWorkspacePlanSupported } from "./plan-support.js";
9
10
  /**
10
11
  * Publish a resolved revision for exactly one native subprocess UID. The
@@ -52,12 +53,20 @@ export async function prepareSecretConcurrentProfileRevision(input) {
52
53
  if (plan.files.some((file) => file.relPath.startsWith(".config/opencode/"))) {
53
54
  throw new Error("Secret OpenCode reserves its native config directory");
54
55
  }
56
+ // OpenCode and every process it spawns run as the session UID, so a value in
57
+ // its environment, argv or config is readable by the agent's own shell
58
+ // (`/proc/<pid>/environ`, `/proc/<pid>/cmdline`, `cat`). No profile secret
59
+ // value may reach the launch: config slots stay references whose values the
60
+ // launch never receives (#11875).
61
+ if (Object.values(plan.env).some((value) => typeof value !== "string") ||
62
+ plan.flags.some((argument) => typeof normalizeWorkspacePlanArgument(argument) !== "string")) {
63
+ throw new Error("Secret OpenCode refuses a profile secret in its launch environment or arguments");
64
+ }
55
65
  assertWorkspacePlanSupported(plan);
56
66
  assertLaunchRoot(input.launchRoot);
57
- // Secret values stay out of the revision: file slots reference them, so an
58
- // unchanged profile with a rotated per-turn secret reuses this revision.
59
- const secretEnvironment = {};
60
- const prepared = await prepareManagedWorkspacePlan(plan, input.managedRoot, input.sharedWorkspaceDir, input.secretProvider, { ...input.options, secretFileSlotEnvironment: secretEnvironment });
67
+ // File slots reference their secrets, so an unchanged profile with a rotated
68
+ // per-turn secret reuses this revision.
69
+ const prepared = await prepareManagedWorkspacePlan(plan, input.managedRoot, input.sharedWorkspaceDir, input.secretProvider, { ...input.options, secretFileSlotsByReference: true });
61
70
  try {
62
71
  // XDG_CONFIG_HOME is bound to this executor-owned tree at native launch.
63
72
  // An agent-owned HOME may hold SQLite state, never native authority.
@@ -112,14 +121,10 @@ export async function prepareSecretConcurrentProfileRevision(input) {
112
121
  prepared.dispose();
113
122
  }
114
123
  }
115
- // Added after launchDigest so the digest and receipt never cover a value.
116
124
  return {
117
125
  directory,
118
126
  manifest: prepared.manifest,
119
- application: {
120
- ...prepared.application,
121
- spawn: { ...spawn, env: { ...spawn.env, ...secretEnvironment } },
122
- },
127
+ application: { ...prepared.application, spawn },
123
128
  };
124
129
  }
125
130
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.26.3",
3
+ "version": "0.26.4",
4
4
  "description": "Materialize an AgentProfile into the native per-harness workspace files (the one shared materializer for cli-bridge, agent-container, and VB).",
5
5
  "type": "module",
6
6
  "repository": {
@@ -35,8 +35,8 @@
35
35
  "@tangle-network/agent-interface": "^3.0.0 || ^4.0.0 || ^5.0.0"
36
36
  },
37
37
  "tangleRelease": {
38
- "sourceSha": "c7971c78f69c191b991da10bcb17ac1a30ab66b2",
39
- "mergedAt": "2026-10-19T02:02:40-06:00"
38
+ "sourceSha": "7375b922752bb85e4e7541a519be661c363b115f",
39
+ "mergedAt": "2026-10-19T05:43:15-06:00"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsc -p tsconfig.json",