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/projectTools.mjs
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
/** Repo-owned tool configuration, read from the base ref for every agent turn. */
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { delimiter, isAbsolute, join } from 'node:path';
|
|
2
|
+
import { accessSync, constants, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
4
|
import { tmpdir } from 'node:os';
|
|
6
|
-
import {
|
|
5
|
+
import { git, gitRaw, splitNul } from './git.mjs';
|
|
6
|
+
import { withoutMachineCredentials } from './machineEnv.mjs';
|
|
7
|
+
import { fencedMountFor, mcpRefusal, prepareForRunner, skillsRefusal } from './projectToolRuntimes.mjs';
|
|
7
8
|
|
|
8
9
|
const MAX_TOOLS = 40;
|
|
9
10
|
const MAX_REPORT_TOOLS = 80; // 40 MCP servers plus 40 instruction directories
|
|
10
11
|
const MAX_INSTRUCTION_FILES = 80;
|
|
11
12
|
const MAX_INSTRUCTION_BYTES = 512 * 1024;
|
|
12
13
|
const nameOk = (name) => /^[A-Za-z0-9_-]{1,80}$/.test(name) && name !== 'flowviant';
|
|
14
|
+
/** Base-ref reads are QUIET (git's "does not exist in" for an absent file is
|
|
15
|
+
* an answer here, not a message for the daemon's terminal) and keep a 1 MiB
|
|
16
|
+
* cap per file. */
|
|
17
|
+
const SHOW = Object.freeze({ stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1024 * 1024 });
|
|
13
18
|
const show = (root, ref, path) => {
|
|
14
19
|
try {
|
|
15
|
-
return
|
|
16
|
-
cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1024 * 1024,
|
|
17
|
-
});
|
|
20
|
+
return gitRaw(['show', `${ref}:${path}`], root, SHOW);
|
|
18
21
|
} catch { return null; }
|
|
19
22
|
};
|
|
20
23
|
const showBytes = (root, ref, path) => {
|
|
21
24
|
try {
|
|
22
|
-
return
|
|
23
|
-
cwd: root, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1024 * 1024,
|
|
24
|
-
});
|
|
25
|
+
return gitRaw(['show', `${ref}:${path}`], root, { ...SHOW, encoding: 'buffer' });
|
|
25
26
|
} catch { return null; }
|
|
26
27
|
};
|
|
27
28
|
|
|
@@ -39,11 +40,39 @@ function referenced(value) {
|
|
|
39
40
|
return [...String(value).matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((m) => m[1]);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
/**
|
|
44
|
+
* THE ENVIRONMENT A `${VAR}` IN THE REPO'S `.mcp.json` IS READ FROM: the
|
|
45
|
+
* caller's, MINUS THE MACHINE CREDENTIAL AND EVERY OTHER `FLOWVIANT_*` NAME,
|
|
46
|
+
* whatever the caller passed (2026-10-10, the audit). A base-branch
|
|
47
|
+
* `"url": "https://x/mcp?k=${FLOWVIANT_FLEET}"` delivered the credential this
|
|
48
|
+
* daemon authenticates with to that host on every agent turn — the
|
|
49
|
+
* inherited-credential leak machineEnv.mjs exists to close. The daemon's own
|
|
50
|
+
* names are nobody's repo config to read, so such a server reads as
|
|
51
|
+
* `missing` ("env FLOWVIANT_FLEET unset").
|
|
52
|
+
*/
|
|
53
|
+
const expansionEnv = (env) =>
|
|
54
|
+
Object.fromEntries(Object.entries(withoutMachineCredentials(env ?? process.env)).filter(([key]) => !key.startsWith('FLOWVIANT_')));
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A TREE LISTING IS NUL-SEPARATED AND SIZED FOR A REAL REPOSITORY (2026-10-10,
|
|
58
|
+
* the audit). The whole-tree listing that finds nested CLAUDE.md/AGENTS.md ran
|
|
59
|
+
* under a 1 MiB buffer — about 20k paths — and nothing caught the ENOBUFS, so
|
|
60
|
+
* EVERY agent turn on a larger repository settled `nothing` before its CLI
|
|
61
|
+
* started. 64 MiB is workAdoptCarry.mjs's precedent; MAX_INSTRUCTION_FILES and
|
|
62
|
+
* MAX_INSTRUCTION_BYTES stay the real bounds. `-z` because git C-quotes a path
|
|
63
|
+
* that is not plain ASCII (`"d\303\251j\303\240/CLAUDE.md"`), which the name
|
|
64
|
+
* filter never matched (git.mjs's `-z` rule).
|
|
65
|
+
*/
|
|
66
|
+
const LIST_MAX_BUFFER = 64 * 1024 * 1024;
|
|
67
|
+
const listTree = (root, args) =>
|
|
68
|
+
splitNul(gitRaw(['ls-tree', '-r', '-z', '--name-only', ...args], root, {
|
|
69
|
+
stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: LIST_MAX_BUFFER,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
export function readBaseTools(root, ref, callerEnv = process.env) {
|
|
73
|
+
const env = expansionEnv(callerEnv);
|
|
43
74
|
// Resolve once: a base branch may advance while the turn is being prepared.
|
|
44
|
-
const commit =
|
|
45
|
-
cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
46
|
-
}).trim();
|
|
75
|
+
const commit = git(['rev-parse', '--verify', ref], root, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
47
76
|
const raw = show(root, commit, '.mcp.json');
|
|
48
77
|
let servers = {};
|
|
49
78
|
if (raw != null) {
|
|
@@ -53,9 +82,7 @@ export function readBaseTools(root, ref, env = process.env) {
|
|
|
53
82
|
const skills = [];
|
|
54
83
|
const skillFiles = [];
|
|
55
84
|
try {
|
|
56
|
-
const paths =
|
|
57
|
-
cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1024 * 1024,
|
|
58
|
-
}).split('\n');
|
|
85
|
+
const paths = listTree(root, [commit, '--', '.claude/skills']);
|
|
59
86
|
let bytes = 0;
|
|
60
87
|
for (const path of paths) {
|
|
61
88
|
if (!/^\.claude\/skills\/[A-Za-z0-9_-]+\/(?:[A-Za-z0-9_.-]+\/)*[A-Za-z0-9_.-]+$/.test(path)) continue;
|
|
@@ -82,9 +109,7 @@ export function readBaseTools(root, ref, env = process.env) {
|
|
|
82
109
|
// Nested instructions are normally discovered when a CLI enters that path.
|
|
83
110
|
// Their worktree copies must not regain authority through that discovery.
|
|
84
111
|
const instructions = [];
|
|
85
|
-
const paths =
|
|
86
|
-
cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1024 * 1024,
|
|
87
|
-
}).split('\n').filter((path) => /(^|\/)(CLAUDE|AGENTS)\.md$/.test(path));
|
|
112
|
+
const paths = listTree(root, [commit]).filter((path) => /(^|\/)(CLAUDE|AGENTS)\.md$/.test(path));
|
|
88
113
|
if (paths.length > MAX_INSTRUCTION_FILES) throw new Error('Base branch has too many agent instruction files');
|
|
89
114
|
let instructionBytes = 0;
|
|
90
115
|
for (const path of paths) {
|
|
@@ -99,6 +124,7 @@ export function readBaseTools(root, ref, env = process.env) {
|
|
|
99
124
|
|
|
100
125
|
const expand = (value, env) => String(value).replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, key) => env[key] ?? '');
|
|
101
126
|
|
|
127
|
+
|
|
102
128
|
/**
|
|
103
129
|
* Temporary per-turn files are removed after the last retry. No personal CLI config is written.
|
|
104
130
|
*
|
|
@@ -107,32 +133,33 @@ const expand = (value, env) => String(value).replace(/\$\{([A-Za-z_][A-Za-z0-9_]
|
|
|
107
133
|
* removed by `cleanup`, because Codex indexes a thread under the home it ran
|
|
108
134
|
* in and the next card resumes that thread. Absent, a Codex turn gets a home
|
|
109
135
|
* that dies with it.
|
|
136
|
+
*
|
|
137
|
+
* NO EXPANDED VALUE RIDES ARGV (2026-10-10, the audit): Claude's servers go
|
|
138
|
+
* to this turn's 0600 mcp.json, Codex's into the isolated home's config.toml
|
|
139
|
+
* (projectToolRuntimes.mjs) — never `-c` pairs on the command line. A fenced
|
|
140
|
+
* Codex turn, which ignores that file, takes `fencedMount` instead: the
|
|
141
|
+
* repo's own words as `-c` pairs and every expanded value in its environment.
|
|
110
142
|
*/
|
|
111
|
-
export function prepareAgentTools(snapshot, runner,
|
|
143
|
+
export function prepareAgentTools(snapshot, runner, callerEnv = process.env, worktree = null, { codexHome: keptCodexHome = null } = {}) {
|
|
144
|
+
const env = expansionEnv(callerEnv);
|
|
112
145
|
const dir = mkdtempSync(join(tmpdir(), 'flowviant-agent-tools-'));
|
|
113
146
|
const usable = snapshot.tools.filter((t) => t.state === 'ready' && mcpRefusal(runner, t.config) == null);
|
|
147
|
+
// What the base lists and this turn cannot mount, and why — the turn is told
|
|
148
|
+
// (postureTools.mjs), so a missing server is a named fact, never a guess.
|
|
149
|
+
const mcpUnmounted = snapshot.tools
|
|
150
|
+
.filter((t) => !usable.includes(t))
|
|
151
|
+
.map((t) => ({ name: t.name, reason: String(mcpRefusal(runner, t.config) ?? t.reason ?? 'not mounted').slice(0, 200) }));
|
|
114
152
|
const mcpServers = {};
|
|
115
|
-
const codexArgs = [];
|
|
116
153
|
for (const { name, config } of usable) {
|
|
117
154
|
if (typeof config.command === 'string') {
|
|
118
155
|
const command = expand(config.command, env);
|
|
119
156
|
const args = Array.isArray(config.args) ? config.args.map((v) => expand(v, env)) : [];
|
|
120
157
|
const vars = Object.fromEntries(Object.entries(config.env ?? {}).map(([k, v]) => [k, expand(v, env)]));
|
|
121
158
|
mcpServers[name] = { command, args, env: vars };
|
|
122
|
-
codexArgs.push('-c', `mcp_servers.${name}.command=${toml(command)}`);
|
|
123
|
-
codexArgs.push('-c', `mcp_servers.${name}.args=${JSON.stringify(args)}`);
|
|
124
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
125
|
-
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) codexArgs.push('-c', `mcp_servers.${name}.env.${key}=${toml(value)}`);
|
|
126
|
-
}
|
|
127
159
|
} else if (typeof config.url === 'string') {
|
|
128
160
|
const url = expand(config.url, env);
|
|
129
161
|
const headers = Object.fromEntries(Object.entries(config.headers ?? {}).map(([k, v]) => [k, expand(v, env)]));
|
|
130
162
|
mcpServers[name] = { type: config.type ?? 'http', url, ...(Object.keys(headers).length ? { headers } : {}) };
|
|
131
|
-
codexArgs.push('-c', `mcp_servers.${name}.url=${toml(url)}`);
|
|
132
|
-
if (Object.keys(headers).length) {
|
|
133
|
-
const inline = Object.entries(headers).map(([key, value]) => `${toml(key)}=${toml(value)}`).join(',');
|
|
134
|
-
codexArgs.push('-c', `mcp_servers.${name}.http_headers={${inline}}`);
|
|
135
|
-
}
|
|
136
163
|
}
|
|
137
164
|
}
|
|
138
165
|
const mcpPath = join(dir, 'mcp.json');
|
|
@@ -150,7 +177,7 @@ export function prepareAgentTools(snapshot, runner, env = process.env, worktree
|
|
|
150
177
|
].filter(Boolean).join('\n\n');
|
|
151
178
|
let prepared;
|
|
152
179
|
try {
|
|
153
|
-
prepared = prepareForRunner(runner, dir, snapshot, env, worktree, { codexHome: keptCodexHome });
|
|
180
|
+
prepared = prepareForRunner(runner, dir, snapshot, env, worktree, { codexHome: keptCodexHome, mcpServers });
|
|
154
181
|
} catch (error) {
|
|
155
182
|
rmSync(dir, { recursive: true, force: true });
|
|
156
183
|
throw error;
|
|
@@ -158,7 +185,12 @@ export function prepareAgentTools(snapshot, runner, env = process.env, worktree
|
|
|
158
185
|
const { pluginDir, codexHome, personalDeveloperInstructions } = prepared;
|
|
159
186
|
return {
|
|
160
187
|
mcpPath,
|
|
161
|
-
|
|
188
|
+
/** The names of the servers mounted, as Claude's allow rules need them. */
|
|
189
|
+
mcpServers: Object.keys(mcpServers),
|
|
190
|
+
mcpUnmounted,
|
|
191
|
+
/** The mount a fenced turn takes where the runner's file is ignored
|
|
192
|
+
* (`fencedMountFor`, projectToolRuntimes.mjs); null where it has none. */
|
|
193
|
+
fencedMount: fencedMountFor(runner, usable, (value) => expand(value, env), callerEnv),
|
|
162
194
|
instructions,
|
|
163
195
|
pluginDir,
|
|
164
196
|
codexHome,
|
|
@@ -179,3 +211,46 @@ export function toolReadout(snapshot, runner) {
|
|
|
179
211
|
}));
|
|
180
212
|
return [...mcp, ...instructions].slice(0, MAX_REPORT_TOOLS);
|
|
181
213
|
}
|
|
214
|
+
|
|
215
|
+
const MCP_JSON_MAX_BYTES = 1024 * 1024;
|
|
216
|
+
const MCP_JSON_MAX_NAMES = 500;
|
|
217
|
+
/**
|
|
218
|
+
* EVERY SERVER NAMED IN A `.mcp.json` FROM `dir` UP TO `/` (2026-10-11). An
|
|
219
|
+
* agent turn that mounts the machine's MCP servers is not strict, and Claude
|
|
220
|
+
* Code then reads `.mcp.json` in its cwd and every parent wherever a settings
|
|
221
|
+
* scope approves one (measured, 2.1.290). The base branch's copy is the
|
|
222
|
+
* project's only `.mcp.json` input to an agent, so each of these names is
|
|
223
|
+
* disabled for the turn (claudePosture.mjs `withMachineMcp`): the agent's edit
|
|
224
|
+
* of its worktree's file mounts only after review lands it on base. A server
|
|
225
|
+
* the machine should have everywhere belongs in its CLI's user scope.
|
|
226
|
+
*
|
|
227
|
+
* NULL when the answer cannot be bounded — a `.mcp.json` over 1 MiB, or more
|
|
228
|
+
* than 500 names — and the lane then mounts strictly (the base copy alone)
|
|
229
|
+
* rather than leave one discovered. A file that does not parse names nothing:
|
|
230
|
+
* the CLI cannot read it either.
|
|
231
|
+
*/
|
|
232
|
+
export function mcpJsonServerNamesAbove(dir) {
|
|
233
|
+
const names = new Set();
|
|
234
|
+
let at = resolve(dir);
|
|
235
|
+
for (;;) {
|
|
236
|
+
const file = join(at, '.mcp.json');
|
|
237
|
+
let raw = null;
|
|
238
|
+
try {
|
|
239
|
+
const st = statSync(file);
|
|
240
|
+
if (st.isFile()) {
|
|
241
|
+
if (st.size > MCP_JSON_MAX_BYTES) return null;
|
|
242
|
+
raw = readFileSync(file, 'utf8');
|
|
243
|
+
}
|
|
244
|
+
} catch { /* none here */ }
|
|
245
|
+
if (raw != null) {
|
|
246
|
+
try {
|
|
247
|
+
const servers = JSON.parse(raw)?.mcpServers;
|
|
248
|
+
if (servers && typeof servers === 'object' && !Array.isArray(servers)) for (const name of Object.keys(servers)) names.add(name);
|
|
249
|
+
} catch { /* the CLI cannot parse it either */ }
|
|
250
|
+
}
|
|
251
|
+
if (names.size > MCP_JSON_MAX_NAMES) return null;
|
|
252
|
+
const up = dirname(at);
|
|
253
|
+
if (up === at) return [...names];
|
|
254
|
+
at = up;
|
|
255
|
+
}
|
|
256
|
+
}
|