flowviant 0.115.0 → 0.117.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 (135) hide show
  1. package/README.md +31 -12
  2. package/bin/cli.mjs +13 -17
  3. package/bin/lib/admission.mjs +7 -14
  4. package/bin/lib/agentInternalWait.mjs +83 -0
  5. package/bin/lib/agentPlan.mjs +56 -24
  6. package/bin/lib/agentPublish.mjs +12 -13
  7. package/bin/lib/agentTaskKinds.mjs +4 -7
  8. package/bin/lib/agySessions.mjs +3 -3
  9. package/bin/lib/artifactPreview.mjs +92 -18
  10. package/bin/lib/artifactReporter.mjs +16 -8
  11. package/bin/lib/artifacts.mjs +64 -60
  12. package/bin/lib/authproxy.mjs +2 -1
  13. package/bin/lib/childEnv.mjs +9 -4
  14. package/bin/lib/claude.mjs +10 -20
  15. package/bin/lib/claudePersonal.mjs +71 -6
  16. package/bin/lib/claudePosture.mjs +119 -42
  17. package/bin/lib/claudeSessions.mjs +7 -7
  18. package/bin/lib/claudeStream.mjs +7 -11
  19. package/bin/lib/cliLimit.mjs +2 -2
  20. package/bin/lib/codexHome.mjs +17 -0
  21. package/bin/lib/codexPersonal.mjs +44 -3
  22. package/bin/lib/codexRelay.mjs +21 -5
  23. package/bin/lib/codexResearch.mjs +180 -0
  24. package/bin/lib/config.mjs +37 -24
  25. package/bin/lib/conversationIds.mjs +51 -0
  26. package/bin/lib/credentials.mjs +2 -7
  27. package/bin/lib/daemonLocks.mjs +88 -0
  28. package/bin/lib/daemonState.mjs +5 -5
  29. package/bin/lib/deploy.mjs +6 -9
  30. package/bin/lib/deployCheckout.mjs +26 -22
  31. package/bin/lib/deployRunner.mjs +54 -18
  32. package/bin/lib/deployWire.mjs +32 -23
  33. package/bin/lib/desktopContract.mjs +3 -5
  34. package/bin/lib/env.mjs +104 -35
  35. package/bin/lib/fleet.mjs +7 -9
  36. package/bin/lib/fleetPost.mjs +172 -25
  37. package/bin/lib/fleetReports.mjs +130 -170
  38. package/bin/lib/fleetStartup.mjs +6 -6
  39. package/bin/lib/fleetWire.mjs +42 -0
  40. package/bin/lib/git.mjs +111 -61
  41. package/bin/lib/gitMergeIdentity.mjs +2 -8
  42. package/bin/lib/grant.mjs +23 -7
  43. package/bin/lib/holder.mjs +108 -81
  44. package/bin/lib/hooks/readGuard.mjs +41 -7
  45. package/bin/lib/install.mjs +80 -33
  46. package/bin/lib/instance.mjs +36 -83
  47. package/bin/lib/intakeDraft.mjs +4 -1
  48. package/bin/lib/knowledgeFileSync.mjs +31 -0
  49. package/bin/lib/knowledgeFiles.mjs +28 -83
  50. package/bin/lib/landed.mjs +22 -29
  51. package/bin/lib/launchCommand.mjs +91 -0
  52. package/bin/lib/listeners.mjs +0 -5
  53. package/bin/lib/localSessions.mjs +52 -39
  54. package/bin/lib/login.mjs +22 -12
  55. package/bin/lib/loginPath.mjs +22 -17
  56. package/bin/lib/machineListing.mjs +6 -3
  57. package/bin/lib/mcp-cli.mjs +9 -10
  58. package/bin/lib/noWindowEnv.mjs +23 -3
  59. package/bin/lib/ownedDirs.mjs +99 -0
  60. package/bin/lib/pinnedCapture.mjs +383 -0
  61. package/bin/lib/prWorkflow.mjs +94 -17
  62. package/bin/lib/preflight.mjs +8 -25
  63. package/bin/lib/preview.mjs +19 -25
  64. package/bin/lib/previewGatePolicy.mjs +34 -9
  65. package/bin/lib/previewRegistry.mjs +4 -11
  66. package/bin/lib/printable.mjs +70 -7
  67. package/bin/lib/procRegistry.mjs +33 -13
  68. package/bin/lib/procScan.mjs +2 -3
  69. package/bin/lib/procStat.mjs +76 -0
  70. package/bin/lib/processGroup.mjs +48 -0
  71. package/bin/lib/processes.mjs +10 -24
  72. package/bin/lib/projectToolRuntimes.mjs +101 -16
  73. package/bin/lib/projectTools.mjs +50 -32
  74. package/bin/lib/prompts.mjs +41 -19
  75. package/bin/lib/repoPath.mjs +19 -0
  76. package/bin/lib/repoState.mjs +17 -19
  77. package/bin/lib/runTurn.mjs +268 -210
  78. package/bin/lib/runtimeAntigravity.mjs +16 -5
  79. package/bin/lib/runtimeCapabilities.mjs +140 -20
  80. package/bin/lib/runtimeClaude.mjs +55 -5
  81. package/bin/lib/runtimeCodex.mjs +245 -205
  82. package/bin/lib/runtimeDetection.mjs +2 -2
  83. package/bin/lib/runtimeEvents.mjs +16 -10
  84. package/bin/lib/runtimeLimits.mjs +5 -6
  85. package/bin/lib/runtimeOfferings.mjs +24 -11
  86. package/bin/lib/runtimes.mjs +10 -0
  87. package/bin/lib/shipSweep.mjs +5 -3
  88. package/bin/lib/shot.mjs +91 -17
  89. package/bin/lib/sleep.mjs +11 -0
  90. package/bin/lib/startProject.mjs +5 -5
  91. package/bin/lib/statePaths.mjs +40 -0
  92. package/bin/lib/tty.mjs +25 -15
  93. package/bin/lib/turnLock.mjs +7 -14
  94. package/bin/lib/turnProfile.mjs +1 -1
  95. package/bin/lib/ui.mjs +0 -3
  96. package/bin/lib/uninstall.mjs +30 -26
  97. package/bin/lib/update.mjs +79 -27
  98. package/bin/lib/uplinkScrub.mjs +296 -31
  99. package/bin/lib/vault.mjs +78 -10
  100. package/bin/lib/vaultDiff.mjs +5 -2
  101. package/bin/lib/views.mjs +37 -12
  102. package/bin/lib/wikiRunner.mjs +40 -61
  103. package/bin/lib/work.mjs +17 -7
  104. package/bin/lib/workAdoptCarry.mjs +2 -4
  105. package/bin/lib/workAgentCheck.mjs +104 -57
  106. package/bin/lib/workAgentMerges.mjs +62 -57
  107. package/bin/lib/workAgentPlans.mjs +99 -145
  108. package/bin/lib/workAgentPrecheck.mjs +71 -138
  109. package/bin/lib/workAgentPublish.mjs +6 -8
  110. package/bin/lib/workAgentTurnBegun.mjs +15 -11
  111. package/bin/lib/workAgentTurnExecution.mjs +46 -53
  112. package/bin/lib/workAgentTurnReports.mjs +9 -37
  113. package/bin/lib/workAgentTurns.mjs +4 -3
  114. package/bin/lib/workAttachments.mjs +66 -11
  115. package/bin/lib/workBrain.mjs +9 -4
  116. package/bin/lib/workCappedTurn.mjs +109 -0
  117. package/bin/lib/workChildren.mjs +5 -5
  118. package/bin/lib/workCommandAudit.mjs +11 -2
  119. package/bin/lib/workDiffs.mjs +52 -6
  120. package/bin/lib/workIntake.mjs +35 -60
  121. package/bin/lib/workPlaceReports.mjs +7 -4
  122. package/bin/lib/workPlaces.mjs +96 -30
  123. package/bin/lib/workPreviews.mjs +47 -112
  124. package/bin/lib/workProcesses.mjs +8 -35
  125. package/bin/lib/workPullRequests.mjs +37 -56
  126. package/bin/lib/workReportQueue.mjs +100 -148
  127. package/bin/lib/workRetire.mjs +2 -1
  128. package/bin/lib/workSessionRuntime.mjs +38 -37
  129. package/bin/lib/workSessionTurns.mjs +111 -189
  130. package/bin/lib/workShip.mjs +43 -23
  131. package/bin/lib/workTurnShape.mjs +163 -0
  132. package/npm-shrinkwrap.json +59 -0
  133. package/package.json +3 -2
  134. package/bin/lib/fleetJobs.mjs +0 -120
  135. package/bin/lib/patchRevert.mjs +0 -60
