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