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/artifacts.mjs
CHANGED
|
@@ -78,6 +78,7 @@ import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readdi
|
|
|
78
78
|
import { createHash } from 'node:crypto';
|
|
79
79
|
import { join } from 'node:path';
|
|
80
80
|
import { cutKeepingExtension } from './safeFileName.mjs';
|
|
81
|
+
import { ensureRealSubdir, flowviantParentState, realDirState } from './ownedDirs.mjs';
|
|
81
82
|
import {
|
|
82
83
|
ARTIFACT_BINARY_MODEL_EXTS,
|
|
83
84
|
ARTIFACT_BINARY_MODEL_MAX_BYTES,
|
|
@@ -119,23 +120,9 @@ export const ARTIFACT_MAX_FILES = ARTIFACT_MAX_PER_OWNER;
|
|
|
119
120
|
* refuses the turn in words.
|
|
120
121
|
*/
|
|
121
122
|
export function ensureArtifactDir(placeDir) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
const st = lstatSync(p);
|
|
127
|
-
return st.isDirectory() && !st.isSymbolicLink() ? true : false;
|
|
128
|
-
} catch (e) {
|
|
129
|
-
return e?.code === 'ENOENT' ? null : false;
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
if (real(parent) === false || real(dir) === false) return false;
|
|
133
|
-
try {
|
|
134
|
-
mkdirSync(dir, { recursive: true });
|
|
135
|
-
} catch {
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
return real(parent) === true && real(dir) === true;
|
|
123
|
+
// The rule's one home since 2026-10-10 (ownedDirs.mjs, the audit's F100):
|
|
124
|
+
// refused, never replaced, and both components checked again after the make.
|
|
125
|
+
return ensureRealSubdir(placeDir, ARTIFACT_DIR);
|
|
139
126
|
}
|
|
140
127
|
|
|
141
128
|
/**
|
|
@@ -159,25 +146,16 @@ export function ensureArtifactDir(placeDir) {
|
|
|
159
146
|
*/
|
|
160
147
|
export const FENCE_TMP_DIR = '.flowviant/tmp';
|
|
161
148
|
export function ensureFenceScratch(placeDir) {
|
|
162
|
-
const parent = join(placeDir, '.flowviant');
|
|
163
149
|
const dir = join(placeDir, FENCE_TMP_DIR);
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
const st = lstatSync(p);
|
|
167
|
-
return st.isDirectory() && !st.isSymbolicLink() ? 'dir' : 'other';
|
|
168
|
-
} catch (e) {
|
|
169
|
-
return e?.code === 'ENOENT' ? 'none' : 'other';
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
if (kind(parent) !== 'dir' || kind(dir) === 'other') return false;
|
|
150
|
+
if (flowviantParentState(placeDir) !== 'dir' || realDirState(dir) === 'other') return false;
|
|
173
151
|
try {
|
|
174
152
|
// A real directory (checked above): its contents go, never through a link.
|
|
175
|
-
if (
|
|
153
|
+
if (realDirState(dir) === 'dir') rmSync(dir, { recursive: true, force: true });
|
|
176
154
|
mkdirSync(dir);
|
|
177
155
|
} catch {
|
|
178
156
|
return false;
|
|
179
157
|
}
|
|
180
|
-
return
|
|
158
|
+
return flowviantParentState(placeDir) === 'dir' && realDirState(dir) === 'dir';
|
|
181
159
|
}
|
|
182
160
|
|
|
183
161
|
export function artifactTypeFor(name) {
|
|
@@ -187,46 +165,72 @@ export function artifactTypeFor(name) {
|
|
|
187
165
|
}
|
|
188
166
|
|
|
189
167
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* symlink — nothing to report is the only honest reading of any of those.
|
|
168
|
+
* HOW DEEP A BUNDLE GOES: files are listed to depth four (a path of up to five
|
|
169
|
+
* components), so a directory found AT depth four is never looked inside.
|
|
193
170
|
*/
|
|
194
|
-
export
|
|
171
|
+
export const ARTIFACT_MAX_DEPTH = 4;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* THE ONE WALK OF THE ARTIFACTS DIRECTORY (2026-10-10, the audit's F067) —
|
|
175
|
+
* what the scan lists and what the check's fence keeps are read off the same
|
|
176
|
+
* entries, so the two cannot disagree about what an artifact is again. They
|
|
177
|
+
* did: the scan learned folders in 0.99.0 (a bundle's sidecars, the 3D-model
|
|
178
|
+
* page's `<name>/index.html`) and the fence went on removing every
|
|
179
|
+
* subdirectory whole, so a model agent whose repo declared a check lost its
|
|
180
|
+
* page the moment its queue emptied — and a send-back turn that did not
|
|
181
|
+
* rewrite it settled "nothing".
|
|
182
|
+
*
|
|
183
|
+
* `visit({ name, path, kind, st })` is called for every entry, depth first.
|
|
184
|
+
* `name` is the relative path; `kind` is one of:
|
|
185
|
+
* 'dir' — a real directory within depth: walked next
|
|
186
|
+
* 'file' — a regular file (any type: the allowlist is each caller's)
|
|
187
|
+
* 'hidden' — a dot-name, whatever it is; never looked at further
|
|
188
|
+
* 'deep' — a real directory past the depth: nothing in it is ever listed
|
|
189
|
+
* 'other' — a symlink, a fifo, a socket, a device: never followed or read
|
|
190
|
+
*
|
|
191
|
+
* `lstat` and never followed, and BOTH components checked first — `lstat` on
|
|
192
|
+
* `.flowviant/artifacts` refuses a symlink AT that name, but it resolves
|
|
193
|
+
* THROUGH a symlinked `.flowviant`, and git commits symlinks: a cloned repo
|
|
194
|
+
* carrying `.flowviant -> /some/dir` once had the scan listing, reading and
|
|
195
|
+
* uploading `/some/dir/artifacts/*` (2026-09-23). False, having visited
|
|
196
|
+
* nothing, when either is not a real directory.
|
|
197
|
+
*/
|
|
198
|
+
export function walkArtifactDir(placeDir, visit) {
|
|
195
199
|
const dir = join(placeDir, ARTIFACT_DIR);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
* `.flowviant/artifacts` refuses a symlink AT that name, but it resolves
|
|
199
|
-
* THROUGH a symlinked `.flowviant` — and git commits symlinks, so a cloned
|
|
200
|
-
* repo carrying `.flowviant -> /some/dir` would have had this scan listing,
|
|
201
|
-
* reading and uploading `/some/dir/artifacts/*`, a directory outside the
|
|
202
|
-
* place the turn stood in. The knowledge sync already refuses a `.flowviant`
|
|
203
|
-
* that is not a real directory (`flowviantDirOk`); this is the same check on
|
|
204
|
-
* the read side.
|
|
205
|
-
*/
|
|
206
|
-
try {
|
|
207
|
-
const parent = lstatSync(join(placeDir, '.flowviant'));
|
|
208
|
-
if (parent.isSymbolicLink() || !parent.isDirectory()) return [];
|
|
209
|
-
if (!lstatSync(dir).isDirectory()) return [];
|
|
210
|
-
} catch {
|
|
211
|
-
return [];
|
|
212
|
-
}
|
|
213
|
-
const out = [];
|
|
214
|
-
const walk = (at, prefix = '', depth = 0) => {
|
|
215
|
-
if (depth > 4) return;
|
|
200
|
+
if (flowviantParentState(placeDir) !== 'dir' || realDirState(dir) !== 'dir') return false;
|
|
201
|
+
const walk = (at, prefix, depth) => {
|
|
216
202
|
let names;
|
|
217
203
|
try { names = readdirSync(at); } catch { return; }
|
|
218
204
|
for (const part of names) {
|
|
219
|
-
if (part.startsWith('.')) continue;
|
|
220
205
|
const name = prefix ? `${prefix}/${part}` : part;
|
|
221
206
|
const path = join(at, part);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
207
|
+
let st = null;
|
|
208
|
+
let kind = 'hidden';
|
|
209
|
+
if (!part.startsWith('.')) {
|
|
210
|
+
try { st = lstatSync(path); } catch { continue; /* vanished */ }
|
|
211
|
+
kind = st.isSymbolicLink() ? 'other'
|
|
212
|
+
: st.isDirectory() ? (depth < ARTIFACT_MAX_DEPTH ? 'dir' : 'deep')
|
|
213
|
+
: st.isFile() ? 'file'
|
|
214
|
+
: 'other';
|
|
215
|
+
}
|
|
216
|
+
visit({ name, path, kind, st });
|
|
217
|
+
if (kind === 'dir') walk(path, name, depth + 1);
|
|
227
218
|
}
|
|
228
219
|
};
|
|
229
|
-
walk(dir);
|
|
220
|
+
walk(dir, '', 0);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The directory's regular files, newest first, at most `ARTIFACT_MAX_FILES`.
|
|
226
|
+
* `[]` for a directory that does not exist, is not a directory, or is a
|
|
227
|
+
* symlink — nothing to report is the only honest reading of any of those.
|
|
228
|
+
*/
|
|
229
|
+
export function scanArtifacts(placeDir) {
|
|
230
|
+
const out = [];
|
|
231
|
+
walkArtifactDir(placeDir, ({ name, path, kind, st }) => {
|
|
232
|
+
if (kind === 'file') out.push({ name, path, size: st.size, mtimeMs: st.mtimeMs });
|
|
233
|
+
});
|
|
230
234
|
out.sort((a, b) => b.mtimeMs - a.mtimeMs || a.name.localeCompare(b.name));
|
|
231
235
|
return out.slice(0, ARTIFACT_MAX_FILES);
|
|
232
236
|
}
|
|
@@ -239,8 +243,8 @@ export function snapshotArtifacts(placeDir) {
|
|
|
239
243
|
}
|
|
240
244
|
|
|
241
245
|
/** What the turn wrote: every entry whose (name, size, mtime) is not what the
|
|
242
|
-
* snapshot held.
|
|
243
|
-
*
|
|
246
|
+
* snapshot held. Deletions and replacement relationships are reported by
|
|
247
|
+
* artifactIteration.mjs separately from upload bodies. */
|
|
244
248
|
export function changedArtifacts(before, entries) {
|
|
245
249
|
return entries.filter((e) => before?.get(e.name) !== sigOf(e));
|
|
246
250
|
}
|
|
@@ -289,6 +293,9 @@ function safeArtifactName(raw) {
|
|
|
289
293
|
return clean ? cutKeepingExtension(clean) : 'artifact';
|
|
290
294
|
}
|
|
291
295
|
|
|
296
|
+
/** The stored name is also the key for iteration and queued upload cancellation. */
|
|
297
|
+
export const artifactStoredName = (name) => name.split('/').map(safeArtifactName).join('/');
|
|
298
|
+
|
|
292
299
|
/**
|
|
293
300
|
* ONE UPLOAD BODY, as data — fields plus optional bytes — so a held body is
|
|
294
301
|
* re-sent exactly as it was first built (scrubbed once, never re-read from a
|
|
@@ -299,7 +306,7 @@ export function buildArtifactUpload(entry, { sessionId, agentId, turnId }, scrub
|
|
|
299
306
|
const fields = {
|
|
300
307
|
...(sessionId ? { sessionId } : { agentId }),
|
|
301
308
|
...(turnId ? { turnId } : {}),
|
|
302
|
-
name: entry.name
|
|
309
|
+
name: artifactStoredName(entry.name),
|
|
303
310
|
bytes: String(entry.size),
|
|
304
311
|
};
|
|
305
312
|
if (!type) return { fields, bytes: null }; // reported by name only
|
package/bin/lib/authproxy.mjs
CHANGED
|
@@ -230,7 +230,8 @@ export function startAuthProxy({
|
|
|
230
230
|
return;
|
|
231
231
|
}
|
|
232
232
|
// 4. The grant is a cookie, and a cookie can be ridden (header rule 4a's
|
|
233
|
-
// backstop)
|
|
233
|
+
// backstop) — and so can a cached Basic password, by a cross-site write
|
|
234
|
+
// (2026-10-10). Never a bounce — the caller HAS a credential; the request
|
|
234
235
|
// SHAPE is what is refused.
|
|
235
236
|
if (verdict === 'cross-site') {
|
|
236
237
|
res.writeHead(403, { 'Content-Type': 'text/plain', ...noStore });
|
package/bin/lib/childEnv.mjs
CHANGED
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
* surface may describe anything here as "sandboxed" or "isolated".
|
|
29
29
|
*/
|
|
30
30
|
import { MACHINE_CREDENTIAL_ENV } from './machineEnv.mjs';
|
|
31
|
+
// The variables that can carry Claude's auth in from the environment — the
|
|
32
|
+
// one list claudeAuth.mjs reads presence from (2026-10-10, the audit's F073).
|
|
33
|
+
import { AUTH_ENV_VARS } from './claudeAuth.mjs';
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
36
|
* The complete kept set. Two groups, and both are here for a reason that bit
|
|
@@ -203,7 +206,8 @@ export const DEPLOY_KEEP_NAMES = [...DEPLOY_KEEP];
|
|
|
203
206
|
* other half. Deriving it FROM that array is what makes forgetting impossible,
|
|
204
207
|
* and it is why the merge happens inside `learnSecrets` rather than at a
|
|
205
208
|
* call site somebody has to remember. The dependency direction is the only one
|
|
206
|
-
* that is acyclic besides: this module imports
|
|
209
|
+
* that is acyclic besides: this module imports only two leaves (the name lists
|
|
210
|
+
* in machineEnv.mjs and claudeAuth.mjs), neither of which imports back.
|
|
207
211
|
*
|
|
208
212
|
* TWO NAMES ARE HERE THAT ARE NOT IN `DEPLOY_KEEP`, and the asymmetry is the
|
|
209
213
|
* point. `CF_API_TOKEN` and `CF_ACCOUNT_ID` are wrangler's older spellings — it
|
|
@@ -249,9 +253,10 @@ const REDACT_ONLY = [
|
|
|
249
253
|
// of (machineEnv.mjs): a name removed from children is redacted by
|
|
250
254
|
// construction, never by a second hand-kept copy.
|
|
251
255
|
...MACHINE_CREDENTIAL_ENV,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
'
|
|
256
|
+
// Every Claude auth variable, from the list claudeAuth.mjs reports presence
|
|
257
|
+
// from — derived, not re-listed, for the same reason as the line above
|
|
258
|
+
// (2026-10-10, the audit's F073).
|
|
259
|
+
...AUTH_ENV_VARS,
|
|
255
260
|
'OPENAI_API_KEY',
|
|
256
261
|
'GH_TOKEN',
|
|
257
262
|
'GITHUB_TOKEN',
|
package/bin/lib/claude.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Flowviant side of driving a coding CLI that is not a posture, a stream
|
|
3
|
-
* or a process: the sentinel protocol (a line the
|
|
4
|
-
*
|
|
5
|
-
* runtime runs the turn.
|
|
6
|
-
* interactive user — the only channel to a human is the blocker loop.
|
|
3
|
+
* or a process: the sentinel protocol (a line the wiki prompts require a turn
|
|
4
|
+
* to end on — WIKI_DONE, REGROUND_DONE) and the hand-over of the flowviant MCP
|
|
5
|
+
* server to whichever runtime runs the turn.
|
|
7
6
|
*
|
|
8
7
|
* This file used to hold everything about a turn. It was split by reason to
|
|
9
8
|
* change (SOLID 2026-09-26): the contract prompts live in prompts.mjs, the
|
|
@@ -11,22 +10,21 @@
|
|
|
11
10
|
* claudeStream.mjs, and the spawn-and-supervise of one turn in runTurn.mjs.
|
|
12
11
|
* The argv, the binary and the event shape of each vendor come from each
|
|
13
12
|
* CLI's registry row (runtime{Claude,Codex,Antigravity}.mjs, assembled in
|
|
14
|
-
* runtimes.mjs; the stream's meaning in runtimeEvents.mjs).
|
|
13
|
+
* runtimes.mjs; the stream's meaning in runtimeEvents.mjs). The generic
|
|
14
|
+
* `sleep` that used to live here is a leaf of its own (sleep.mjs, 2026-10-10):
|
|
15
|
+
* importing it from here loaded the whole runtime registry into `login`.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import { runtimeById } from './runtimes.mjs';
|
|
18
19
|
|
|
19
|
-
export const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000));
|
|
20
|
-
|
|
21
20
|
// Sentinels must appear on their OWN line (the prompts require it). Substring
|
|
22
21
|
// matching falsely fired when an agent merely *mentioned* the word in prose
|
|
23
|
-
// (e.g. "I won't
|
|
24
|
-
//
|
|
22
|
+
// (e.g. "I won't print WIKI_DONE until the pages are written"), ending a turn
|
|
23
|
+
// that had not finished. Anchor to a full line instead.
|
|
25
24
|
//
|
|
26
25
|
// LINE-SPLIT, NEVER A `^...$` REGEX WITH A `\s*` ON BOTH ENDS (audit
|
|
27
|
-
// 2026-09-24). `new RegExp('^\\s*NAME\\s*$', 'm')`
|
|
28
|
-
//
|
|
29
|
-
// whitespace on one line — the same shape `taskIdsFromMessage` already had
|
|
26
|
+
// 2026-09-24). `new RegExp('^\\s*NAME\\s*$', 'm')` is QUADRATIC against a
|
|
27
|
+
// long run of whitespace on one line — the same shape `taskIdsFromMessage` already had
|
|
30
28
|
// to fix, measured ~0.5s at 40KB here — and a CLI's own stdout is exactly
|
|
31
29
|
// the kind of text a stray control sequence or a pasted blob can grow past
|
|
32
30
|
// that. A length cap first, then plain string work, same as there.
|
|
@@ -35,14 +33,6 @@ export const sawSentinel = (out, name) =>
|
|
|
35
33
|
String(out ?? '')
|
|
36
34
|
.split('\n')
|
|
37
35
|
.some((l) => l.length < SENTINEL_MAX_LINE && l.trim() === name);
|
|
38
|
-
export const blockedId = (out) => {
|
|
39
|
-
for (const l of String(out ?? '').split('\n')) {
|
|
40
|
-
if (l.length > SENTINEL_MAX_LINE) continue;
|
|
41
|
-
const t = l.trim();
|
|
42
|
-
if (t.startsWith('BLOCKED:') && /^BLOCKED:\S+$/.test(t)) return t.slice(8);
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
};
|
|
46
36
|
|
|
47
37
|
/**
|
|
48
38
|
* Hand a runtime the flowviant MCP server, however that runtime wants it.
|
package/bin/lib/claudeAuth.mjs
CHANGED
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
* daemon started, so a variable exported weeks ago is still in there long
|
|
27
27
|
* after it left your shell. A stale one fails in a way a fresh `claude`
|
|
28
28
|
* never reproduces.
|
|
29
|
-
* · THE
|
|
30
|
-
* different user, or a container has a different one than
|
|
29
|
+
* · THE STORE. The CLI uses $CLAUDE_CONFIG_DIR, else $HOME/.claude. A daemon
|
|
30
|
+
* under systemd, a different user, or a container has a different one than
|
|
31
|
+
* the shell you
|
|
31
32
|
* tested in, so "I am logged in" was measured on the wrong file.
|
|
32
33
|
*
|
|
33
34
|
* ── WHAT THIS IS NOT ──
|
|
@@ -75,11 +76,19 @@ export const AUTH_ENV_VARS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAU
|
|
|
75
76
|
/** Claude Code's OAuth store, relative to the HOME the CLI will be handed. */
|
|
76
77
|
const CRED_REL = ['.claude', '.credentials.json'];
|
|
77
78
|
|
|
79
|
+
/** The same location Claude's personal settings reader uses. */
|
|
80
|
+
export function claudeCredentialPath(env = process.env, home = null) {
|
|
81
|
+
const root = home ?? env.HOME ?? env.USERPROFILE ?? null;
|
|
82
|
+
return env.CLAUDE_CONFIG_DIR
|
|
83
|
+
? join(env.CLAUDE_CONFIG_DIR, '.credentials.json')
|
|
84
|
+
: root ? join(root, ...CRED_REL) : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
78
87
|
|
|
79
88
|
/**
|
|
80
89
|
* What the CLI on this machine will resolve, as far as it can be seen without
|
|
81
90
|
* spending anything: no process spawned, no request made, no quota touched.
|
|
82
|
-
* Just this process's own environment and
|
|
91
|
+
* Just this process's own environment and the CLI-selected file's metadata.
|
|
83
92
|
*
|
|
84
93
|
* `env` and `home` are parameters rather than reads of `process` so the tests
|
|
85
94
|
* can drive every branch — and so the caller passes the SAME environment the
|
|
@@ -98,9 +107,10 @@ export function claudeAuthContext({ env = process.env, home = null } = {}) {
|
|
|
98
107
|
let refreshExpiresAt = null;
|
|
99
108
|
let accessExpiresAt = null;
|
|
100
109
|
let subscriptionType = null;
|
|
101
|
-
|
|
110
|
+
const credentialPath = claudeCredentialPath(env, HOME);
|
|
111
|
+
if (credentialPath) {
|
|
102
112
|
try {
|
|
103
|
-
const raw = readFileSync(
|
|
113
|
+
const raw = readFileSync(credentialPath, 'utf8');
|
|
104
114
|
const oauth = JSON.parse(raw)?.claudeAiOauth;
|
|
105
115
|
if (oauth && typeof oauth === 'object') {
|
|
106
116
|
hasFile = true;
|
|
@@ -33,10 +33,22 @@
|
|
|
33
33
|
* later CLI adds — a sandbox auto-allow, a permission mode — never rides by
|
|
34
34
|
* default. It rides INSIDE the posture's own `--settings`, because the CLI
|
|
35
35
|
* reads one: given two, only the last one's hook fired (measured).
|
|
36
|
+
*
|
|
37
|
+
* …AND NEVER ON THE COMMAND LINE (2026-10-10, the audit). What is kept is
|
|
38
|
+
* exactly where a person puts ANTHROPIC_API_KEY, an auth token or Bedrock's
|
|
39
|
+
* AWS keys (`env`, `apiKeyHelper`), and argv is every local user's to read
|
|
40
|
+
* (`ps`, /proc/<pid>/cmdline) for the life of the turn. So the merged object
|
|
41
|
+
* is written to a file only this user can read — 0600, in a 0700
|
|
42
|
+
* `~/.flowviant/run/` — and `--settings` names its path, which the CLI reads
|
|
43
|
+
* as a settings file ("--settings <file-or-json>"). Still exactly one
|
|
44
|
+
* `--settings`. The file goes when the turn settles (runTurn.mjs asks the
|
|
45
|
+
* adapter's `releaseArgs`), and a leftover from a process that is gone is
|
|
46
|
+
* swept on the next write.
|
|
36
47
|
*/
|
|
37
|
-
import {
|
|
48
|
+
import { randomBytes } from 'node:crypto';
|
|
49
|
+
import { chmodSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
38
50
|
import { homedir } from 'node:os';
|
|
39
|
-
import { join } from 'node:path';
|
|
51
|
+
import { basename, dirname, join } from 'node:path';
|
|
40
52
|
import { MACHINE_CREDENTIAL_ENV } from './machineEnv.mjs';
|
|
41
53
|
import { WINDOW_ENV_REMOVED, withoutWindows } from './noWindowEnv.mjs';
|
|
42
54
|
|
|
@@ -129,17 +141,70 @@ export function personalClaudeSettings(env = process.env) {
|
|
|
129
141
|
return out;
|
|
130
142
|
}
|
|
131
143
|
|
|
144
|
+
/** Where a fenced turn's settings file lives — the daemon's own state
|
|
145
|
+
* directory, private to this user. */
|
|
146
|
+
export const turnSettingsDir = () => join(homedir(), '.flowviant', 'run');
|
|
147
|
+
/** A settings file of ours: the writing process's pid, then a random tag. */
|
|
148
|
+
const TURN_SETTINGS_FILE = /^settings-(\d+)-[0-9a-f]{16}\.json$/;
|
|
149
|
+
|
|
150
|
+
const pidAlive = (pid) => {
|
|
151
|
+
try {
|
|
152
|
+
process.kill(pid, 0);
|
|
153
|
+
return true;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return error?.code === 'EPERM';
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Write `settings` to a fresh 0600 file in the run directory and return its
|
|
161
|
+
* path. The directory is made 0700 (and tightened if it was not), and must be
|
|
162
|
+
* a real directory — a link standing there is refused, never written through.
|
|
163
|
+
* A file another process left behind is removed first if that process is gone;
|
|
164
|
+
* this process's own files are live turns'. THROWS when it cannot write: the
|
|
165
|
+
* turn then fails in words rather than carrying the person's settings on argv.
|
|
166
|
+
*/
|
|
167
|
+
export function writeTurnSettings(settings, { dir = turnSettingsDir() } = {}) {
|
|
168
|
+
try {
|
|
169
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
170
|
+
if (!lstatSync(dir).isDirectory()) throw new Error(`${dir} is not a directory`);
|
|
171
|
+
chmodSync(dir, 0o700);
|
|
172
|
+
for (const name of readdirSync(dir)) {
|
|
173
|
+
const m = TURN_SETTINGS_FILE.exec(name);
|
|
174
|
+
if (!m || Number(m[1]) === process.pid || pidAlive(Number(m[1]))) continue;
|
|
175
|
+
rmSync(join(dir, name), { force: true });
|
|
176
|
+
}
|
|
177
|
+
const path = join(dir, `settings-${process.pid}-${randomBytes(8).toString('hex')}.json`);
|
|
178
|
+
writeFileSync(path, JSON.stringify(settings), { mode: 0o600, flag: 'wx' });
|
|
179
|
+
return path;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
throw new Error(`cannot write this turn's Claude settings file under ${dir} (${error?.message ?? error}) — the person's settings never ride the command line`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Remove the settings file a turn's argv names, when it is one of ours. */
|
|
186
|
+
export function releaseTurnSettings(args, { dir = turnSettingsDir() } = {}) {
|
|
187
|
+
if (!Array.isArray(args)) return;
|
|
188
|
+
args.forEach((a, i) => {
|
|
189
|
+
if (i === 0 || args[i - 1] !== '--settings' || typeof a !== 'string') return;
|
|
190
|
+
if (dirname(a) !== dir || !TURN_SETTINGS_FILE.test(basename(a))) return;
|
|
191
|
+
rmSync(a, { force: true });
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
132
195
|
/**
|
|
133
196
|
* `perm` with `kept` folded into its `--settings` — the posture's keys win, so
|
|
134
197
|
* a kept key a posture ever names is the fence's word. A posture with no
|
|
135
198
|
* `--settings` gets one before its lists (`--allowedTools` is variadic). With
|
|
136
|
-
* nothing kept, `perm` is returned as it was
|
|
199
|
+
* nothing kept, `perm` is returned as it was (the posture's own read guard
|
|
200
|
+
* names no secret). With something kept, the one `--settings` names the
|
|
201
|
+
* merged object's file (`write`, writeTurnSettings) and never carries it.
|
|
137
202
|
*/
|
|
138
|
-
export function withPersonalSettings(perm, kept) {
|
|
203
|
+
export function withPersonalSettings(perm, kept, { write = writeTurnSettings } = {}) {
|
|
139
204
|
const personal = kept && typeof kept === 'object' && !Array.isArray(kept) ? kept : {};
|
|
140
205
|
if (!Object.keys(personal).length) return [...perm];
|
|
141
206
|
const at = perm.indexOf('--settings');
|
|
142
|
-
if (at < 0) return ['--settings',
|
|
207
|
+
if (at < 0) return ['--settings', write(personal), ...perm];
|
|
143
208
|
const posture = JSON.parse(perm[at + 1]);
|
|
144
|
-
return [...perm.slice(0, at), '--settings',
|
|
209
|
+
return [...perm.slice(0, at), '--settings', write({ ...personal, ...posture }), ...perm.slice(at + 2)];
|
|
145
210
|
}
|