@@ -14,6 +14,7 @@ import { SAFE } from './config.mjs';
14
14
  import { parseCodexLine } from './runtimeEvents.mjs';
15
15
  import { ARTIFACT_DIR, FENCE_TMP_DIR } from './artifacts.mjs';
16
16
  import { codexPersonalTurn } from './codexPersonal.mjs';
17
+ import { researchFence } from './codexResearch.mjs';
17
18
 
18
19
  /**
19
20
  * FLOWVIANT'S OWN TOOLS NEED NO APPROVAL FROM A CHAT NOBODY CAN ANSWER
@@ -51,16 +52,6 @@ function codexMcp(token, mcpUrl) {
51
52
  };
52
53
  }
53
54
 
54
- /**
55
- * THE POSTURES THAT READ NONE OF THE PERSON'S CONFIG — the branches of `args()`
56
- * below that pass `--ignore-user-config`, said once so the adapter and its
57
- * `personal` hook cannot disagree about which turns carry the person's login
58
- * back (codexPersonal.mjs). A wiki turn with no vault is a build turn there,
59
- * and so here.
60
- */
61
- export const codexIgnoresUserConfig = ({ profile = 'build', vaultDir } = {}) =>
62
- profile === 'consult' || profile === 'plan' || profile === 'image' || profile === 'design' || (profile === 'wiki' && Boolean(vaultDir));
63
-
64
55
  /**
65
56
  * WHAT A FENCED CODEX TURN MAY NOT REACH FOR (0.114.0). codex-cli 0.156.1
66
57
  * ships `browser_use` (and its external and full-CDP forms), an in-app
@@ -69,12 +60,133 @@ export const codexIgnoresUserConfig = ({ profile = 'build', vaultDir } = {}) =>
69
60
  * a browser or the desktop is a way around a filesystem fence. A build turn
70
61
  * keeps the person's own config.
71
62
  */
