flowviant 0.116.0 → 0.124.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.
Files changed (161) hide show
  1. package/README.md +66 -12
  2. package/bin/cli.mjs +50 -17
  3. package/bin/lib/admission.mjs +7 -14
  4. package/bin/lib/agentAsk.mjs +123 -0
  5. package/bin/lib/agentBrief.mjs +36 -0
  6. package/bin/lib/agentFootprint.mjs +76 -0
  7. package/bin/lib/agentInternalWait.mjs +83 -0
  8. package/bin/lib/agentPlan.mjs +134 -31
  9. package/bin/lib/agentPrerequisiteContract.mjs +43 -0
  10. package/bin/lib/agentPublish.mjs +12 -13
  11. package/bin/lib/agentTaskKinds.mjs +5 -8
  12. package/bin/lib/agySessions.mjs +3 -3
  13. package/bin/lib/artifactContracts.mjs +91 -20
  14. package/bin/lib/artifactIteration.mjs +182 -0
  15. package/bin/lib/artifactPreview.mjs +92 -18
  16. package/bin/lib/artifactRecovery.mjs +36 -0
  17. package/bin/lib/artifactReporter.mjs +194 -22
  18. package/bin/lib/artifacts.mjs +70 -63
  19. package/bin/lib/authproxy.mjs +2 -1
  20. package/bin/lib/childEnv.mjs +9 -4
  21. package/bin/lib/claude.mjs +10 -20
  22. package/bin/lib/claudeAuth.mjs +15 -5
  23. package/bin/lib/claudePersonal.mjs +71 -6
  24. package/bin/lib/claudePosture.mjs +227 -54
  25. package/bin/lib/claudeSessions.mjs +7 -7
  26. package/bin/lib/claudeStream.mjs +27 -13
  27. package/bin/lib/cliLimit.mjs +2 -2
  28. package/bin/lib/codexHome.mjs +17 -0
  29. package/bin/lib/codexPersonal.mjs +44 -3
  30. package/bin/lib/codexProjectRead.mjs +63 -0
  31. package/bin/lib/codexRelay.mjs +21 -5
  32. package/bin/lib/codexResearch.mjs +193 -0
  33. package/bin/lib/commandSpecs.mjs +6 -0
  34. package/bin/lib/config.mjs +37 -24
  35. package/bin/lib/conversationIds.mjs +51 -0
  36. package/bin/lib/credentials.mjs +2 -7
  37. package/bin/lib/daemonLocks.mjs +90 -0
  38. package/bin/lib/daemonState.mjs +5 -5
  39. package/bin/lib/deploy.mjs +6 -9
  40. package/bin/lib/deployCheckout.mjs +26 -22
  41. package/bin/lib/deployRunner.mjs +54 -18
  42. package/bin/lib/deployWire.mjs +32 -23
  43. package/bin/lib/desktopContract.mjs +6 -5
  44. package/bin/lib/discoveryContract.mjs +19 -0
  45. package/bin/lib/env.mjs +104 -35
  46. package/bin/lib/fleet.mjs +53 -13
  47. package/bin/lib/fleetPost.mjs +172 -25
  48. package/bin/lib/fleetReports.mjs +172 -171
  49. package/bin/lib/fleetStartup.mjs +6 -6
  50. package/bin/lib/fleetWire.mjs +42 -0
  51. package/bin/lib/git.mjs +111 -61
  52. package/bin/lib/gitMergeIdentity.mjs +2 -8
  53. package/bin/lib/grant.mjs +23 -7
  54. package/bin/lib/holder.mjs +108 -81
  55. package/bin/lib/hooks/mcpAllow.mjs +60 -0
  56. package/bin/lib/hooks/readGuard.mjs +54 -11
  57. package/bin/lib/imageArtifact.mjs +74 -0
  58. package/bin/lib/install.mjs +80 -33
  59. package/bin/lib/instance.mjs +36 -83
  60. package/bin/lib/intakeDraft.mjs +4 -1
  61. package/bin/lib/knowledgeFileSync.mjs +31 -0
  62. package/bin/lib/knowledgeFiles.mjs +28 -83
  63. package/bin/lib/landed.mjs +22 -29
  64. package/bin/lib/launchCommand.mjs +91 -0
  65. package/bin/lib/listeners.mjs +0 -5
  66. package/bin/lib/localSessions.mjs +52 -39
  67. package/bin/lib/login.mjs +22 -12
  68. package/bin/lib/loginPath.mjs +22 -17
  69. package/bin/lib/machineListing.mjs +6 -3
  70. package/bin/lib/maintenance.mjs +47 -0
  71. package/bin/lib/maintenanceInventory.mjs +39 -0
  72. package/bin/lib/mcp-cli.mjs +9 -10
  73. package/bin/lib/mcpInstall.mjs +163 -0
  74. package/bin/lib/mcpInventory.mjs +119 -0
  75. package/bin/lib/noWindowEnv.mjs +23 -3
  76. package/bin/lib/ownedDirs.mjs +99 -0
  77. package/bin/lib/pinnedCapture.mjs +383 -0
  78. package/bin/lib/planDependencies.mjs +70 -0
  79. package/bin/lib/postureTools.mjs +85 -0
  80. package/bin/lib/prWorkflow.mjs +94 -17
  81. package/bin/lib/preflight.mjs +8 -25
  82. package/bin/lib/preview.mjs +19 -25
  83. package/bin/lib/previewGatePolicy.mjs +34 -9
  84. package/bin/lib/previewRegistry.mjs +4 -11
  85. package/bin/lib/printable.mjs +70 -7
  86. package/bin/lib/procRegistry.mjs +33 -13
  87. package/bin/lib/procScan.mjs +2 -3
  88. package/bin/lib/procStat.mjs +76 -0
  89. package/bin/lib/processGroup.mjs +48 -0
  90. package/bin/lib/processes.mjs +10 -24
  91. package/bin/lib/projectToolRuntimes.mjs +328 -19
  92. package/bin/lib/projectTools.mjs +109 -34
  93. package/bin/lib/prompts.mjs +205 -60
  94. package/bin/lib/repoPath.mjs +19 -0
  95. package/bin/lib/repoState.mjs +17 -19
  96. package/bin/lib/repositoryBaseline.mjs +68 -0
  97. package/bin/lib/runTurn.mjs +286 -209
  98. package/bin/lib/runtimeAntigravity.mjs +16 -5
  99. package/bin/lib/runtimeCapabilities.mjs +215 -21
  100. package/bin/lib/runtimeClaude.mjs +78 -10
  101. package/bin/lib/runtimeCodex.mjs +217 -209
  102. package/bin/lib/runtimeDetection.mjs +8 -2
  103. package/bin/lib/runtimeEvents.mjs +28 -11
  104. package/bin/lib/runtimeLimits.mjs +5 -6
  105. package/bin/lib/runtimeOfferings.mjs +99 -14
  106. package/bin/lib/runtimes.mjs +10 -0
  107. package/bin/lib/shipSweep.mjs +5 -3
  108. package/bin/lib/shot.mjs +91 -17
  109. package/bin/lib/sleep.mjs +11 -0
  110. package/bin/lib/startProject.mjs +5 -5
  111. package/bin/lib/statePaths.mjs +40 -0
  112. package/bin/lib/tty.mjs +25 -15
  113. package/bin/lib/turnDiagnostics.mjs +93 -0
  114. package/bin/lib/turnLock.mjs +7 -14
  115. package/bin/lib/turnProfile.mjs +16 -4
  116. package/bin/lib/ui.mjs +0 -3
  117. package/bin/lib/uninstall.mjs +30 -26
  118. package/bin/lib/update.mjs +79 -27
  119. package/bin/lib/uplinkScrub.mjs +296 -31
  120. package/bin/lib/vault.mjs +78 -10
  121. package/bin/lib/vaultDiff.mjs +5 -2
  122. package/bin/lib/views.mjs +37 -12
  123. package/bin/lib/wikiRunner.mjs +40 -61
  124. package/bin/lib/work.mjs +44 -13
  125. package/bin/lib/workAdoptCarry.mjs +2 -4
  126. package/bin/lib/workAgentBrief.mjs +92 -0
  127. package/bin/lib/workAgentBriefRunner.mjs +59 -0
  128. package/bin/lib/workAgentCheck.mjs +104 -57
  129. package/bin/lib/workAgentMerges.mjs +62 -57
  130. package/bin/lib/workAgentPlans.mjs +82 -199
  131. package/bin/lib/workAgentPrecheck.mjs +76 -138
  132. package/bin/lib/workAgentPublish.mjs +6 -8
  133. package/bin/lib/workAgentTurnBegun.mjs +15 -11
  134. package/bin/lib/workAgentTurnExecution.mjs +131 -67
  135. package/bin/lib/workAgentTurnOutcome.mjs +11 -2
  136. package/bin/lib/workAgentTurnReports.mjs +9 -37
  137. package/bin/lib/workAgentTurns.mjs +64 -7
  138. package/bin/lib/workArtifactReconcile.mjs +79 -0
  139. package/bin/lib/workAttachments.mjs +66 -11
  140. package/bin/lib/workBrain.mjs +9 -4
  141. package/bin/lib/workCappedTurn.mjs +119 -0
  142. package/bin/lib/workChildren.mjs +5 -5
  143. package/bin/lib/workCommandAudit.mjs +18 -2
  144. package/bin/lib/workDiffs.mjs +52 -6
  145. package/bin/lib/workIntake.mjs +35 -60
  146. package/bin/lib/workPlaceReports.mjs +7 -4
  147. package/bin/lib/workPlaces.mjs +113 -31
  148. package/bin/lib/workPreviews.mjs +47 -112
  149. package/bin/lib/workProcesses.mjs +8 -35
  150. package/bin/lib/workPullRequests.mjs +37 -56
  151. package/bin/lib/workReportQueue.mjs +100 -148
  152. package/bin/lib/workRetire.mjs +2 -1
  153. package/bin/lib/workSessionRuntime.mjs +38 -37
  154. package/bin/lib/workSessionTurns.mjs +256 -199
  155. package/bin/lib/workShip.mjs +43 -23
  156. package/bin/lib/workToolLog.mjs +70 -3
  157. package/bin/lib/workTurnShape.mjs +165 -0
  158. package/npm-shrinkwrap.json +69 -0
  159. package/package.json +9 -4
  160. package/bin/lib/fleetJobs.mjs +0 -120
  161. package/bin/lib/patchRevert.mjs +0 -60
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { fileURLToPath } from 'node:url';
14
14
  import { SAFE } from './config.mjs';
