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.
- package/README.md +31 -12
- package/bin/cli.mjs +13 -17
- package/bin/lib/admission.mjs +7 -14
- package/bin/lib/agentInternalWait.mjs +83 -0
- package/bin/lib/agentPlan.mjs +56 -24
- package/bin/lib/agentPublish.mjs +12 -13
- package/bin/lib/agentTaskKinds.mjs +4 -7
- package/bin/lib/agySessions.mjs +3 -3
- package/bin/lib/artifactPreview.mjs +92 -18
- package/bin/lib/artifactReporter.mjs +16 -8
- package/bin/lib/artifacts.mjs +64 -60
- package/bin/lib/authproxy.mjs +2 -1
- package/bin/lib/childEnv.mjs +9 -4
- package/bin/lib/claude.mjs +10 -20
- package/bin/lib/claudePersonal.mjs +71 -6
- package/bin/lib/claudePosture.mjs +119 -42
- package/bin/lib/claudeSessions.mjs +7 -7
- package/bin/lib/claudeStream.mjs +7 -11
- package/bin/lib/cliLimit.mjs +2 -2
- package/bin/lib/codexHome.mjs +17 -0
- package/bin/lib/codexPersonal.mjs +44 -3
- package/bin/lib/codexRelay.mjs +21 -5
- package/bin/lib/codexResearch.mjs +180 -0
- package/bin/lib/config.mjs +37 -24
- package/bin/lib/conversationIds.mjs +51 -0
- package/bin/lib/credentials.mjs +2 -7
- package/bin/lib/daemonLocks.mjs +88 -0
- package/bin/lib/daemonState.mjs +5 -5
- package/bin/lib/deploy.mjs +6 -9
- package/bin/lib/deployCheckout.mjs +26 -22
- package/bin/lib/deployRunner.mjs +54 -18
- package/bin/lib/deployWire.mjs +32 -23
- package/bin/lib/desktopContract.mjs +3 -5
- package/bin/lib/env.mjs +104 -35
- package/bin/lib/fleet.mjs +7 -9
- package/bin/lib/fleetPost.mjs +172 -25
- package/bin/lib/fleetReports.mjs +130 -170
- package/bin/lib/fleetStartup.mjs +6 -6
- package/bin/lib/fleetWire.mjs +42 -0
- package/bin/lib/git.mjs +111 -61
- package/bin/lib/gitMergeIdentity.mjs +2 -8
- package/bin/lib/grant.mjs +23 -7
- package/bin/lib/holder.mjs +108 -81
- package/bin/lib/hooks/readGuard.mjs +41 -7
- package/bin/lib/install.mjs +80 -33
- package/bin/lib/instance.mjs +36 -83
- package/bin/lib/intakeDraft.mjs +4 -1
- package/bin/lib/knowledgeFileSync.mjs +31 -0
- package/bin/lib/knowledgeFiles.mjs +28 -83
- package/bin/lib/landed.mjs +22 -29
- package/bin/lib/launchCommand.mjs +91 -0
- package/bin/lib/listeners.mjs +0 -5
- package/bin/lib/localSessions.mjs +52 -39
- package/bin/lib/login.mjs +22 -12
- package/bin/lib/loginPath.mjs +22 -17
- package/bin/lib/machineListing.mjs +6 -3
- package/bin/lib/mcp-cli.mjs +9 -10
- package/bin/lib/noWindowEnv.mjs +23 -3
- package/bin/lib/ownedDirs.mjs +99 -0
- package/bin/lib/pinnedCapture.mjs +383 -0
- package/bin/lib/prWorkflow.mjs +94 -17
- package/bin/lib/preflight.mjs +8 -25
- package/bin/lib/preview.mjs +19 -25
- package/bin/lib/previewGatePolicy.mjs +34 -9
- package/bin/lib/previewRegistry.mjs +4 -11
- package/bin/lib/printable.mjs +70 -7
- package/bin/lib/procRegistry.mjs +33 -13
- package/bin/lib/procScan.mjs +2 -3
- package/bin/lib/procStat.mjs +76 -0
- package/bin/lib/processGroup.mjs +48 -0
- package/bin/lib/processes.mjs +10 -24
- package/bin/lib/projectToolRuntimes.mjs +101 -16
- package/bin/lib/projectTools.mjs +50 -32
- package/bin/lib/prompts.mjs +41 -19
- package/bin/lib/repoPath.mjs +19 -0
- package/bin/lib/repoState.mjs +17 -19
- package/bin/lib/runTurn.mjs +268 -210
- package/bin/lib/runtimeAntigravity.mjs +16 -5
- package/bin/lib/runtimeCapabilities.mjs +140 -20
- package/bin/lib/runtimeClaude.mjs +55 -5
- package/bin/lib/runtimeCodex.mjs +245 -205
- package/bin/lib/runtimeDetection.mjs +2 -2
- package/bin/lib/runtimeEvents.mjs +16 -10
- package/bin/lib/runtimeLimits.mjs +5 -6
- package/bin/lib/runtimeOfferings.mjs +24 -11
- package/bin/lib/runtimes.mjs +10 -0
- package/bin/lib/shipSweep.mjs +5 -3
- package/bin/lib/shot.mjs +91 -17
- package/bin/lib/sleep.mjs +11 -0
- package/bin/lib/startProject.mjs +5 -5
- package/bin/lib/statePaths.mjs +40 -0
- package/bin/lib/tty.mjs +25 -15
- package/bin/lib/turnLock.mjs +7 -14
- package/bin/lib/turnProfile.mjs +1 -1
- package/bin/lib/ui.mjs +0 -3
- package/bin/lib/uninstall.mjs +30 -26
- package/bin/lib/update.mjs +79 -27
- package/bin/lib/uplinkScrub.mjs +296 -31
- package/bin/lib/vault.mjs +78 -10
- package/bin/lib/vaultDiff.mjs +5 -2
- package/bin/lib/views.mjs +37 -12
- package/bin/lib/wikiRunner.mjs +40 -61
- package/bin/lib/work.mjs +17 -7
- package/bin/lib/workAdoptCarry.mjs +2 -4
- package/bin/lib/workAgentCheck.mjs +104 -57
- package/bin/lib/workAgentMerges.mjs +62 -57
- package/bin/lib/workAgentPlans.mjs +99 -145
- package/bin/lib/workAgentPrecheck.mjs +71 -138
- package/bin/lib/workAgentPublish.mjs +6 -8
- package/bin/lib/workAgentTurnBegun.mjs +15 -11
- package/bin/lib/workAgentTurnExecution.mjs +46 -53
- package/bin/lib/workAgentTurnReports.mjs +9 -37
- package/bin/lib/workAgentTurns.mjs +4 -3
- package/bin/lib/workAttachments.mjs +66 -11
- package/bin/lib/workBrain.mjs +9 -4
- package/bin/lib/workCappedTurn.mjs +109 -0
- package/bin/lib/workChildren.mjs +5 -5
- package/bin/lib/workCommandAudit.mjs +11 -2
- package/bin/lib/workDiffs.mjs +52 -6
- package/bin/lib/workIntake.mjs +35 -60
- package/bin/lib/workPlaceReports.mjs +7 -4
- package/bin/lib/workPlaces.mjs +96 -30
- package/bin/lib/workPreviews.mjs +47 -112
- package/bin/lib/workProcesses.mjs +8 -35
- package/bin/lib/workPullRequests.mjs +37 -56
- package/bin/lib/workReportQueue.mjs +100 -148
- package/bin/lib/workRetire.mjs +2 -1
- package/bin/lib/workSessionRuntime.mjs +38 -37
- package/bin/lib/workSessionTurns.mjs +111 -189
- package/bin/lib/workShip.mjs +43 -23
- package/bin/lib/workTurnShape.mjs +163 -0
- package/npm-shrinkwrap.json +59 -0
- package/package.json +3 -2
- package/bin/lib/fleetJobs.mjs +0 -120
- package/bin/lib/patchRevert.mjs +0 -60
package/bin/lib/runtimeCodex.mjs
CHANGED
|
@@ -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:
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
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.
|
|
111
|
-
*
|
|
112
|
-
* (
|
|
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
|
|
135
|
-
* in a git worktree, and if it
|
|
136
|
-
* silently editing files nobody
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
380
|
-
*
|
|
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
|
|
383
|
-
* reads the repo and writes ONLY
|
|
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
|
|
427
|
-
* the web, and a turn that can read `.env` and
|
|
428
|
-
* while holding `web_search` (both measured
|
|
429
|
-
* `web_search="live"`) is the exfiltration
|
|
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
|
|
404
|
+
export function codexArtifactFence(cwd, { name, imageGeneration = false, what }) {
|
|
433
405
|
if (!cwd || !isAbsolute(cwd)) {
|
|
434
|
-
throw new Error(
|
|
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',
|
|
447
|
-
// The table WHOLE and the path a TOML basic string — the wiki
|
|
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.
|
|
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',
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
90
|
-
// is a 60-char readout for a console and the rail, and truncation
|
|
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,
|
|
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,
|
|
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
|
|
299
|
-
*
|
|
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
|
|
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,
|
|
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':
|