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.
- package/README.md +66 -12
- package/bin/cli.mjs +50 -17
- package/bin/lib/admission.mjs +7 -14
- package/bin/lib/agentAsk.mjs +123 -0
- package/bin/lib/agentBrief.mjs +36 -0
- package/bin/lib/agentFootprint.mjs +76 -0
- package/bin/lib/agentInternalWait.mjs +83 -0
- package/bin/lib/agentPlan.mjs +134 -31
- package/bin/lib/agentPrerequisiteContract.mjs +43 -0
- package/bin/lib/agentPublish.mjs +12 -13
- package/bin/lib/agentTaskKinds.mjs +5 -8
- package/bin/lib/agySessions.mjs +3 -3
- package/bin/lib/artifactContracts.mjs +91 -20
- package/bin/lib/artifactIteration.mjs +182 -0
- package/bin/lib/artifactPreview.mjs +92 -18
- package/bin/lib/artifactRecovery.mjs +36 -0
- package/bin/lib/artifactReporter.mjs +194 -22
- package/bin/lib/artifacts.mjs +70 -63
- package/bin/lib/authproxy.mjs +2 -1
- package/bin/lib/childEnv.mjs +9 -4
- package/bin/lib/claude.mjs +10 -20
- package/bin/lib/claudeAuth.mjs +15 -5
- package/bin/lib/claudePersonal.mjs +71 -6
- package/bin/lib/claudePosture.mjs +227 -54
- package/bin/lib/claudeSessions.mjs +7 -7
- package/bin/lib/claudeStream.mjs +27 -13
- package/bin/lib/cliLimit.mjs +2 -2
- package/bin/lib/codexHome.mjs +17 -0
- package/bin/lib/codexPersonal.mjs +44 -3
- package/bin/lib/codexProjectRead.mjs +63 -0
- package/bin/lib/codexRelay.mjs +21 -5
- package/bin/lib/codexResearch.mjs +193 -0
- package/bin/lib/commandSpecs.mjs +6 -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 +90 -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 +6 -5
- package/bin/lib/discoveryContract.mjs +19 -0
- package/bin/lib/env.mjs +104 -35
- package/bin/lib/fleet.mjs +53 -13
- package/bin/lib/fleetPost.mjs +172 -25
- package/bin/lib/fleetReports.mjs +172 -171
- 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/mcpAllow.mjs +60 -0
- package/bin/lib/hooks/readGuard.mjs +54 -11
- package/bin/lib/imageArtifact.mjs +74 -0
- 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/maintenance.mjs +47 -0
- package/bin/lib/maintenanceInventory.mjs +39 -0
- package/bin/lib/mcp-cli.mjs +9 -10
- package/bin/lib/mcpInstall.mjs +163 -0
- package/bin/lib/mcpInventory.mjs +119 -0
- package/bin/lib/noWindowEnv.mjs +23 -3
- package/bin/lib/ownedDirs.mjs +99 -0
- package/bin/lib/pinnedCapture.mjs +383 -0
- package/bin/lib/planDependencies.mjs +70 -0
- package/bin/lib/postureTools.mjs +85 -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 +328 -19
- package/bin/lib/projectTools.mjs +109 -34
- package/bin/lib/prompts.mjs +205 -60
- package/bin/lib/repoPath.mjs +19 -0
- package/bin/lib/repoState.mjs +17 -19
- package/bin/lib/repositoryBaseline.mjs +68 -0
- package/bin/lib/runTurn.mjs +286 -209
- package/bin/lib/runtimeAntigravity.mjs +16 -5
- package/bin/lib/runtimeCapabilities.mjs +215 -21
- package/bin/lib/runtimeClaude.mjs +78 -10
- package/bin/lib/runtimeCodex.mjs +217 -209
- package/bin/lib/runtimeDetection.mjs +8 -2
- package/bin/lib/runtimeEvents.mjs +28 -11
- package/bin/lib/runtimeLimits.mjs +5 -6
- package/bin/lib/runtimeOfferings.mjs +99 -14
- 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/turnDiagnostics.mjs +93 -0
- package/bin/lib/turnLock.mjs +7 -14
- package/bin/lib/turnProfile.mjs +16 -4
- 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 +44 -13
- package/bin/lib/workAdoptCarry.mjs +2 -4
- package/bin/lib/workAgentBrief.mjs +92 -0
- package/bin/lib/workAgentBriefRunner.mjs +59 -0
- package/bin/lib/workAgentCheck.mjs +104 -57
- package/bin/lib/workAgentMerges.mjs +62 -57
- package/bin/lib/workAgentPlans.mjs +82 -199
- package/bin/lib/workAgentPrecheck.mjs +76 -138
- package/bin/lib/workAgentPublish.mjs +6 -8
- package/bin/lib/workAgentTurnBegun.mjs +15 -11
- package/bin/lib/workAgentTurnExecution.mjs +131 -67
- package/bin/lib/workAgentTurnOutcome.mjs +11 -2
- package/bin/lib/workAgentTurnReports.mjs +9 -37
- package/bin/lib/workAgentTurns.mjs +64 -7
- package/bin/lib/workArtifactReconcile.mjs +79 -0
- package/bin/lib/workAttachments.mjs +66 -11
- package/bin/lib/workBrain.mjs +9 -4
- package/bin/lib/workCappedTurn.mjs +119 -0
- package/bin/lib/workChildren.mjs +5 -5
- package/bin/lib/workCommandAudit.mjs +18 -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 +113 -31
- 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 +256 -199
- package/bin/lib/workShip.mjs +43 -23
- package/bin/lib/workToolLog.mjs +70 -3
- package/bin/lib/workTurnShape.mjs +165 -0
- package/npm-shrinkwrap.json +69 -0
- package/package.json +9 -4
- package/bin/lib/fleetJobs.mjs +0 -120
- package/bin/lib/patchRevert.mjs +0 -60
package/bin/lib/runtimeCodex.mjs
CHANGED
|
@@ -14,6 +14,8 @@ 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';
|
|
18
|
+
import { projectReadBoundary, projectReadFence } from './codexProjectRead.mjs';
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* FLOWVIANT'S OWN TOOLS NEED NO APPROVAL FROM A CHAT NOBODY CAN ANSWER
|
|
@@ -51,16 +53,6 @@ function codexMcp(token, mcpUrl) {
|
|
|
51
53
|
};
|
|
52
54
|
}
|
|
53
55
|
|
|
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
56
|
/**
|
|
65
57
|
* WHAT A FENCED CODEX TURN MAY NOT REACH FOR (0.114.0). codex-cli 0.156.1
|
|
66
58
|
* ships `browser_use` (and its external and full-CDP forms), an in-app
|
|
@@ -69,12 +61,101 @@ export const codexIgnoresUserConfig = ({ profile = 'build', vaultDir } = {}) =>
|
|
|
69
61
|
* a browser or the desktop is a way around a filesystem fence. A build turn
|
|
70
62
|
* keeps the person's own config.
|
|
71
63
|
*/
|
|
72
|
-
const NO_REACH_OUT = [
|
|
64
|
+
export const NO_REACH_OUT = Object.freeze([
|
|
73
65
|
'-c', 'features.browser_use=false',
|
|
74
66
|
'-c', 'features.browser_use_external=false',
|
|
75
67
|
'-c', 'features.in_app_browser=false',
|
|
76
68
|
'-c', 'features.computer_use=false',
|
|
77
69
|
'-c', 'features.apps=false',
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* THE HOLE NO SANDBOX COVERS. `web_search` ships in `codex exec`'s default
|
|
74
|
+
* tool list even without --search, and it executes SERVER-SIDE at OpenAI — no
|
|
75
|
+
* local sandbox touches it. An injected turn could pack what it read into a
|
|
76
|
+
* query and egress it straight past every filesystem and network fence. Both
|
|
77
|
+
* spellings, because the two config systems disagree about which one is live.
|
|
78
|
+
*/
|
|
79
|
+
export const NO_WEB = Object.freeze(['-c', 'tools.web_search=false', '-c', 'web_search="disabled"']);
|
|
80
|
+
|
|
81
|
+
/** Sub-agents would be a second turn whose posture nobody here chose. */
|
|
82
|
+
export const NO_SUBAGENTS = Object.freeze(['-c', 'features.multi_agent=false', '-c', 'features.goals=false']);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* HERMETIC. Without these a user's ~/.codex/config.toml, a project `.rules`
|
|
86
|
+
* execpolicy file, or their own MCP servers can widen a posture we are
|
|
87
|
+
* asserting on their behalf — silently, and on the turns whose prompt comes
|
|
88
|
+
* from someone else's typing. Last in every fence: the person's login rides
|
|
89
|
+
* back right after `--ignore-user-config` (see `args()`).
|
|
90
|
+
*/
|
|
91
|
+
export const HERMETIC = Object.freeze(['--ignore-user-config', '--ignore-rules']);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Every fenced posture keeps config isolation, desktop/app closure and the
|
|
95
|
+
* delegation policy. Artifact postures explicitly enable native images as a
|
|
96
|
+
* supporting capability; consult, plan and wiki keep them off. Native web
|
|
97
|
+
* needs the isolated research read fence because broad-read tools can read
|
|
98
|
+
* host credentials. Feature flags do not prove account/model entitlement.
|
|
99
|
+
*/
|
|
100
|
+
export const fencedTail = ({ imageGeneration = false, web = false } = {}) => [
|
|
101
|
+
// Native web is paired with the copied, root-denied read fence.
|
|
102
|
+
...(web ? [] : NO_WEB),
|
|
103
|
+
...NO_SUBAGENTS,
|
|
104
|
+
...NO_REACH_OUT,
|
|
105
|
+
imageGeneration ? '--enable' : '--disable',
|
|
106
|
+
'image_generation',
|
|
107
|
+
...HERMETIC,
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* THE FENCED POSTURES, AS A PREDICATE (2026-10-10, the audit) — which turns
|
|
112
|
+
* run under a fence of Flowviant's (and so read none of the person's config),
|
|
113
|
+
* answered WITHOUT building argv: the `personal` hook asks with only the
|
|
114
|
+
* profile and the vault, and the image and design builders need a worktree.
|
|
115
|
+
* `args()` picks its fence builder by the same keys (CODEX_FENCES below), so
|
|
116
|
+
* the hook that carries the person's login back and the argv that ignores
|
|
117
|
+
* their file cannot disagree. A wiki turn with no vault has no fence to build
|
|
118
|
+
* and is a build turn, here and in `args()`.
|
|
119
|
+
*/
|
|
120
|
+
export const CODEX_FENCED = Object.freeze({
|
|
121
|
+
consult: () => true,
|
|
122
|
+
plan: () => true,
|
|
123
|
+
wiki: ({ vaultDir } = {}) => Boolean(vaultDir),
|
|
124
|
+
image: () => true,
|
|
125
|
+
design: () => true,
|
|
126
|
+
research: () => true,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export const codexIgnoresUserConfig = ({ profile = 'build', vaultDir } = {}) =>
|
|
130
|
+
Object.hasOwn(CODEX_FENCED, profile) && CODEX_FENCED[profile]({ vaultDir });
|
|
131
|
+
|
|
132
|
+
/** Consult/capture use root-denied tool reads over the actual project,
|
|
133
|
+
* knowledge and Git metadata. Scrubbing remains supplemental. */
|
|
134
|
+
/**
|
|
135
|
+
* THE CARTOGRAPHER: read the repo, write ONLY the vault. Codex's permission
|
|
136
|
+
* profiles take a per-path filesystem map, so the rule is enforced by the
|
|
137
|
+
* kernel. Measured: repo readable, vault writable, repo writes fail
|
|
138
|
+
* "Read-only file system", and curl returns 000 — the network is off. The
|
|
139
|
+
* vault is an uplink (every page is synced to the project's wiki), so web
|
|
140
|
+
* search and sub-agents are shut here as on every other fence (2026-10-10:
|
|
141
|
+
* this copy had drifted without them, and no ruling ever granted them). The
|
|
142
|
+
* write covers the vault's own `.git` too, which is why the daemon's git
|
|
143
|
+
* there runs nothing the vault's config names (vault.mjs `vaultGit`).
|
|
144
|
+
*
|
|
145
|
+
* The table is set WHOLE rather than as a dotted key, and that is not style:
|
|
146
|
+
* `-c permissions.wiki.filesystem."<path>"="write"` splits the dotted path on
|
|
147
|
+
* the dots INSIDE the path, and every real vault lives under
|
|
148
|
+
* `~/.flowviant/vaults/…`. It fails with "filesystem path must be absolute",
|
|
149
|
+
* which reads like a path problem and is a parsing one. The path is a TOML
|
|
150
|
+
* basic string (JSON's escapes), as the artifact fences' are. Selected by
|
|
151
|
+
* config: `codex exec` (0.156.1) has no `-P`, so every Codex wiki turn was
|
|
152
|
+
* refused before it started (0.114.0).
|
|
153
|
+
*/
|
|
154
|
+
const codexWikiFence = (vaultDir) => [
|
|
155
|
+
'-c', 'permissions.flowviantwiki.extends=":read-only"',
|
|
156
|
+
'-c', `permissions.flowviantwiki.filesystem={${JSON.stringify(vaultDir)}="write"}`,
|
|
157
|
+
'-c', 'default_permissions="flowviantwiki"',
|
|
158
|
+
...fencedTail(),
|
|
78
159
|
];
|
|
79
160
|
|
|
80
161
|
export const CODEX_RUNTIME = {
|
|
@@ -91,12 +172,12 @@ export const CODEX_RUNTIME = {
|
|
|
91
172
|
* a verb allowlist, and for a consult's actual threat (exfiltration driven
|
|
92
173
|
* by an injected question) that is sufficient and arguably stronger.
|
|
93
174
|
*
|
|
94
|
-
* ALL THREE, and `wiki` is the one worth pausing on:
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
175
|
+
* ALL THREE, and `wiki` is the one worth pausing on: a cartographer must
|
|
176
|
+
* read the repo and write only the vault. Claude's list says so in rules
|
|
177
|
+
* (`wikiPermFor`, probed 2026-10-10); Codex's permission profiles enforce
|
|
178
|
+
* the write half in the kernel — measured: repo readable, vault writable,
|
|
179
|
+
* repo writes refused, network off — while reading the whole box, as every
|
|
180
|
+
* Codex fence does (see `codexArtifactFence`).
|
|
100
181
|
*
|
|
101
182
|
* Enforcement was verified on Linux (bubblewrap + seccomp). macOS Seatbelt
|
|
102
183
|
* and Windows are UNTESTED; if this daemon starts running there, re-verify
|
|
@@ -107,14 +188,23 @@ export const CODEX_RUNTIME = {
|
|
|
107
188
|
* Claude's, because the pictures come from Codex's own image tool.
|
|
108
189
|
*
|
|
109
190
|
* AND `design` (0.115.0), shared with Claude: the mockup, 3D-model and deck
|
|
110
|
-
* cards' fence, expressed as `codexDesignFence` below.
|
|
111
|
-
*
|
|
112
|
-
* (
|
|
191
|
+
* cards' fence, expressed as `codexDesignFence` below.
|
|
192
|
+
* AND `research` (0.117.0): live web over a copied view under a root-denied
|
|
193
|
+
* Linux tool fence (`codexResearch.mjs`), outside the login and checkout.
|
|
113
194
|
*/
|
|
114
|
-
profiles: ['build', 'consult', 'wiki', 'plan', 'image', 'design'],
|
|
195
|
+
profiles: ['build', 'consult', 'wiki', 'plan', 'image', 'design', 'research'],
|
|
115
196
|
/** `model_reasoning_effort` tops out at `xhigh` — there is no `max`, and a
|
|
116
197
|
* queued or skewed job naming it would be an argument Codex refuses. */
|
|
117
198
|
efforts: ['low', 'medium', 'high', 'xhigh'],
|
|
199
|
+
/**
|
|
200
|
+
* A TERMINAL TAB ON CODEX (audit 2026-10-10, F159; see runtimeClaude.mjs
|
|
201
|
+
* for the three facts): an MCP tab like Claude's, with no plan mode (there
|
|
202
|
+
* is no flag to spell it) and no adoption — Codex has no adoptable
|
|
203
|
+
* terminal store yet, and `args()` backstops an adopt id with a throw.
|
|
204
|
+
*/
|
|
205
|
+
tab: 'mcp',
|
|
206
|
+
planTurns: false,
|
|
207
|
+
adopt: null,
|
|
118
208
|
mcp: codexMcp,
|
|
119
209
|
/**
|
|
120
210
|
* Codex has NO system-prompt flag. The contract therefore rides inside the
|
|
@@ -131,9 +221,11 @@ export const CODEX_RUNTIME = {
|
|
|
131
221
|
* the prompt always carries them too. Workbench tabs keep their normal
|
|
132
222
|
* checkout discovery.
|
|
133
223
|
*
|
|
134
|
-
* `--skip-git-repo-check` is deliberately NOT passed
|
|
135
|
-
* in a git worktree, and if it
|
|
136
|
-
* silently editing files nobody
|
|
224
|
+
* `--skip-git-repo-check` is deliberately NOT passed by the build or any
|
|
225
|
+
* fence but research's: a task always builds in a git worktree, and if it
|
|
226
|
+
* somehow is not one, failing loudly beats silently editing files nobody
|
|
227
|
+
* can diff. A write-up reads a COPY with no `.git` and never resumes
|
|
228
|
+
* (codexResearch.mjs), so its fence passes it.
|
|
137
229
|
*
|
|
138
230
|
* THE PROMPT IS LAST, and that is load-bearing rather than tidy. Codex takes
|
|
139
231
|
* it as a trailing POSITIONAL, so every flag — including the two `-c` MCP
|
|
@@ -141,7 +233,7 @@ export const CODEX_RUNTIME = {
|
|
|
141
233
|
* placed before it. Appending them after the positional is the kind of argv
|
|
142
234
|
* that parses today and stops parsing on some future clap upgrade.
|
|
143
235
|
*/
|
|
144
|
-
args({ prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, cwd, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools, personal = null }) {
|
|
236
|
+
args({ prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, cwd, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools, personal = null, researchIsolation, knowledgeDir, projectReads }) {
|
|
145
237
|
// Adoption resumes a conversation in ITS OWN CLI's store (claude forks,
|
|
146
238
|
// agy moves) — codex has no adoptable store wired yet. Reaching here
|
|
147
239
|
// with an adopt id is a wiring mistake upstream, and it fails loudly on
|
|
@@ -169,166 +261,11 @@ export const CODEX_RUNTIME = {
|
|
|
169
261
|
// Effort is a config value on Codex rather than a flag.
|
|
170
262
|
if (effort) a.push('-c', `model_reasoning_effort="${effort}"`);
|
|
171
263
|
|
|
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));
|
|
264
|
+
// THE FENCE, picked by the same keys as `codexIgnoresUserConfig`: a fenced
|
|
265
|
+
// posture builds its own (CODEX_FENCES below); anything else is a build.
|
|
266
|
+
const fenced = codexIgnoresUserConfig({ profile, vaultDir });
|
|
267
|
+
if (fenced) {
|
|
268
|
+
a.push(...CODEX_FENCES[profile]({ cwd, vaultDir, researchIsolation, knowledgeDir, projectReads }));
|
|
332
269
|
} else {
|
|
333
270
|
// The daemon's build posture, mapped: SAFE keeps writes inside the
|
|
334
271
|
// workspace, the default lets the agent run its own tests and git
|
|
@@ -356,7 +293,10 @@ export const CODEX_RUNTIME = {
|
|
|
356
293
|
if (a.includes('--ignore-user-config')) a.push(...(personal?.args ?? []));
|
|
357
294
|
|
|
358
295
|
a.push(...mcp, ...resultSchemaArgs);
|
|
359
|
-
|
|
296
|
+
const readers = profile === 'consult' || profile === 'plan'
|
|
297
|
+
? '\nUse exec_command with shell="/bin/bash" and login=false for project discovery. Project files, local Git history and the project knowledge directory are readable; host files are outside this turn’s read boundary.'
|
|
298
|
+
: '';
|
|
299
|
+
a.push(`${system}${readers}\n\n---\n\n${prompt}`);
|
|
360
300
|
return a;
|
|
361
301
|
},
|
|
362
302
|
/**
|
|
@@ -376,16 +316,19 @@ export const CODEX_RUNTIME = {
|
|
|
376
316
|
};
|
|
377
317
|
|
|
378
318
|
/**
|
|
379
|
-
* THE
|
|
380
|
-
*
|
|
319
|
+
* THE ARTIFACT CARDS' FENCE ON CODEX — the image card's (0.114.0, the image
|
|
320
|
+
* tool switched on) and the mockup, 3D-model and deck cards' (0.115.0;
|
|
321
|
+
* 2026-09-29, the owner: "its saying 3d models run on claude only but thats
|
|
322
|
+
* not true"), one builder under each card's own profile name since 2026-10-10.
|
|
381
323
|
*
|
|
382
|
-
*
|
|
383
|
-
* reads the repo and writes ONLY
|
|
324
|
+
* Design and image share supporting native image generation and a
|
|
325
|
+
* permission profile that reads the repo and writes ONLY
|
|
326
|
+
* `<worktree>/.flowviant/artifacts/` (and the turn's scratch), selected
|
|
384
327
|
* by `default_permissions` (`codex exec` and `exec resume` refuse `-P` on
|
|
385
328
|
* 0.156.1), no network, no web search, no sub-agents, no browser, computer or
|
|
386
|
-
* apps, nobody's config or rules, and no approvals to wait on.
|
|
387
|
-
*
|
|
388
|
-
*
|
|
329
|
+
* apps, nobody's config or rules, and no approvals to wait on. Needed image
|
|
330
|
+
* generation supports the page without changing its end product. CDN scripts
|
|
331
|
+
* load in the viewer, never on this machine.
|
|
389
332
|
*
|
|
390
333
|
* MEASURED on codex-cli 0.156.1 (Linux, bubblewrap), with gpt-6-luna at low
|
|
391
334
|
* effort, in a scratch git repository (2026-09-29), with exactly this argv:
|
|
@@ -423,15 +366,16 @@ export const CODEX_RUNTIME = {
|
|
|
423
366
|
* descriptor"). With the network and the web shut, the one road out is the
|
|
424
367
|
* artifact itself, which is scrubbed of the machine's known secret values and
|
|
425
368
|
* 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.
|
|
369
|
+
* standing (0.114.0), and why RESEARCH does not use this fence: a write-up
|
|
370
|
+
* needs the web, and a turn that can read `.env` and
|
|
371
|
+
* `~/.flowviant/credentials.json` while holding `web_search` (both measured
|
|
372
|
+
* readable under this profile with `web_search="live"`) is the exfiltration
|
|
373
|
+
* Claude's research list was fenced to prevent. Codex research (0.117.0) runs
|
|
374
|
+
* under its own fence instead, over a copied view (CODEX_FENCES `research`).
|
|
431
375
|
*/
|
|
432
|
-
export function
|
|
376
|
+
export function codexArtifactFence(cwd, { name, imageGeneration = false, what }) {
|
|
433
377
|
if (!cwd || !isAbsolute(cwd)) {
|
|
434
|
-
throw new Error(
|
|
378
|
+
throw new Error(`${what} needs its worktree to fence — none reached codex's builder`);
|
|
435
379
|
}
|
|
436
380
|
const artifacts = join(cwd, ARTIFACT_DIR);
|
|
437
381
|
// THE TURN'S OWN SCRATCH (artifacts.mjs `ensureFenceScratch`, made empty by
|
|
@@ -443,17 +387,81 @@ export function codexDesignFence(cwd) {
|
|
|
443
387
|
// `shell_environment_policy.set` — measured to reach the sandboxed shell.
|
|
444
388
|
const scratch = join(cwd, FENCE_TMP_DIR);
|
|
445
389
|
return [
|
|
446
|
-
'-c',
|
|
447
|
-
// The table WHOLE and the path a TOML basic string — the wiki
|
|
390
|
+
'-c', `permissions.${name}.extends=":read-only"`,
|
|
391
|
+
// The table WHOLE and the path a TOML basic string — the wiki fence's
|
|
448
392
|
// reason: a dotted key splits on the dots inside the path.
|
|
449
|
-
'-c', `permissions.
|
|
393
|
+
'-c', `permissions.${name}.filesystem={${JSON.stringify(artifacts)}="write",${JSON.stringify(scratch)}="write"}`,
|
|
450
394
|
'-c', `shell_environment_policy.set={TMPDIR=${JSON.stringify(scratch)},TMPPREFIX=${JSON.stringify(join(scratch, 'zsh'))}}`,
|
|
451
|
-
'-c',
|
|
395
|
+
'-c', `default_permissions="${name}"`,
|
|
396
|
+
// Nobody is on this end of the pipe to approve an escalation.
|
|
452
397
|
'-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',
|
|
398
|
+
...fencedTail({ imageGeneration }),
|
|
458
399
|
];
|
|
459
400
|
}
|
|
401
|
+
|
|
402
|
+
/** The design card's fence — `codexArtifactFence` under its own profile name,
|
|
403
|
+
* with native image generation available for supporting assets. */
|
|
404
|
+
export const codexDesignFence = (cwd) =>
|
|
405
|
+
codexArtifactFence(cwd, { name: 'flowviantdesign', imageGeneration: true, what: 'a design turn' });
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* EVERY FENCED POSTURE'S ARGV, by the keys of CODEX_FENCED (a test holds the
|
|
409
|
+
* two tables to one key set), each built from the shared pieces above.
|
|
410
|
+
*/
|
|
411
|
+
const CODEX_FENCES = {
|
|
412
|
+
// A CONSULT, EXPRESSED THE ONLY WAY CODEX CAN EXPRESS IT — a different
|
|
413
|
+
// shape from Claude's verb allowlist, which is the whole reason `profile` is
|
|
414
|
+
// a promise rather than a flag list. See codexProjectRead.mjs.
|
|
415
|
+
consult: (opts) => [...projectReadFence(opts.projectReads ?? projectReadBoundary(opts)), ...fencedTail()],
|
|
416
|
+
// A PLANNING SESSION. Read-only on the filesystem, exactly like a consult —
|
|
417
|
+
// the writes it makes go through the control plane, not through this box —
|
|
418
|
+
// and for the same reason: its prompt is steered by anything a project
|
|
419
|
+
// editor can type. What differs is the ONE thing this profile exists for:
|
|
420
|
+
// an MCP config IS passed (by the caller, after the fence), carrying this
|
|
421
|
+
// turn's scoped token. The plan token has the planning tools; a capture
|
|
422
|
+
// token has only staging and transcript tools. The server refuses
|
|
423
|
+
// everything outside that scope.
|
|
424
|
+
plan: (opts) => [...projectReadFence(opts.projectReads ?? projectReadBoundary(opts)), ...fencedTail()],
|
|
425
|
+
wiki: ({ vaultDir }) => codexWikiFence(vaultDir),
|
|
426
|
+
// AN IMAGE CARD (0.114.0): read the repo, write ONLY the worktree's
|
|
427
|
+
// `.flowviant/artifacts/`, generate pictures, and nothing else. MEASURED on
|
|
428
|
+
// codex-cli 0.156.1 (Linux, bubblewrap), with no model turn spent: under
|
|
429
|
+
// this profile a `cp` from a CODEX_HOME-shaped folder into the artifacts
|
|
430
|
+
// directory landed, a write to a repo file failed "Read-only file system",
|
|
431
|
+
// and curl got no network; `codex debug prompt-input` with these overrides
|
|
432
|
+
// renders a managed profile of `write <worktree>/.flowviant/artifacts` +
|
|
433
|
+
// `read :root`, "Approval policy is currently never".
|
|
434
|
+
//
|
|
435
|
+
// SELECTED BY `default_permissions`, NOT `-P`: `codex exec` (and `exec
|
|
436
|
+
// resume`) on 0.156.1 refuse `-P` as an unexpected argument — only `codex
|
|
437
|
+
// sandbox` takes it — while `-c default_permissions="<name>"` resolves the
|
|
438
|
+
// named profile in both. The directory must EXIST before spawn (measured: a
|
|
439
|
+
// missing one cannot be created from inside the fence), which the agent lane
|
|
440
|
+
// sees to (`ensureArtifactDir`).
|
|
441
|
+
//
|
|
442
|
+
// WHERE THE PICTURES COME FROM. Codex's own `image_gen` tool saves each one
|
|
443
|
+
// under `$CODEX_HOME/generated_images/…` and says the path (the binary's own
|
|
444
|
+
// words: "Generated images are saved to … by default. If you need to use a
|
|
445
|
+
// generated image at another path, copy it"). An agent turn's CODEX_HOME is
|
|
446
|
+
// the agent's own isolated home (projectToolRuntimes, kept per agent since
|
|
447
|
+
// 2026-09-29); `:root` reads admit it, so the agent copies what it keeps
|
|
448
|
+
// into the one writable directory, where the artifact scan finds it.
|
|
449
|
+
//
|
|
450
|
+
// Native images support every artifact kind; this profile still delivers
|
|
451
|
+
// PNG/WebP, while design/model/deck still deliver their self-contained page.
|
|
452
|
+
image: ({ cwd }) => codexArtifactFence(cwd, { name: 'flowviantimage', imageGeneration: true, what: 'an image turn' }),
|
|
453
|
+
// A MOCKUP, 3D-MODEL OR DECK CARD (0.115.0): the fence is
|
|
454
|
+
// `codexDesignFence`'s, argued and measured at `codexArtifactFence` above.
|
|
455
|
+
design: ({ cwd }) => codexDesignFence(cwd),
|
|
456
|
+
// A WRITE-UP CARD (0.117.0): live web over a COPIED view of the worktree,
|
|
457
|
+
// never the checkout or the login store, under a root-denied tool fence
|
|
458
|
+
// (codexResearch.mjs `researchFence`, built by runTurn's
|
|
459
|
+
// `runIsolatedResearch`). It is the one fence that keeps the web, so its
|
|
460
|
+
// tail leaves NO_WEB out and closes everything else.
|
|
461
|
+
research: ({ researchIsolation }) => {
|
|
462
|
+
if (!researchIsolation) throw new Error('Codex research requires its isolated input copy.');
|
|
463
|
+
return [...researchFence(researchIsolation), ...fencedTail({ web: true, imageGeneration: true })];
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
/** The fenced profile names — for the test that holds both tables to one set. */
|
|
467
|
+
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,6 +86,12 @@ export function runtimesReport(detected) {
|
|
|
86
86
|
.join(',');
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/** Existing probe evidence only; diagnostics never spawn another CLI. */
|
|
90
|
+
export function measuredRuntimeVersion(id) {
|
|
91
|
+
const row = detectedCache?.find((runtime) => runtime.id === id);
|
|
92
|
+
return row ? { version: row.version, measuredAt: new Date(detectedAt).toISOString() } : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
89
95
|
export function detectRuntimes({ refresh = false } = {}) {
|
|
90
96
|
if (detectedCache && !refresh && Date.now() - detectedAt < DETECT_TTL_MS) return detectedCache;
|
|
91
97
|
detectedAt = Date.now();
|
|
@@ -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) {
|
|
@@ -316,11 +322,16 @@ export function parseCodexLine(line, cwd) {
|
|
|
316
322
|
// text: nothing here is the model speaking.
|
|
317
323
|
case 'thread.started':
|
|
318
324
|
return { activity: null, text: '', threadId: String(ev.thread_id ?? '') || null };
|
|
325
|
+
case 'item.started': {
|
|
326
|
+
const item = ev.item ?? {};
|
|
327
|
+
if ((item.item_type ?? item.type) !== 'command_execution') return null;
|
|
328
|
+
return { toolEvent: ['Bash', { command: item.command }, { id: item.id, runtime: 'codex' }] };
|
|
329
|
+
}
|
|
319
330
|
case 'item.completed': {
|
|
320
331
|
const item = ev.item ?? {};
|
|
321
332
|
const activity = humanizeCodexItem(item, cwd);
|
|
322
333
|
// Only the agent's MESSAGES are sentinel-bearing text. Reasoning is not:
|
|
323
|
-
// a model that muses "I could
|
|
334
|
+
// a model that muses "I could print WIKI_DONE now" must not end the turn.
|
|
324
335
|
const isMessage = (item.item_type ?? item.type) === 'agent_message';
|
|
325
336
|
const text = isMessage ? `${item.text ?? item.message ?? ''}\n` : '';
|
|
326
337
|
// `answer` is the SAME message, surfaced alone: `text` accumulates every
|
|
@@ -328,7 +339,13 @@ export function parseCodexLine(line, cwd) {
|
|
|
328
339
|
// the agent said LAST" (the agent lane's final JSON object) cannot recover
|
|
329
340
|
// it from `out`. Claude's stream has this for free under
|
|
330
341
|
// `answerFromResult`; codex needed it said.
|
|
331
|
-
|
|
342
|
+
const command = (item.item_type ?? item.type) === 'command_execution';
|
|
343
|
+
const toolEvent = command ? ['Bash', { command: item.command }, {
|
|
344
|
+
id: item.id, result: true, runtime: 'codex',
|
|
345
|
+
exitCode: item.exit_code, isError: item.status === 'failed',
|
|
346
|
+
output: item.aggregated_output ?? '',
|
|
347
|
+
}] : null;
|
|
348
|
+
return { activity, text, ...(toolEvent ? { toolEvent } : {}), ...(isMessage ? { answer: String(item.text ?? item.message ?? '') } : {}) };
|
|
332
349
|
}
|
|
333
350
|
// THE TURN'S OWN TOKEN COUNT (2026-09-24). `codex exec --json` closes a
|
|
334
351
|
// turn with `turn.completed` carrying `usage` — the codex twin of Claude's
|
|
@@ -439,7 +456,7 @@ function humanizeAgyTool(name, p = {}, cwd = '') {
|
|
|
439
456
|
case 'run_command':
|
|
440
457
|
return {
|
|
441
458
|
kind: 'bash',
|
|
442
|
-
command: String(p.CommandLine ?? '').slice(0,
|
|
459
|
+
command: String(p.CommandLine ?? '').slice(0, SCRUB_WINDOW), // windowed, not capped — see Claude's Bash
|
|
443
460
|
label: `$ ${oneLine(p.CommandLine, 60)}`,
|
|
444
461
|
};
|
|
445
462
|
case 'call_mcp_tool':
|