15
+ import { runningCompiledBinary } from './launchCommand.mjs';
15
16
 
16
17
  /**
17
18
  * THE READ GUARD, ON EVERY CURATED POSTURE (2026-09-23) — see
@@ -35,20 +36,40 @@ import { SAFE } from './config.mjs';
35
36
  * `process.execPath`, never a `node` looked up on a PATH the operator may not
36
37
  * have, because a hook that fails to START is a non-blocking error and the
37
38
  * command runs anyway.
39
+ *
40
+ * …AND AS `<flowviant> __hook read-guard` UNDER THE COMPILED BINARY
41
+ * (2026-10-11). There `process.execPath` IS flowviant and the script path is
42
+ * Bun's virtual `/$bunfs/…`, so the old command was `flowviant
43
+ * '/$bunfs/root/hooks/readGuard.mjs'`: the CLI answered "is not a command"
44
+ * with exit 1 (measured on the installed 0.116.0), a non-blocking error, and
45
+ * every guarded command ran — the curl install had no read guard. The CLI now
46
+ * runs the hook itself (bin/cli.mjs, `__hook`), before anything else loads.
38
47
  */
39
48
  export const READ_GUARD_PATH = fileURLToPath(new URL('./hooks/readGuard.mjs', import.meta.url));
49
+ export const MCP_ALLOW_PATH = fileURLToPath(new URL('./hooks/mcpAllow.mjs', import.meta.url));
40
50
  const hookQuote = (s) =>