72
- const NO_REACH_OUT = [
63
+ export const NO_REACH_OUT = Object.freeze([
73
64
  '-c', 'features.browser_use=false',
74
65
  '-c', 'features.browser_use_external=false',
75
66
  '-c', 'features.in_app_browser=false',
76
67
  '-c', 'features.computer_use=false',
77
68
  '-c', 'features.apps=false',
69
+ ]);
70
+
71
+ /**
72
+ * THE HOLE NO SANDBOX COVERS. `web_search` ships in `codex exec`'s default
73
+ * tool list even without --search, and it executes SERVER-SIDE at OpenAI — no
74
+ * local sandbox touches it. An injected turn could pack what it read into a
75
+ * query and egress it straight past every filesystem and network fence. Both
76
+ * spellings, because the two config systems disagree about which one is live.
77
+ */
78
+ export const NO_WEB = Object.freeze(['-c', 'tools.web_search=false', '-c', 'web_search="disabled"']);
79
+
80
+ /** Sub-agents would be a second turn whose posture nobody here chose. */
81
+ export const NO_SUBAGENTS = Object.freeze(['-c', 'features.multi_agent=false', '-c', 'features.goals=false']);
82
+
83
+ /**
84
+ * HERMETIC. Without these a user's ~/.codex/config.toml, a project `.rules`
85
+ * execpolicy file, or their own MCP servers can widen a posture we are
86
+ * asserting on their behalf — silently, and on the turns whose prompt comes
87
+ * from someone else's typing. Last in every fence: the person's login rides
88
+ * back right after `--ignore-user-config` (see `args()`).
89
+ */
90
+ export const HERMETIC = Object.freeze(['--ignore-user-config', '--ignore-rules']);
91
+
92
+ /**
93
+ * THE TAIL EVERY FENCED POSTURE ENDS IN (2026-10-10, the audit), built once so
94
+ * no fence can drift from it: no web unless the posture IS the write-up
95
+ * card's (`web: true`, 0.117.0), no sub-agents, no browser, desktop or apps,
96
+ * image generation off unless the posture IS the image card's, and nobody's
97
+ * config or rules. It was hand-copied into five branches, and the
98
+ * wiki's copy had already lost the web and sub-agent closures.
99
+ *
100
+ * Image generation ships ON in codex-cli 0.156.1 even under
101
+ * --ignore-user-config (measured 2026-09-29): a turn that asked for no picture
102
+ * is handed no tool that spends image credits, and the one that did must not
103
+ * depend on the person's config to have it.
104
+ */
105
+ export const fencedTail = ({ imageGeneration = false, web = false } = {}) => [
106
+ // The web stays on for one posture alone, the write-up card's, whose fence
107
+ // reads only a copied view (CODEX_FENCES `research`).
108
+ ...(web ? [] : NO_WEB),
109
+ ...NO_SUBAGENTS,
110
+ ...NO_REACH_OUT,
111
+ imageGeneration ? '--enable' : '--disable',
112
+ 'image_generation',
113
+ ...HERMETIC,
114
+ ];
115
+
116
+ /**
117
+ * THE FENCED POSTURES, AS A PREDICATE (2026-10-10, the audit) — which turns
118
+ * run under a fence of Flowviant's (and so read none of the person's config),
119
+ * answered WITHOUT building argv: the `personal` hook asks with only the
120
+ * profile and the vault, and the image and design builders need a worktree.
121
+ * `args()` picks its fence builder by the same keys (CODEX_FENCES below), so
122
+ * the hook that carries the person's login back and the argv that ignores
123
+ * their file cannot disagree. A wiki turn with no vault has no fence to build
124
+ * and is a build turn, here and in `args()`.
125
+ */
126
+ export const CODEX_FENCED = Object.freeze({
127
+ consult: () => true,
128
+ plan: () => true,
129
+ wiki: ({ vaultDir } = {}) => Boolean(vaultDir),
130
+ image: () => true,
131
+ design: () => true,
132
+ research: () => true,
133
+ });
134
+
135
+ export const codexIgnoresUserConfig = ({ profile = 'build', vaultDir } = {}) =>
136
+ Object.hasOwn(CODEX_FENCED, profile) && CODEX_FENCED[profile]({ vaultDir });
137
+
138
+ /**
139
+ * READ-ONLY, KERNEL-ENFORCED — the consult's and the plan's filesystem.
140
+ *
141
+ * Claude gets a VERB allowlist: Read, Grep, Glob and a handful of read-only
142
+ * Bash forms. Codex has no such thing — it has no file-read tool at all, so
143
+ * reading the repo IS command execution (`cat`, `rg`). Removing the shell
144
+ * leaves the model with exactly ["update_plan","request_user_input"], which
145
+ * cannot answer a question about a codebase. You get both capabilities or
146
+ * neither.
147
+ *
148
+ * So the promise is kept one layer down instead. `read-only` is
149
+ * kernel-enforced (bubblewrap + seccomp on Linux): writes fail, and a
150
+ * direct-IP connect fails with "Operation not permitted" — socket() is
151
+ * denied, not merely DNS. An injected command still RUNS, and cannot write
152
+ * or send anything itself.
153
+ *
154
+ * THE WAY OUT IS THE ANSWER. What such a turn reads can leave the box in
155
+ * the words it hands back — a consult's reply, a plan's proposal, and
156
+ * intake's drafted brief, which lands on the board with no press — and this
157
+ * profile READS the whole box (see `codexArtifactFence`'s STATED note). Those
158
+ * words are scrubbed on the way up (uplinkScrub.mjs) — of the machine's known
159
+ * secret values and the token shapes that scrub knows, and of nothing else; it
160
+ * is that scrub, not this sandbox, that stands between a read secret and the
161
+ * board.
162
+ */
163
+ const READ_ONLY_SANDBOX = ['-c', 'sandbox_mode="read-only"'];
164
+
165
+ /**
166
+ * THE CARTOGRAPHER: read the repo, write ONLY the vault. Codex's permission
167
+ * profiles take a per-path filesystem map, so the rule is enforced by the
168
+ * kernel. Measured: repo readable, vault writable, repo writes fail
169
+ * "Read-only file system", and curl returns 000 — the network is off. The
170
+ * vault is an uplink (every page is synced to the project's wiki), so web
171
+ * search and sub-agents are shut here as on every other fence (2026-10-10:
172
+ * this copy had drifted without them, and no ruling ever granted them). The
173
+ * write covers the vault's own `.git` too, which is why the daemon's git
174
+ * there runs nothing the vault's config names (vault.mjs `vaultGit`).
175
+ *
176
+ * The table is set WHOLE rather than as a dotted key, and that is not style:
177
+ * `-c permissions.wiki.filesystem."<path>"="write"` splits the dotted path on
178
+ * the dots INSIDE the path, and every real vault lives under
179
+ * `~/.flowviant/vaults/…`. It fails with "filesystem path must be absolute",
180
+ * which reads like a path problem and is a parsing one. The path is a TOML
181
+ * basic string (JSON's escapes), as the artifact fences' are. Selected by
182
+ * config: `codex exec` (0.156.1) has no `-P`, so every Codex wiki turn was
183
+ * refused before it started (0.114.0).
184
+ */
185
+ const codexWikiFence = (vaultDir) => [
186
+ '-c', 'permissions.flowviantwiki.extends=":read-only"',
187
+ '-c', `permissions.flowviantwiki.filesystem={${JSON.stringify(vaultDir)}="write"}`,
188
+ '-c', 'default_permissions="flowviantwiki"',
189
+ ...fencedTail(),
78
190
  ];
79
191
 
80
192
  export const CODEX_RUNTIME = {
@@ -91,12 +203,12 @@ export const CODEX_RUNTIME = {
91
203
  * a verb allowlist, and for a consult's actual threat (exfiltration driven
92
204
  * by an injected question) that is sufficient and arguably stronger.
93
205
  *
94
- * ALL THREE, and `wiki` is the one worth pausing on: Codex expresses it
95
- * MORE strictly than Claude does. A cartographer must read the repo and
96
- * write only the vault; Claude's WIKI_PERM cannot path-scope Write and says
97
- * so, leaning on the worktree reset as a backstop. Codex's permission
98
- * profiles enforce the same rule in the kernel — measured: repo readable,
99
- * vault writable, repo writes refused, network off.
206
+ * ALL THREE, and `wiki` is the one worth pausing on: a cartographer must
207
+ * read the repo and write only the vault. Claude's list says so in rules
208
+ * (`wikiPermFor`, probed 2026-10-10); Codex's permission profiles enforce
209
+ * the write half in the kernel — measured: repo readable, vault writable,
210
+ * repo writes refused, network off — while reading the whole box, as every
211
+ * Codex fence does (see `codexArtifactFence`).
100
212
  *
101
213
  * Enforcement was verified on Linux (bubblewrap + seccomp). macOS Seatbelt
102
214
  * and Windows are UNTESTED; if this daemon starts running there, re-verify
@@ -107,14 +219,23 @@ export const CODEX_RUNTIME = {
107
219
  * Claude's, because the pictures come from Codex's own image tool.
108
220
  *
109
221
  * AND `design` (0.115.0), shared with Claude: the mockup, 3D-model and deck
110
- * cards' fence, expressed as `codexDesignFence` below. Research is NOT
111
- * declared: its web and Codex's whole-box reads cannot be fenced apart
112
- * (see that function).
222
+ * cards' fence, expressed as `codexDesignFence` below.
223
+ * AND `research` (0.117.0): live web over a copied view under a root-denied
224
+ * Linux tool fence (`codexResearch.mjs`), outside the login and checkout.
113
225
  */
114
- profiles: ['build', 'consult', 'wiki', 'plan', 'image', 'design'],
226
+ profiles: ['build', 'consult', 'wiki', 'plan', 'image', 'design', 'research'],
115
227
  /** `model_reasoning_effort` tops out at `xhigh` — there is no `max`, and a
116
228
  * queued or skewed job naming it would be an argument Codex refuses. */
117
229
  efforts: ['low', 'medium', 'high', 'xhigh'],
230
+ /**
231
+ * A TERMINAL TAB ON CODEX (audit 2026-10-10, F159; see runtimeClaude.mjs
232
+ * for the three facts): an MCP tab like Claude's, with no plan mode (there
233
+ * is no flag to spell it) and no adoption — Codex has no adoptable
234
+ * terminal store yet, and `args()` backstops an adopt id with a throw.
235
+ */
236
+ tab: 'mcp',
237
+ planTurns: false,
238
+ adopt: null,
118
239
  mcp: codexMcp,
119
240
  /**
120
241
  * Codex has NO system-prompt flag. The contract therefore rides inside the
@@ -131,9 +252,11 @@ export const CODEX_RUNTIME = {
131
252
  * the prompt always carries them too. Workbench tabs keep their normal
132
253
  * checkout discovery.
133
254
  *
134
- * `--skip-git-repo-check` is deliberately NOT passed: a task always builds
135
- * in a git worktree, and if it somehow is not one, failing loudly beats
136
- * silently editing files nobody can diff.
255
+ * `--skip-git-repo-check` is deliberately NOT passed by the build or any
256
+ * fence but research's: a task always builds in a git worktree, and if it
257
+ * somehow is not one, failing loudly beats silently editing files nobody
258
+ * can diff. A write-up reads a COPY with no `.git` and never resumes
259
+ * (codexResearch.mjs), so its fence passes it.
137
260
  *
138
261
  * THE PROMPT IS LAST, and that is load-bearing rather than tidy. Codex takes
139
262
  * it as a trailing POSITIONAL, so every flag — including the two `-c` MCP
@@ -141,7 +264,7 @@ export const CODEX_RUNTIME = {
141
264
  * placed before it. Appending them after the positional is the kind of argv
142
265
  * that parses today and stops parsing on some future clap upgrade.
143
266
  */
144
- args({ prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, cwd, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools, personal = null }) {
267
+ args({ prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, cwd, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools, personal = null, researchIsolation }) {
145
268
  // Adoption resumes a conversation in ITS OWN CLI's store (claude forks,
146
269
  // agy moves) — codex has no adoptable store wired yet. Reaching here
147
270
  // with an adopt id is a wiring mistake upstream, and it fails loudly on
@@ -169,166 +292,11 @@ export const CODEX_RUNTIME = {
169
292
  // Effort is a config value on Codex rather than a flag.
170
293
  if (effort) a.push('-c', `model_reasoning_effort="${effort}"`);
171
294
 
172
- if (profile === 'consult') {
173
- // A CONSULT, EXPRESSED THE ONLY WAY CODEX CAN EXPRESS IT — and it is a
174
- // different shape from Claude's, which is the whole reason `profile` is
175
- // a promise rather than a flag list.
176
- //
177
- // Claude gets a VERB allowlist: Read, Grep, Glob and a handful of
178
- // read-only Bash forms, with nothing that reaches the network. Codex has
179
- // no such thing — it has no file-read tool at all, so reading the repo
180
- // IS command execution (`cat`, `rg`). Removing the shell leaves the
181
- // model with exactly ["update_plan","request_user_input"], which cannot
182
- // answer a question about a codebase. You get both capabilities or
183
- // neither.
184
- //
185
- // So the promise is kept one layer down instead. `read-only` is
186
- // kernel-enforced (bubblewrap + seccomp on Linux): writes fail, and a
187
- // direct-IP connect fails with "Operation not permitted" — socket() is
188
- // denied, not merely DNS. An injected command still RUNS and still
189
- // cannot take the repository anywhere, which is the threat a consult
190
- // actually has: its prompt is steered by a question any project editor
191
- // can type. Arguably a stronger guarantee than the allowlist, being
192
- // below the agent rather than inside it.
193
- a.push('-c', 'sandbox_mode="read-only"');
194
-
195
- // THE HOLE THE SANDBOX DOES NOT COVER. `web_search` ships in `codex
196
- // exec`'s default tool list even without --search, and it executes
197
- // SERVER-SIDE at OpenAI — no local sandbox touches it. An injected turn
198
- // could pack repo contents into a query and egress them straight past
199
- // everything above. Both spellings, because the two config systems
200
- // disagree about which one is live.
201
- a.push('-c', 'tools.web_search=false', '-c', 'web_search="disabled"');
202
-
203
- // Sub-agents would be a second turn whose posture nobody here chose.
204
- a.push('-c', 'features.multi_agent=false', '-c', 'features.goals=false', ...NO_REACH_OUT);
205
- // Image generation ships ON in codex-cli 0.156.1 even under
206
- // --ignore-user-config (measured 2026-09-29): a turn that asked for no
207
- // picture is handed no tool that spends image credits.
208
- a.push('--disable', 'image_generation');
209
-
210
- // HERMETIC. Without these a user's ~/.codex/config.toml, a project
211
- // `.rules` execpolicy file, or their own MCP servers can widen a posture
212
- // we are asserting on their behalf — silently, and on the one turn whose
213
- // prompt comes from someone else's typing.
214
- a.push('--ignore-user-config', '--ignore-rules');
215
- } else if (profile === 'plan') {
216
- // A PLANNING SESSION. Read-only on the filesystem, exactly like a
217
- // consult — the writes it makes go through the control plane, not
218
- // through this box — so the kernel sandbox is the same one, and for the
219
- // same reason: this turn's prompt is steered by anything a project
220
- // editor can type.
221
- //
222
- // Everything the consult branch above closes stays closed, and the
223
- // reasoning is unchanged, so it is not restated: web_search egresses
224
- // server-side at OpenAI where no local sandbox reaches it, sub-agents
225
- // would be a turn whose posture nobody here chose, and a user's own
226
- // config or MCP servers must not widen a posture we are asserting on
227
- // their behalf.
228
- //
229
- // What differs from a consult is the ONE thing this profile exists for:
230
- // an MCP config IS passed, carrying this turn's scoped token. The plan
231
- // token has the planning tools; a capture token has only staging and
232
- // transcript tools. The server refuses everything outside that scope.
233
- a.push('-c', 'sandbox_mode="read-only"');
234
- a.push('-c', 'tools.web_search=false', '-c', 'web_search="disabled"');
235
- a.push('-c', 'features.multi_agent=false', '-c', 'features.goals=false', ...NO_REACH_OUT);
236
- // Image generation ships ON in codex-cli 0.156.1 even under
237
- // --ignore-user-config (measured 2026-09-29): a turn that asked for no
238
- // picture is handed no tool that spends image credits.
239
- a.push('--disable', 'image_generation');
240
- a.push('--ignore-user-config', '--ignore-rules');
241
- } else if (profile === 'wiki' && vaultDir) {
242
- // THE CARTOGRAPHER, AND THIS ONE IS STRICTER THAN CLAUDE'S.
243
- //
244
- // A wiki turn reads the whole repo and writes ONLY the vault. Claude
245
- // cannot actually express that: WIKI_PERM hands it Write/Edit and its own
246
- // comment admits "Write/Edit can't be path-scoped here; the worktree
247
- // reset is the backstop" — i.e. the cartographer CAN scribble on the
248
- // checkout and we clean up afterwards.
249
- //
250
- // Codex's permission profiles take a per-path filesystem map, so the
251
- // rule is enforced by the kernel instead of apologised for. Measured:
252
- // repo readable, vault writable, repo writes fail "Read-only file
253
- // system", and curl returns 000 — the network is off, which is the half
254
- // WIKI_PERM was really protecting (its comment: "Command execution is
255
- // the line: it enables network exfil").
256
- //
257
- // The table is set WHOLE rather than as a dotted key, and that is not
258
- // style: `-c permissions.wiki.filesystem."<path>"="write"` splits the
259
- // dotted path on the dots INSIDE the path, and every real vault lives
260
- // under `~/.flowviant/vaults/…`. It fails with "filesystem path must be
261
- // absolute", which reads like a path problem and is a parsing one.
262
- a.push('-c', 'permissions.flowviantwiki.extends=":read-only"');
263
- a.push('-c', `permissions.flowviantwiki.filesystem={"${vaultDir}"="write"}`);
264
- // Selected by config: `codex exec` (0.156.1) has no `-P`, so every Codex
265
- // wiki turn was refused before it started (0.114.0).
266
- a.push('-c', 'default_permissions="flowviantwiki"');
267
- a.push('--disable', 'image_generation');
268
- a.push(...NO_REACH_OUT);
269
- a.push('--ignore-user-config', '--ignore-rules');
270
- } else if (profile === 'image') {
271
- // AN IMAGE CARD (0.114.0): read the repo, write ONLY the worktree's
272
- // `.flowviant/artifacts/`, generate pictures, and nothing else. The
273
- // cartographer's shape — a permission profile whose filesystem map the
274
- // kernel enforces — pointed at the artifacts directory instead of a
275
- // vault. MEASURED on codex-cli 0.156.1 (Linux, bubblewrap), with no model
276
- // turn spent: under this profile a `cp` from a CODEX_HOME-shaped folder
277
- // into the artifacts directory landed, a write to a repo file failed
278
- // "Read-only file system", and curl got no network; `codex debug
279
- // prompt-input` with these overrides renders a managed profile of
280
- // `write <worktree>/.flowviant/artifacts` + `read :root`, "Approval
281
- // policy is currently never".
282
- //
283
- // SELECTED BY `default_permissions`, NOT `-P`: `codex exec` (and `exec
284
- // resume`) on 0.156.1 refuse `-P` as an unexpected argument — only
285
- // `codex sandbox` takes it — while `-c default_permissions="<name>"`
286
- // resolves the named profile in both. The directory must EXIST before
287
- // spawn (measured: a missing one cannot be created from inside the
288
- // fence), which the agent lane sees to (`ensureArtifactDir`).
289
- //
290
- // WHERE THE PICTURES COME FROM. Codex's own `image_gen` tool saves each
291
- // one under `$CODEX_HOME/generated_images/…` and says the path (the
292
- // binary's own words: "Generated images are saved to … by default. If
293
- // you need to use a generated image at another path, copy it"). An agent
294
- // turn's CODEX_HOME is the agent's own isolated home (projectToolRuntimes,
295
- // kept per agent since 2026-09-29); `:root` reads admit it, so the agent
296
- // copies what it keeps into the one writable directory, where the
297
- // artifact scan finds it.
298
- //
299
- // IMAGE GENERATION IS SWITCHED ON HERE AND ONLY HERE — a decision. This
300
- // is the one turn whose card asks for pictures, so it must not depend
301
- // on whatever the person's config says about the feature. A BUILD turn
302
- // keeps the person's own config (not hermetic, by design — see below),
303
- // and the other fenced postures (consult, plan, wiki) never asked for a
304
- // picture, so none of them is handed a tool that spends image credits.
305
- if (!cwd || !isAbsolute(cwd)) {
306
- throw new Error("an image turn needs its worktree to fence — none reached codex's builder");
307
- }
308
- const artifacts = join(cwd, ARTIFACT_DIR);
309
- a.push('-c', 'permissions.flowviantimage.extends=":read-only"');
310
- // The table WHOLE (the wiki branch's reason: a dotted key splits on the
311
- // dots inside the path), the path as a TOML basic string.
312
- // …and the fence's own scratch (2026-09-29, the design fence's
313
- // measurement: a `cat > x <<EOF` needs a temp file, and /tmp is
314
- // read-only in here), named for the shells through TMPDIR/TMPPREFIX.
315
- const scratch = join(cwd, FENCE_TMP_DIR);
316
- a.push('-c', `permissions.flowviantimage.filesystem={${JSON.stringify(artifacts)}="write",${JSON.stringify(scratch)}="write"}`);
317
- a.push('-c', `shell_environment_policy.set={TMPDIR=${JSON.stringify(scratch)},TMPPREFIX=${JSON.stringify(join(scratch, 'zsh'))}}`);
318
- a.push('-c', 'default_permissions="flowviantimage"');
319
- a.push(...NO_REACH_OUT);
320
- // Nobody is on this end of the pipe to approve an escalation.
321
- a.push('-c', 'approval_policy="never"');
322
- a.push('--enable', 'image_generation');
323
- // No web: the consult branch's two spellings, for its reason — web
324
- // search runs server-side where no sandbox reaches it.
325
- a.push('-c', 'tools.web_search=false', '-c', 'web_search="disabled"');
326
- a.push('-c', 'features.multi_agent=false', '-c', 'features.goals=false', ...NO_REACH_OUT);
327
- a.push('--ignore-user-config', '--ignore-rules');
328
- } else if (profile === 'design') {
329
- // A MOCKUP, 3D-MODEL OR DECK CARD (0.115.0): the fence is
330
- // `codexDesignFence`'s, argued and measured at its definition below.
331
- a.push(...codexDesignFence(cwd));
295
+ // THE FENCE, picked by the same keys as `codexIgnoresUserConfig`: a fenced
296
+ // posture builds its own (CODEX_FENCES below); anything else is a build.
297
+ const fenced = codexIgnoresUserConfig({ profile, vaultDir });
298
+ if (fenced) {
299
+ a.push(...CODEX_FENCES[profile]({ cwd, vaultDir, researchIsolation }));
332
300
  } else {
333
301
  // The daemon's build posture, mapped: SAFE keeps writes inside the
334
302
  // workspace, the default lets the agent run its own tests and git
@@ -376,11 +344,14 @@ export const CODEX_RUNTIME = {
376
344
  };
377
345
 
378
346
  /**
379
- * THE MOCKUP, 3D-MODEL AND DECK CARDS' FENCE ON CODEX (0.115.0; 2026-09-29, the
380
- * owner: "its saying 3d models run on claude only but thats not true").
347
+ * THE ARTIFACT CARDS' FENCE ON CODEX — the image card's (0.114.0, the image
348
+ * tool switched on) and the mockup, 3D-model and deck cards' (0.115.0;
349
+ * 2026-09-29, the owner: "its saying 3d models run on claude only but thats
350
+ * not true"), one builder under each card's own profile name since 2026-10-10.
381
351
  *
382
- * The image fence's shape without the image tool: a permission profile that
383
- * reads the repo and writes ONLY `<worktree>/.flowviant/artifacts/`, selected
352
+ * The design card's shape is the image fence's without the image tool: a
353
+ * permission profile that reads the repo and writes ONLY
354
+ * `<worktree>/.flowviant/artifacts/` (and the turn's scratch), selected
384
355
  * by `default_permissions` (`codex exec` and `exec resume` refuse `-P` on
385
356
  * 0.156.1), no network, no web search, no sub-agents, no browser, computer or
386
357
  * apps, nobody's config or rules, and no approvals to wait on. Nothing about
@@ -423,15 +394,16 @@ export const CODEX_RUNTIME = {
423
394
  * descriptor"). With the network and the web shut, the one road out is the
424
395
  * artifact itself, which is scrubbed of the machine's known secret values and
425
396
  * of any Flowviant credential by shape on the way up — the image fence's
426
- * standing (0.114.0), and why RESEARCH is not declared here: a write-up needs
427
- * the web, and a turn that can read `.env` and `~/.flowviant/credentials.json`
428
- * while holding `web_search` (both measured readable under this profile with
429
- * `web_search="live"`) is the exfiltration Claude's research list was fenced
430
- * to prevent.
397
+ * standing (0.114.0), and why RESEARCH does not use this fence: a write-up
398
+ * needs the web, and a turn that can read `.env` and
399
+ * `~/.flowviant/credentials.json` while holding `web_search` (both measured
400
+ * readable under this profile with `web_search="live"`) is the exfiltration
401
+ * Claude's research list was fenced to prevent. Codex research (0.117.0) runs
402
+ * under its own fence instead, over a copied view (CODEX_FENCES `research`).
431
403
  */
432
- export function codexDesignFence(cwd) {
404
+ export function codexArtifactFence(cwd, { name, imageGeneration = false, what }) {
433
405
  if (!cwd || !isAbsolute(cwd)) {
434
- throw new Error("a design turn needs its worktree to fence — none reached codex's builder");
406
+ throw new Error(`${what} needs its worktree to fence — none reached codex's builder`);
435
407
  }
436
408
  const artifacts = join(cwd, ARTIFACT_DIR);
437
409
  // THE TURN'S OWN SCRATCH (artifacts.mjs `ensureFenceScratch`, made empty by
@@ -443,17 +415,85 @@ export function codexDesignFence(cwd) {
443
415
  // `shell_environment_policy.set` — measured to reach the sandboxed shell.
444
416
  const scratch = join(cwd, FENCE_TMP_DIR);
445
417
  return [
446
- '-c', 'permissions.flowviantdesign.extends=":read-only"',
447
- // The table WHOLE and the path a TOML basic string — the wiki branch's
418
+ '-c', `permissions.${name}.extends=":read-only"`,
419
+ // The table WHOLE and the path a TOML basic string — the wiki fence's
448
420
  // reason: a dotted key splits on the dots inside the path.
449
- '-c', `permissions.flowviantdesign.filesystem={${JSON.stringify(artifacts)}="write",${JSON.stringify(scratch)}="write"}`,
421
+ '-c', `permissions.${name}.filesystem={${JSON.stringify(artifacts)}="write",${JSON.stringify(scratch)}="write"}`,
450
422
  '-c', `shell_environment_policy.set={TMPDIR=${JSON.stringify(scratch)},TMPPREFIX=${JSON.stringify(join(scratch, 'zsh'))}}`,
451
- '-c', 'default_permissions="flowviantdesign"',
423
+ '-c', `default_permissions="${name}"`,
424
+ // Nobody is on this end of the pipe to approve an escalation.
452
425
  '-c', 'approval_policy="never"',
453
- '--disable', 'image_generation',
454
- '-c', 'tools.web_search=false', '-c', 'web_search="disabled"',
455
- '-c', 'features.multi_agent=false', '-c', 'features.goals=false',
456
- ...NO_REACH_OUT,
457
- '--ignore-user-config', '--ignore-rules',
426
+ ...fencedTail({ imageGeneration }),
458
427
  ];
459
428
  }
429
+
430
+ /** The design card's fence — `codexArtifactFence` under its own profile name,
431
+ * with no image tool. */
432
+ export const codexDesignFence = (cwd) =>
433
+ codexArtifactFence(cwd, { name: 'flowviantdesign', what: 'a design turn' });
434
+
435
+ /**
436
+ * EVERY FENCED POSTURE'S ARGV, by the keys of CODEX_FENCED (a test holds the
437
+ * two tables to one key set), each built from the shared pieces above.
438
+ */
439
+ const CODEX_FENCES = {
440
+ // A CONSULT, EXPRESSED THE ONLY WAY CODEX CAN EXPRESS IT — a different
441
+ // shape from Claude's verb allowlist, which is the whole reason `profile` is
442
+ // a promise rather than a flag list. See READ_ONLY_SANDBOX.
443
+ consult: () => [...READ_ONLY_SANDBOX, ...fencedTail()],
444
+ // A PLANNING SESSION. Read-only on the filesystem, exactly like a consult —
445
+ // the writes it makes go through the control plane, not through this box —
446
+ // and for the same reason: its prompt is steered by anything a project
447
+ // editor can type. What differs is the ONE thing this profile exists for:
448
+ // an MCP config IS passed (by the caller, after the fence), carrying this
449
+ // turn's scoped token. The plan token has the planning tools; a capture
450
+ // token has only staging and transcript tools. The server refuses
451
+ // everything outside that scope.
452
+ plan: () => [...READ_ONLY_SANDBOX, ...fencedTail()],
453
+ wiki: ({ vaultDir }) => codexWikiFence(vaultDir),
454
+ // AN IMAGE CARD (0.114.0): read the repo, write ONLY the worktree's
455
+ // `.flowviant/artifacts/`, generate pictures, and nothing else. MEASURED on
456
+ // codex-cli 0.156.1 (Linux, bubblewrap), with no model turn spent: under
457
+ // this profile a `cp` from a CODEX_HOME-shaped folder into the artifacts
458
+ // directory landed, a write to a repo file failed "Read-only file system",
459
+ // and curl got no network; `codex debug prompt-input` with these overrides
460
+ // renders a managed profile of `write <worktree>/.flowviant/artifacts` +
461
+ // `read :root`, "Approval policy is currently never".
462
+ //
463
+ // SELECTED BY `default_permissions`, NOT `-P`: `codex exec` (and `exec
464
+ // resume`) on 0.156.1 refuse `-P` as an unexpected argument — only `codex
465
+ // sandbox` takes it — while `-c default_permissions="<name>"` resolves the
466
+ // named profile in both. The directory must EXIST before spawn (measured: a
467
+ // missing one cannot be created from inside the fence), which the agent lane
468
+ // sees to (`ensureArtifactDir`).
469
+ //
470
+ // WHERE THE PICTURES COME FROM. Codex's own `image_gen` tool saves each one
471
+ // under `$CODEX_HOME/generated_images/…` and says the path (the binary's own
472
+ // words: "Generated images are saved to … by default. If you need to use a
473
+ // generated image at another path, copy it"). An agent turn's CODEX_HOME is
474
+ // the agent's own isolated home (projectToolRuntimes, kept per agent since
475
+ // 2026-09-29); `:root` reads admit it, so the agent copies what it keeps
476
+ // into the one writable directory, where the artifact scan finds it.
477
+ //
478
+ // IMAGE GENERATION IS SWITCHED ON HERE AND ONLY HERE — a decision. This is
479
+ // the one turn whose card asks for pictures, so it must not depend on
480
+ // whatever the person's config says about the feature. A BUILD turn keeps
481
+ // the person's own config (not hermetic, by design), and the other fenced
482
+ // postures never asked for a picture, so none of them is handed a tool that
483
+ // spends image credits.
484
+ image: ({ cwd }) => codexArtifactFence(cwd, { name: 'flowviantimage', imageGeneration: true, what: 'an image turn' }),
485
+ // A MOCKUP, 3D-MODEL OR DECK CARD (0.115.0): the fence is
486
+ // `codexDesignFence`'s, argued and measured at `codexArtifactFence` above.
487
+ design: ({ cwd }) => codexDesignFence(cwd),
488
+ // A WRITE-UP CARD (0.117.0): live web over a COPIED view of the worktree,
489
+ // never the checkout or the login store, under a root-denied tool fence
490
+ // (codexResearch.mjs `researchFence`, built by runTurn's
491
+ // `runIsolatedResearch`). It is the one fence that keeps the web, so its
492
+ // tail leaves NO_WEB out and closes everything else.
493
+ research: ({ researchIsolation }) => {
494
+ if (!researchIsolation) throw new Error('Codex research requires its isolated input copy.');
495
+ return [...researchFence(researchIsolation), ...fencedTail({ web: true })];
496
+ },
497
+ };
498
+ /** The fenced profile names — for the test that holds both tables to one set. */
499
+ export const CODEX_FENCE_PROFILES = Object.freeze(Object.keys(CODEX_FENCES));
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  import { execFileSync } from 'node:child_process';
17
- import { RUNTIMES, canRun, drivableHere } from './runtimes.mjs';
17
+ import { RUNTIMES, canRun, drivableHere, runtimeById } from './runtimes.mjs';
18
18
 
19
19
  /**
20
20
  * WHICH RUNTIME RUNS A JOB THAT NOBODY @MENTIONED.
@@ -41,7 +41,7 @@ import { RUNTIMES, canRun, drivableHere } from './runtimes.mjs';
41
41
  export function pickRuntimeFor(profile, { detected } = {}) {
42
42
  const rows = detected ?? detectRuntimes();
43
43
  const ok = (id) =>
44
- canRun(RUNTIMES[id], profile) && Boolean(rows.find((d) => d.id === id)?.installed);
44
+ canRun(runtimeById(id), profile) && Boolean(rows.find((d) => d.id === id)?.installed);
45
45
  if (ok('claude')) return 'claude';
46
46
  return Object.keys(RUNTIMES).find(ok) ?? null;
47
47
  }
@@ -86,14 +86,20 @@ export function humanizeClaudeTool(name, input = {}, cwd = '') {
86
86
  case 'LS':
87
87
  return { kind: 'list', label: `ls ${shortPath(input.path ?? '.', cwd)}` };
88
88
  case 'Bash':
89
- // `command` rides beside the display label, VERBATIM (capped): the label
90
- // is a 60-char readout for a console and the rail, and truncation is
91
- // fine there — but the admin's command audit exists to answer "what
89
+ // `command` rides beside the display label, VERBATIM (windowed): the
90
+ // label is a 60-char readout for a console and the rail, and truncation
91
+ // is fine there — but the admin's command audit exists to answer "what
92
92
  // actually ran on our box", and an ellipsis is exactly where the part
93
93
  // that matters would hide.
94
+ //
95
+ // WINDOWED, NOT CAPPED (2026-10-10, the audit's F128). This cut at the
96
+ // server's 2000 before the audit scrubbed it — the cap-then-scrub order
97
+ // `toolEventOf` below records as a leak: a secret straddling character
98
+ // 2000 kept its prefix. The window only bounds memory; the audit
99
+ // scrubs, THEN cuts to the server's cap (workCommandAudit.mjs).
94
100
  return {
95
101
  kind: 'bash',
96
- command: String(input.command ?? '').slice(0, 2000),
102
+ command: String(input.command ?? '').slice(0, SCRUB_WINDOW),
97
103
  label: `$ ${oneLine(input.command, 60)}`,
98
104
  };
99
105
  case 'TodoWrite': {
@@ -265,7 +271,7 @@ function humanizeCodexItem(item = {}, cwd = '') {
265
271
  case 'command_execution':
266
272
  return {
267
273
  kind: 'bash',
268
- command: String(item.command ?? '').slice(0, 2000),
274
+ command: String(item.command ?? '').slice(0, SCRUB_WINDOW), // windowed, not capped — see Claude's Bash
269
275
  label: `$ ${oneLine(item.command, 60)}`,
270
276
  };
271
277
  case 'file_change': {
@@ -295,9 +301,9 @@ function humanizeCodexItem(item = {}, cwd = '') {
295
301
 
296
302
  /**
297
303
  * Codex `--json` emits JSONL of ThreadEvents. Returns `{ activity, text,
298
- * threadId }` — `text` accumulates the agent's own words, because the turn
299
- * loop reads its sentinels (NOTHING / BLOCKED:<id> / DONE) out of exactly
300
- * that; `threadId` surfaces once, off the lifecycle event, for callers that
304
+ * threadId }` — `text` accumulates the agent's own words, because the wiki
305
+ * lane reads its sentinels (WIKI_DONE / REGROUND_DONE, claude.mjs
306
+ * `sawSentinel`) out of exactly that; `threadId` surfaces once, off the lifecycle event, for callers that
301
307
  * need to resume THIS conversation later (runTurn's onThreadId).
302
308
  */
303
309
  export function parseCodexLine(line, cwd) {
@@ -320,7 +326,7 @@ export function parseCodexLine(line, cwd) {
320
326
  const item = ev.item ?? {};
321
327
  const activity = humanizeCodexItem(item, cwd);
322
328
  // Only the agent's MESSAGES are sentinel-bearing text. Reasoning is not:
323
- // a model that muses "I could output NOTHING here" must not end the turn.
329
+ // a model that muses "I could print WIKI_DONE now" must not end the turn.
324
330
  const isMessage = (item.item_type ?? item.type) === 'agent_message';
325
331
  const text = isMessage ? `${item.text ?? item.message ?? ''}\n` : '';
326
332
  // `answer` is the SAME message, surfaced alone: `text` accumulates every
@@ -439,7 +445,7 @@ function humanizeAgyTool(name, p = {}, cwd = '') {
439
445
  case 'run_command':
440
446
  return {
441
447
  kind: 'bash',
442
- command: String(p.CommandLine ?? '').slice(0, 2000),
448
+ command: String(p.CommandLine ?? '').slice(0, SCRUB_WINDOW), // windowed, not capped — see Claude's Bash
443
449
  label: `$ ${oneLine(p.CommandLine, 60)}`,
444
450
  };
445
451
  case 'call_mcp_tool':