41
51
  process.platform === 'win32' ? `"${s}"` : `'${String(s).replace(/'/g, `'\\''`)}'`;
52
+ /** The command a hook runs as: this node and the script, or this binary and
53
+ * its `__hook` word (`compiled` is runningCompiledBinary's answer; a test
54
+ * passes it). */
55
+ export function hookCommand(name, scriptPath, { compiled = runningCompiledBinary() } = {}) {
56
+ return compiled ? `${hookQuote(process.execPath)} __hook ${name}` : `${hookQuote(process.execPath)} ${hookQuote(scriptPath)}`;
57
+ }
42
58
  export const READ_GUARD_SETTINGS = JSON.stringify({
43
59
  hooks: {
44
60
  PreToolUse: [
45
61
  {
46
62
  matcher: 'Bash',
47
- hooks: [{ type: 'command', command: `${hookQuote(process.execPath)} ${hookQuote(READ_GUARD_PATH)}` }],
63
+ hooks: [{ type: 'command', command: hookCommand('read-guard', READ_GUARD_PATH) }],
48
64
  },
49
65
  ],
50
66
  },
51
67
  });
68
+ /** The MCP allow hook's entry (hooks/mcpAllow.mjs), matched on MCP tools only. */
69
+ export const MCP_ALLOW_HOOK = Object.freeze({
70
+ matcher: 'mcp__.*',
71
+ hooks: [{ type: 'command', command: hookCommand('mcp-allow', MCP_ALLOW_PATH) }],
72
+ });
52
73
  /** Spread FIRST into a curated list: `--allowedTools` is variadic, so the
53
74
  * `--settings` pair must stand before it or be swallowed as a tool name. */
54
75
  const READ_GUARD = ['--settings', READ_GUARD_SETTINGS];
@@ -91,12 +112,22 @@ function fencedReads(knowledgeDir) {
91
112
  ];
92
113
  }
93
114
 
94
- /** The shell readers CONSULT and PLAN keep. Each one is PATH-VALIDATED by the
95
- * CLI itself — measured on 2.1.281 (2026-09-24): with `Bash(cat:*)`,
96
- * `Bash(head:*)` and `Bash(ls:*)` allowed, `cat`/`ls`/`head` on a file outside
97
- * the working directories were each refused ("Claude Code may only
98
- * concatenate files from the allowed working directories") — so it is the
99
- * Read/Grep/Glob TOOLS, not these, that needed the fence above. */
115
+ /** The shell readers CONSULT and PLAN keep. `cat`, `ls` and `head` are
116
+ * PATH-VALIDATED by the CLI itself — measured on 2.1.281 (2026-09-24): with
117
+ * `Bash(cat:*)`, `Bash(head:*)` and `Bash(ls:*)` allowed, each was refused a
118
+ * file outside the working directories ("Claude Code may only concatenate
119
+ * files from the allowed working directories") — so it is the Read/Grep/Glob
120
+ * TOOLS, not these, that needed the fence above.
121
+ *
122
+ * THE GIT READERS WERE NEVER MEASURED THAT WAY (2026-10-10, the audit), and
123
+ * `git diff` reads any file on the box: `--no-index` compares two arbitrary
124
+ * paths, and git switches to it on its own when a path lies outside the work
125
+ * tree (`git diff ~/.ssh/id_ed25519 /dev/null` prints the key). So the read
126
+ * guard is their path fence: it refuses `--no-index`, and a git argument that
127
+ * is absolute, `~`-rooted or climbs a `..` segment (revision ranges `a..b`
128
+ * still pass). It also refuses every one of these readers the checkout's
129
+ * secret files by name — the shell's half of CHECKOUT_SECRETS below, since a
130
+ * Read deny says nothing about `cat`. See hooks/readGuard.mjs. */
100
131
  const CONSULT_BASH = [
101
132
  'Bash(ls:*)',
102
133
  'Bash(wc:*)',
@@ -108,6 +139,14 @@ const CONSULT_BASH = [
108
139
  'Bash(git rev-parse:*)',
109
140
  ];
110
141
 
142
+ /** The checkout's own secrets, denied BY NAME on the postures that read the
143
+ * repo and hand what they read to someone (2026-10-10, the audit): consult
144
+ * (intake's brief lands with no press), plan and the wiki (every page is
145
+ * uploaded). Research's measured pair — `Read(./**)` read `.env` without it —
146
+ * plus Wrangler's `.dev.vars`, which no root `.env*` scan sees. The read guard
147
+ * refuses the same files to the shell readers (hooks/readGuard.mjs). */
148
+ const CHECKOUT_SECRETS = ['Read(./.env*)', 'Read(./**/.env*)', 'Read(./**/.dev.vars)'];
149
+
111
150
  /**
112
151
  * PLAN — read the repo, write the plan, never the code.
113
152
  *
@@ -120,10 +159,11 @@ const CONSULT_BASH = [
120
159
  * hijacked turn's most destructive reachable act is dropping a slice from the
121
160
  * plan it is already in, which a human can see and undo in the thread.
122
161
  *
123
- * Note what is absent versus WIKI_PERM: Write, Edit, mkdir and rm. The
124
- * cartographer needs those because it authors files; a planner authors records
125
- * through an API, and there is no file on this machine it has any business
126
- * touching.
162
+ * Note what is absent versus the wiki's list: Edit, mkdir and rm. The
163
+ * cartographer needs those because it authors files (in the vault, and only
164
+ * there); a planner authors records through an API, and there is no file on
165
+ * this machine it has any business touching. The checkout's secrets are denied
166
+ * by name (CHECKOUT_SECRETS), as on consult.
127
167
  */
128
168
  export function planPermFor(knowledgeDir) {
129
169
  return [
@@ -132,6 +172,8 @@ export function planPermFor(knowledgeDir) {
132
172
  'mcp__flowviant',
133
173
  ...fencedReads(knowledgeDir),
134
174
  ...CONSULT_BASH,
175
+ '--disallowedTools',
176
+ ...CHECKOUT_SECRETS,
135
177
  ];
136
178
  }
137
179
 
@@ -153,24 +195,13 @@ const PERM = SAFE
153
195
  ]
154
196
  : ['--dangerously-skip-permissions'];
155
197
 
156
- // Wiki turns are read-the-repo + write-the-vault ONLY — always curated, never
157
- // --dangerously-skip-permissions: no gh, no push-capable git, no package
158
- // managers, and nothing that can EXECUTE arbitrary commands — no `find`
159
- // (-exec/-delete) and no `git grep` (-O<pager> runs a shell; the Grep tool
160
- // covers search). Command execution is the line: it enables network exfil,
161
- // which plain file writes never do. `rm` IS allowed: pruning a stale vault
162
- // page requires a real file deletion (that's how the sync protocol learns of
163
- // it), and the blast radius is bounded — the daemon resets the repo worktree
164
- // after every wiki turn, and the vault has its own git history.
165
- // (Write/Edit can't be path-scoped here; the worktree reset is the backstop.)
166
- const WIKI_PERM = [
167
- ...READ_GUARD,
168
- '--allowedTools',
169
- 'Read',
170
- 'Grep',
171
- 'Glob',
172
- 'Edit',
173
- 'Write',
198
+ /** The cartographer's shell: the consult readers, plus `mkdir` and `rm` for
199
+ * the vault (pruning a stale page needs a real deletion — that is how the
200
+ * sync protocol learns of it) and `git status`. The CLI path-validates
201
+ * `mkdir`/`rm` to the working directories, which is why the vault is handed
202
+ * over by `--add-dir` (runtimeClaude.mjs). No `find` (-exec/-delete) and no
203
+ * `git grep` (-O<pager> runs a shell; the Grep tool covers search). */
204
+ const WIKI_BASH = [
174
205
  'Bash(ls:*)',
175
206
  'Bash(wc:*)',
176
207
  'Bash(head:*)',
@@ -184,24 +215,93 @@ const WIKI_PERM = [
184
215
  'Bash(git rev-parse:*)',
185
216
  ];
186
217
 
218
+ /**
219
+ * THE CARTOGRAPHER — read the repo, write the vault, and nothing else
220
+ * (2026-10-10, the audit: it joins the fenced postures of 2026-09-24).
221
+ *
222
+ * It ran on bare Read/Grep/Glob/Edit/Write, and its comment said file writes
223
+ * "never" exfiltrate and the worktree reset was the backstop. Neither held.
224
+ * The vault is an UPLINK — every page is synced to the project's wiki — so a
225
+ * bare Read of `~/.flowviant/credentials.json` copied into a page leaves the
226
+ * box; and the reset cleans the wiki worktree only, never a write outside it:
227
+ * `<vault>/.git/config` with a `core.fsmonitor` the daemon's own `git add`
228
+ * then ran, the checkout's `.flowviant/check.json`, a shell rc file. This turn
229
+ * runs UNATTENDED after every merge, on a member-written card title and on
230
+ * repo text, so the list is the enforcement and the prompt is only prose.
231
+ *
232
+ * Reads are the read fence (`fencedReads`: the worktree, the knowledge library
233
+ * when one is passed) plus the vault by its absolute path; Grep carries no
234
+ * allow rule, as in consult. The ONE write is `Edit(//<vault>/**)`, the vault's
235
+ * `.git` is denied both ways, and the checkout's secrets are denied by name.
236
+ *
237
+ * PROBED on Claude Code 2.1.296 before relying on it (2026-10-10), this exact
238
+ * list as the adapter builds it (`--add-dir <vault>`, stated `default` mode),
239
+ * cwd a detached worktree, the vault under `<home>/.flowviant/vaults/<id>`, a
240
+ * `-p` turn told to try each in turn:
241
+ * · ALLOWED: Write of a new `<vault>/pages/x.md` and Edit of `<vault>/index.md`
242
+ * (the Edit rule governs Write too; where ARTIFACT_WRITE's 2.1.281 note
243
+ * below reads anchored spellings as no write, this `//`-anchored Edit rule
244
+ * scoped as written on 2.1.296); `ls`, `rm` and `mkdir` in the vault; Read
245
+ * and Glob in the vault and the worktree.
246
+ * · REFUSED: Write of a canary outside both ("Claude requested permissions to
247
+ * write to …, but you haven't granted it yet"), of the checkout's
248
+ * `.flowviant/check.json`, of a file in the worktree itself, and of
249
+ * `<vault>/.git/config` ("File is in a directory that is denied by your
250
+ * permission settings"); Read of `<home>/.flowviant/credentials.json`, of the
251
+ * checkout's `.git/config`, and of the worktree's `.env` (denied by name).
252
+ * Every refused write left the disk unchanged.
253
+ * · CONTROLS: without the Edit rule the vault page was refused too — so
254
+ * `--add-dir` admits reads, never writes, and the rule is load-bearing;
255
+ * without the `.git` deny, the `.git/config` write was still refused by the
256
+ * CLI's own check ("… which is a sensitive file") — so the deny is a belt
257
+ * on this CLI, kept because that check is the vendor's and unpinned.
258
+ *
259
+ * A vault no permission rule can name (not absolute, or carrying glob or rule
260
+ * syntax) gets NO list: `claudePermFor` answers null and Claude's adapter
261
+ * refuses the turn in words, rather than running it unfenced or unable to
262
+ * write.
263
+ */
264
+ export function wikiPermFor(knowledgeDir, vaultDir) {
265
+ const vault = ruleSafeAbsolute(vaultDir);
266
+ if (!vault) return null;
267
+ return [
268
+ ...READ_GUARD,
269
+ '--allowedTools',
270
+ ...fencedReads(knowledgeDir),
271
+ `Read(/${vault}/**)`,
272
+ `Glob(/${vault}/**)`,
273
+ `Edit(/${vault}/**)`,
274
+ ...WIKI_BASH,
275
+ '--disallowedTools',
276
+ ...CHECKOUT_SECRETS,
277
+ `Read(/${vault}/.git/**)`,
278
+ `Edit(/${vault}/.git/**)`,
279
+ ];
280
+ }
281
+
187
282
  // A CONSULT reads and answers. Nothing else.
188
283
  //
189
- // It used to run on WIKI_PERM, whose comment two blocks up says the quiet part:
190
- // Write/Edit "can't be path-scoped here; the worktree reset is the backstop".
191
- // That is a fine trade for the cartographer, which exists to author files and
192
- // gets reset after every turn. It is the wrong trade for a consult, whose prompt
193
- // is steered by a question ANY project editor can write and which had no reset
194
- // behind it — so a sentence in a chat box could reach Write, rm and mkdir on
195
- // someone else's machine. The permission list is the enforcement; the prompt's
196
- // "do not change anything" is only an instruction, and instructions are exactly
197
- // what an injected question competes with.
284
+ // It used to run on the cartographer's old list, whose comment said the quiet
285
+ // part: Write/Edit "can't be path-scoped here; the worktree reset is the
286
+ // backstop". That was the wrong trade for a consult, whose prompt is steered
287
+ // by a question ANY project editor can write and which had no reset behind it
288
+ // — so a sentence in a chat box could reach Write, rm and mkdir on someone
289
+ // else's machine (and it was the wrong trade for the cartographer too: see
290
+ // `wikiPermFor`). The permission list is the enforcement; the prompt's "do not
291
+ // change anything" is only an instruction, and instructions are exactly what
292
+ // an injected question competes with.
293
+ //
294
+ // The checkout's secrets are denied by name (2026-10-10): a consult's answer
295
+ // leaves the box, and intake's drafted brief lands on the board with no press.
198
296
  export function consultPermFor(knowledgeDir) {
199
- return [...READ_GUARD, '--allowedTools', ...fencedReads(knowledgeDir), ...CONSULT_BASH];
297
+ return [...READ_GUARD, '--allowedTools', ...fencedReads(knowledgeDir), ...CONSULT_BASH, '--disallowedTools', ...CHECKOUT_SECRETS];
200
298
  }
201
299
 
202
300
  /**
203
301
  * THE TWO NON-CODE POSTURES (0.97.0) — a design card and a research card may
204
- * WRITE, and only under `.flowviant/artifacts/`.
302
+ * HISTORICAL ARTIFACT PROBES before 2026-10-12. Current artifact lists below
303
+ * add guarded readers, web and scratch; arbitrary programs still cannot
304
+ * bypass the write fence. WRITE was only under `.flowviant/artifacts/`.
205
305
  *
206
306
  * The card's words are steered by anyone who can file a card, so "change
207
307
  * nothing" as prose is only an instruction, and the permission list is the
@@ -260,11 +360,26 @@ export function consultPermFor(knowledgeDir) {
260
360
  * explicit `Bash(git log:*)` prefix the old list carried). With `Bash` in
261
361
  * `--disallowedTools` the tool was not there at all.
262
362
  *
263
- * NO MCP, and no shell that could commit: the agent turn has no MCP anyway
264
- * (see SYSTEM_AGENT's header), and a posture that could run `git commit` could
265
- * commit the mockup it was told to keep out of git.
363
+ * THE PROJECT'S MCP SERVERS (2026-10-10, the owner): each list allows
364
+ * `mcp__<server>` for every server the agent lane mounted from the base
365
+ * branch's reviewed `.mcp.json` (projectTools.mjs) — the set a code card gets.
366
+ * Under `--allowedTools` an MCP tool with no rule is refused ("Claude requested
367
+ * permissions to use mcp__…, but you haven't granted it yet", measured on the
368
+ * plan posture), so the rule IS the mount. AND THE MACHINE'S OWN (2026-10-11):
369
+ * an agent turn mounts this machine's user/local servers and claude.ai
370
+ * connectors too, whose names only the CLI knows, so the lane adds the MCP
371
+ * allow hook (`withMachineMcp`, below). No shell that could commit: a posture
372
+ * that could run `git commit` could commit the mockup it was told to keep out
373
+ * of git.
266
374
  *
267
- * STATED, not closed: research may still read any file in the worktree and
375
+ * STATED, not closed: an MCP server is a process of its own, outside every
376
+ * list here — it reads, writes and reaches the network as the person who runs
377
+ * it, and a turn steered by a card's words can hand it anything the turn read,
378
+ * or ask it to act: the machine's claude.ai connectors are its owner's
379
+ * accounts. On research that sits beside the web. The base branch's
380
+ * `.mcp.json` is the repository's reviewed choice, and the machine's servers
381
+ * are its owner's (docs/decisions.md, 2026-10-10 and 2026-10-11).
382
+ * Research may still read any file in the worktree and
268
383
  * name it in a WebFetch URL — the repository is what a research card is about,
269
384
  * and a turn that could not read it could not answer. Design may read any
270
385
  * file in its worktree and put it in an artifact the server stores; text
@@ -279,6 +394,12 @@ export function consultPermFor(knowledgeDir) {
279
394
  * the guard's hook sees a subagent's own calls was not measured here.
280
395
  */
281
396
  const ARTIFACT_WRITE = 'Edit(.flowviant/artifacts/**)';
397
+ /** `mcp__<server>` for each mounted project server — a name the base snapshot
398
+ * already admitted (projectTools.mjs `nameOk`), checked again here because a
399
+ * rule is a pattern: anything but a plain server name gets no rule, and
400
+ * Flowviant's own control plane is never a project's server. */
401
+ const projectMcpRules = (names = []) =>
402
+ names.filter((n) => typeof n === 'string' && /^[A-Za-z0-9_-]{1,80}$/.test(n) && n !== 'flowviant').map((n) => `mcp__${n}`);
282
403
  /**
283
404
  * PLAN MODE (0.97.0) — a Workbench tab's `planMode` switch, and Claude Code's
284
405
  * own `--permission-mode plan` INSTEAD OF every list above: the CLI reads,
@@ -306,20 +427,25 @@ const ARTIFACT_WRITE = 'Edit(.flowviant/artifacts/**)';
306
427
  * `readOnlyHint: true`, and none of the session tools (stream_session_turn,
307
428
  * log_work, file_card …) are read-only or annotated so. So a plan turn runs
308
429
  * PLAIN — no MCP, the plain tab's contract — rather than mounting a control
309
- * plane whose every call the CLI refuses. See workSessionTurns.mjs.
430
+ * plane whose every call the CLI refuses. See workTurnShape.mjs.
310
431
  */
311
432
  export const PLAN_MODE_PERM = ['--permission-mode', 'plan'];
312
- export function designPermFor(knowledgeDir) {
433
+ export function designPermFor(knowledgeDir, projectMcp = []) {
313
434
  return [
314
435
  ...READ_GUARD,
315
436
  '--allowedTools',
316
437
  ...fencedReads(knowledgeDir),
317
- 'Bash(ls:*)',
438
+ ...CONSULT_BASH,
439
+ 'WebSearch',
440
+ 'WebFetch',
318
441
  ARTIFACT_WRITE,
442
+ 'Edit(.flowviant/tmp/**)',
443
+ ...projectMcpRules(projectMcp),
319
444
  // A mockup is uploaded and runs scripts in a frame that may navigate
320
445
  // itself, so the checkout's own secrets are denied by name too — the same
321
446
  // pair research carries, measured load-bearing there.
322
447
  '--disallowedTools',
448
+ 'Agent',
323
449
  'Read(./.env*)',
324
450
  'Read(./**/.env*)',
325
451
  ];
@@ -331,18 +457,21 @@ export const DESIGN_PERM = designPermFor(null);
331
457
  * outside the worktree it may read, and only the spawn knows where that is.
332
458
  * `//` is the CLI's own spelling for an absolute path in a rule.
333
459
  */
334
- export function researchPerm(knowledgeDir) {
460
+ export function researchPerm(knowledgeDir, projectMcp = []) {
335
461
  return [
336
462
  ...READ_GUARD,
337
463
  '--allowedTools',
338
464
  ...fencedReads(knowledgeDir),
465
+ ...CONSULT_BASH,
339
466
  'WebSearch',
340
467
  'WebFetch',
341
468
  ARTIFACT_WRITE,
469
+ 'Edit(.flowviant/tmp/**)',
470
+ ...projectMcpRules(projectMcp),
342
471
  '--disallowedTools',
472
+ 'Agent',
343
473
  'Read(./.env*)',
344
474
  'Read(./**/.env*)',
345
- 'Bash',
346
475
  ];
347
476
  }
348
477
  /** The list with no library — what a project that keeps none runs under. */
@@ -352,15 +481,17 @@ export const RESEARCH_PERM = researchPerm(null);
352
481
  * CLAUDE'S PERMISSION LIST FOR A TURN PROFILE, by the profile's name — one
353
482
  * entry per name in turnProfile.mjs (a test holds the two tables equal). The
354
483
  * builders that take the knowledge directory get it; the fixed lists ignore it.
484
+ * `projectMcp` is the names of the project servers the lane mounted; only the
485
+ * design and research lists take them (`projectMcp` in turnProfile.mjs).
355
486
  */
356
487
  const CLAUDE_PERM = {
357
488
  build: () => PERM,
358
- wiki: () => WIKI_PERM,
489
+ wiki: (kd, vaultDir) => wikiPermFor(kd, vaultDir),
359
490
  consult: (kd) => consultPermFor(kd),
360
491
  plan: (kd) => planPermFor(kd),
361
492
  'plan-mode': () => PLAN_MODE_PERM,
362
- design: (kd) => designPermFor(kd),
363
- research: (kd) => researchPerm(kd),
493
+ design: (kd, _vault, mcp) => designPermFor(kd, mcp),
494
+ research: (kd, _vault, mcp) => researchPerm(kd, mcp),
364
495
  // THE IMAGE POSTURE IS CODEX'S (0.114.0) and Claude does not declare it
365
496
  // (runtimeClaude.mjs `profiles`), so turnProfile.mjs refuses a Claude turn
366
497
  // under it before argv is built. The entry exists because `runTurn` looks
@@ -370,8 +501,50 @@ const CLAUDE_PERM = {
370
501
  image: (kd) => consultPermFor(kd),
371
502
  };
372
503
  export const CLAUDE_PERM_PROFILES = Object.freeze(Object.keys(CLAUDE_PERM));
373
- export function claudePermFor(profileName, knowledgeDir) {
504
+ export function claudePermFor(profileName, knowledgeDir, vaultDir, projectMcp = []) {
374
505
  const build = Object.hasOwn(CLAUDE_PERM, profileName) ? CLAUDE_PERM[profileName] : null;
375
506
  if (!build) throw new Error(`no Claude permission list for profile '${profileName}'`);
376
- return build(knowledgeDir);
507
+ return build(knowledgeDir, vaultDir, projectMcp);
508
+ }
509
+
510
+ /**
511
+ * THE MACHINE'S MCP SERVERS ON AN AGENT TURN (2026-10-11, the owner: "the
512
+ * server that is running flowviant's claude HAS these MCPs yet the agents are
513
+ * not using them"). A code, write-up, mockup, 3D-model or deck turn mounts what
514
+ * this machine's Claude Code has — its user and local servers and the account's
515
+ * claude.ai connectors — beside the base branch's `.mcp.json` copy. So the turn
516
+ * drops `--strict-mcp-config` (runtimeClaude.mjs), and its one `--settings`
517
+ * carries two things:
518
+ *
519
+ * · `disabledMcpjsonServers`, every server named in a `.mcp.json` from the
520
+ * worktree up to `/`. Without strict, Claude Code reads `.mcp.json` in the
521
+ * cwd AND EVERY PARENT (measured, 2.1.290: a parent directory's server
522
+ * loaded) wherever a settings scope approves it, so the agent's own edit of
523
+ * its worktree's file would mount before review. A flag
524
+ * `enableAllProjectMcpServers: false` did NOT override a lower scope's
525
+ * `true`; the disabled list did, by name, and left the base copy loaded
526
+ * through `--mcp-config` (a `dynamic` server) untouched — measured with both
527
+ * a lower `enableAllProjectMcpServers` and a lower `enabledMcpjsonServers`.
528
+ * · the MCP allow hook (`MCP_ALLOW_HOOK`), because a fenced list refuses an
529
+ * MCP tool it does not name and a connector's name is the account's. The
530
+ * build's bypass needs no hook; a SAFE build's curated list does, and the
531
+ * hook is harmless under the bypass, so every mount carries it.
532
+ *
533
+ * `perm` is the posture's list; a list with no `--settings` (the build) gets
534
+ * one before its lists (`--allowedTools` is variadic). The fenced turn's
535
+ * personal keys are folded in after (claudePersonal.mjs); the posture's keys
536
+ * win there, and none of the person's kept keys is one of these.
537
+ *
538
+ * STATED, not closed: plugins stay the person's own on the build and out of
539
+ * every fenced turn (`--setting-sources ''`), so a plugin's MCP servers reach
540
+ * code cards only — a plugin also brings hooks, which would be a second author
541
+ * of a fenced posture.
542
+ */
543
+ export function withMachineMcp(perm, disabledProjectMcp) {
544
+ const extra = { disabledMcpjsonServers: [...disabledProjectMcp] };
545
+ const at = perm.indexOf('--settings');
546
+ if (at < 0) return ['--settings', JSON.stringify({ ...extra, hooks: { PreToolUse: [MCP_ALLOW_HOOK] } }), ...perm];
547
+ const posture = JSON.parse(perm[at + 1]);
548
+ const hooks = { ...(posture.hooks ?? {}), PreToolUse: [...(posture.hooks?.PreToolUse ?? []), MCP_ALLOW_HOOK] };
549
+ return [...perm.slice(0, at), '--settings', JSON.stringify({ ...posture, ...extra, hooks }), ...perm.slice(at + 2)];
377
550
  }
@@ -24,6 +24,8 @@ import {
24
24
  import { homedir } from 'node:os';
25
25
  import { join } from 'node:path';
26
26
  import { execFileSync } from 'node:child_process';
27
+ import { parseProcStat } from './procStat.mjs';
28
+ import { CLAUDE_SESSION_RE } from './conversationIds.mjs';
27
29
 
28
30
  // ENDED sessions are the adoptable inventory, and the useful ones are FRESH:
29
31
  // newest per directory, few. (The window itself is the coordinator's.)
@@ -87,7 +89,7 @@ function transcriptTitle(file, mtimeMs) {
87
89
  * the tab keeps whatever name it has.
88
90
  */
89
91
  export function titleForSession(cwd, sessionId) {
90
- if (!cwd || typeof sessionId !== 'string' || !/^[A-Za-z0-9_-]{8,64}$/.test(sessionId)) {
92
+ if (!cwd || typeof sessionId !== 'string' || !CLAUDE_SESSION_RE.test(sessionId)) {
91
93
  return null;
92
94
  }
93
95
  const projects = join(homedir(), '.claude', 'projects');
@@ -137,8 +139,8 @@ export function claudeProjectDir(path) {
137
139
  * else, the file stays. `/proc/<pid>` existing only proves A process; the
138
140
  * starttime (field 22 of /proc/<pid>/stat) proves it is THAT process. The comm
139
141
  * field (parenthesised, may itself contain spaces and parens) makes naive
140
- * whitespace-splitting wrong, so fields are counted from after the LAST ')':
141
- * the first post-comm field is field 3, which puts starttime at index 19.
142
+ * whitespace-splitting wrong, so the line is read by procStat.mjs, the one
143
+ * parser that knows where `comm` ends.
142
144
  */
143
145
  function pidAlive(pid, procStart) {
144
146
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -174,10 +176,8 @@ function pidAlive(pid, procStart) {
174
176
  return false; // /proc is real and has no entry — the process is gone
175
177
  }
176
178
  if (procStart == null) return true; // nothing recorded to compare against
177
- const close = stat.lastIndexOf(')');
178
- if (close === -1) return false;
179
- const fields = stat.slice(close + 1).trim().split(/\s+/);
180
- return fields[19] === String(procStart);
179
+ const fields = parseProcStat(stat);
180
+ return fields !== null && String(fields.starttime) === String(procStart);
181
181
  }
182
182
 
183
183
  /**
@@ -12,16 +12,12 @@
12
12
  import { humanizeClaudeTool, THINK_MARKER } from './runtimeEvents.mjs';
13
13
  import { turnModelOf } from './turnModel.mjs';
14
14
 
15
- // Turn one Claude tool_use into a compact activity {kind, label}, or null for
16
- // tools not worth surfacing. `kind:'read'` is what the file counter counts; a
17
- // Write/Edit of a vault page is the "writing" signal. Used by wiki turns to
18
- // stream exactly which files Claude is touching (daemon console + app cover).
19
- //
20
- // The body moved to runtimeEvents.mjs, beside Codex's equivalent, because they are
21
- // the same job for two vendors and keeping them apart is how the two activity
22
- // vocabularies drift. Re-exported under its original name: a dozen call sites
23
- // know it, and none of them care where it lives.
24
- export const humanizeToolUse = humanizeClaudeTool;
15
+ // One Claude tool_use becomes a compact activity {kind, label}, or null for
16
+ // tools not worth surfacing, through `humanizeClaudeTool` — in runtimeEvents.mjs,
17
+ // beside Codex's equivalent, because they are the same job for two vendors and
18
+ // keeping them apart is how the two activity vocabularies drift. (Its old
19
+ // alias here, `humanizeToolUse`, claimed a dozen call sites; the stream loop
20
+ // below was the only one, and it calls the real name.)
25
21
 
26
22
  // Collapse whitespace + clip so a narration/thinking snippet is one tidy feed line.
27
23
  const oneLine = (s, n = 160) => String(s).replace(/\s+/g, ' ').trim().slice(0, n);
@@ -97,7 +93,16 @@ export function handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, app
97
93
  emit(a.label + '\n');
98
94
  onActivity?.(a);
99
95
  };
100
- if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
96
+ if (ev.type === 'stream_event') {
97
+ // Partial messages announce reasoning before the completed assistant
98
+ // block. Relay only that measured start here: text and tool inputs still
99
+ // come from the complete blocks below, once, with their full contents.
100
+ const block = ev.event?.content_block;
101
+ if (ev.event?.type === 'content_block_start' &&
102
+ (block?.type === 'thinking' || block?.type === 'redacted_thinking')) {
103
+ push({ kind: 'think', label: THINK_MARKER });
104
+ }
105
+ } else if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
101
106
  // THE MODEL THAT ANSWERED (2026-09-29, turnModel.mjs) — the fallback for an
102
107
  // init that named none. The turn's own messages only: a subagent's carry
103
108
  // `parent_tool_use_id`, and its model is not the turn's.
@@ -141,13 +146,21 @@ export function handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, app
141
146
  // the page to read.
142
147
  push({ kind: 'say', label: oneLine(b.text), full: b.text });
143
148
  } else if (b.type === 'tool_use') {
144
- push(humanizeToolUse(b.name, b.input || {}, cwd));
149
+ push(humanizeClaudeTool(b.name, b.input || {}, cwd));
145
150
  // The STRUCTURED form of the same event, for the transcript's tool
146
151
  // cards — raw name + input, so the collector can keep what the
147
152
  // one-line humanizer drops (an Edit's counts, the plan's items).
148
- onToolEvent?.(b.name, b.input || {});
153
+ onToolEvent?.(b.name, b.input || {}, { id: b.id });
149
154
  }
150
155
  }
156
+ } else if (ev.type === 'user' && Array.isArray(ev.message?.content)) {
157
+ for (const b of ev.message.content) {
158
+ if (b.type === 'tool_result') onToolEvent?.(null, {}, {
159
+ id: b.tool_use_id, result: true, isError: b.is_error === true,
160
+ output: typeof b.content === 'string' ? b.content : Array.isArray(b.content) ? b.content.slice(0, 8).filter(x => x?.type === 'text' && typeof x.text === 'string').map(x => x.text).join('\n') : '',
161
+ exitCode: ev.tool_use_result?.exit_code ?? ev.tool_use_result?.exitCode,
162
+ });
163
+ }
151
164
  } else if (ev.type === 'system' && ev.subtype === 'init') {
152
165
  // WHAT THIS MACHINE'S CLI CAN BE ASKED FOR BY NAME. The init event is the
153
166
  // CLI's OWN answer — it has already resolved personal skills, this repo's
@@ -181,6 +194,7 @@ export function handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, app
181
194
  skills: Array.isArray(ev.skills) ? ev.skills.map(String) : undefined,
182
195
  sessionId: typeof ev.session_id === 'string' ? ev.session_id : undefined,
183
196
  mcpServers: Array.isArray(ev.mcp_servers) ? ev.mcp_servers : undefined,
197
+ tools: Array.isArray(ev.tools) ? ev.tools : undefined,
184
198
  });
185
199
  }
186
200
  } else if (ev.type === 'result') {
@@ -5,7 +5,7 @@
5
5
  * which parks on it, and the pre-review, which only skips) and neither owns
6
6
  * it: it is a fact about what the CLIs print.
7
7
  */
8
- import { scrub as envScrub } from './uplinkScrub.mjs';
8
+ import { scrubCut } from './uplinkScrub.mjs';
9
9
 
10
10
  /**
11
11
  * DID THE ACCOUNT HIT A LIMIT?
@@ -28,7 +28,7 @@ const LIMIT_PHRASES = [
28
28
  export const limitLine = (text) => {
29
29
  for (const line of String(text ?? '').split('\n')) {
30
30
  const t = line.trim();
31
- if (t && LIMIT_PHRASES.some((re) => re.test(t))) return envScrub(t).slice(0, 300);
31
+ if (t && LIMIT_PHRASES.some((re) => re.test(t))) return scrubCut(t, 300);
32
32
  }
33
33
  return null;
34
34
  };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * WHERE THIS ENVIRONMENT'S CODEX KEEPS ITS FILES (audit 2026-10-10, F126) —
3
+ * `$CODEX_HOME`, else `~/.codex`, with `~` resolved the way the CLI resolves
4
+ * it: the environment's own HOME first, the process's home only without one.
5
+ *
6
+ * A LEAF: node imports only. The login reader (codexPersonal.mjs), the
7
+ * rollout reader behind plan limits and "which model ran" (runtimeLimits.mjs),
8
+ * the offerings readout (runtimeOfferings.mjs) and an agent's isolated home
9
+ * (projectToolRuntimes.mjs) each spelled this rule, one of them differently;
10
+ * every one of them is a relay that must look where the CLI wrote.
11
+ */
12
+ import { homedir } from 'node:os';
13
+ import { join } from 'node:path';
14
+
15
+ export function codexHome(env = process.env) {
16
+ return env.CODEX_HOME || join(env.HOME || homedir(), '.codex');
17
+ }
@@ -55,9 +55,9 @@
55
55
  * config read "Not logged in").
56
56
  */
57
57
  import { readFileSync, statSync } from 'node:fs';
58
- import { homedir } from 'node:os';
59
58
  import { join } from 'node:path';
60
59
  import { toml } from './projectToolRuntimes.mjs';
60
+ import { codexHome } from './codexHome.mjs';
61
61
 
62
62
  /** Where the login lives and which endpoint it signs into — top-level keys,
63
63
  * each confirmed in codex-cli 0.156.1 (`preferred_auth_method` is not one:
@@ -365,8 +365,7 @@ export function parseCodexToml(text) {
365
365
  * not a document this reader can read whole.
366
366
  */
367
367
  export function readPersonalCodexConfig(env = process.env) {
368
- const home = env.CODEX_HOME || join(env.HOME || homedir(), '.codex');
369
- const path = join(home, 'config.toml');
368
+ const path = join(codexHome(env), 'config.toml');
370
369
  try {
371
370
  if (statSync(path).size > CONFIG_MAX_BYTES) return null;
372
371
  return parseCodexToml(readFileSync(path, 'utf8'));
@@ -461,6 +460,48 @@ export function codexPersonalCarry(config, { model = null, effort = null, env: t
461
460
  return { args, env };
462
461
  }
463
462
 
463
+ /**
464
+ * THE PERSON'S CODEX PROVIDER SECRETS, FOR THE UPLINK SCRUBBER ONLY
465
+ * (2026-10-10, the audit's F073) — `[{ name, value }]`, never a report.
466
+ *
467
+ * The header above says why the turn's own model can read these (`env` lists
468
+ * the carried literals; the read-only sandbox reads config.toml), and nothing
469
+ * taught the scrubber any of them, so a turn that printed one relayed it to
470
+ * every member of the project. The SELECTED provider's
471
+ * `experimental_bearer_token` and literal `http_headers` values, named as the
472
+ * carry names them (`CODEX_BEARER_ENV`, `CODEX_HEADER_ENV_PREFIX` + n), plus
473
+ * the present values of the variables its `env_key` and `env_http_headers`
474
+ * name, under those names. Whatever the posture: a build turn's Codex reads
475
+ * the same file. An unreadable or provider-less config gives `[]`, which the
476
+ * scrubber treats as "nothing new", never as "forget".
477
+ */
478
+ export function codexSecretsOf(config, env = {}) {
479
+ const out = [];
480
+ if (!isTable(config)) return out;
481
+ const id = config.model_provider;
482
+ const table = isString(id) && id && isTable(config.model_providers) ? config.model_providers[id] : undefined;
483
+ if (!isTable(table)) return out;
484
+ if (nonEmpty(table.experimental_bearer_token)) {
485
+ out.push({ name: CODEX_BEARER_ENV, value: table.experimental_bearer_token });
486
+ }
487
+ if (isTable(table.http_headers)) {
488
+ let n = 0;
489
+ for (const literal of Object.values(table.http_headers)) {
490
+ if (nonEmpty(literal)) out.push({ name: `${CODEX_HEADER_ENV_PREFIX}${n++}`, value: literal });
491
+ }
492
+ }
493
+ const named = [table.env_key, ...(isTable(table.env_http_headers) ? Object.values(table.env_http_headers) : [])];
494
+ for (const name of named) {
495
+ if (nonEmpty(name) && nonEmpty(env[name])) out.push({ name, value: env[name] });
496
+ }
497
+ return out;
498
+ }
499
+
500
+ /** `codexSecretsOf` over the config this process's Codex would read. */
501
+ export function readPersonalCodexSecrets(env = process.env) {
502
+ return codexSecretsOf(readPersonalCodexConfig(env), env);
503
+ }
504
+
464
505
  /** The carry for one turn: the config its Codex would read, reduced. */
465
506
  export function codexPersonalTurn({ env = process.env, model = null, effort = null } = {}) {
466
507
  return codexPersonalCarry(readPersonalCodexConfig(env), { model, effort, env });