karajan-code 4.6.3 → 4.8.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/package.json +1 -1
- package/src/agents/base-agent.js +20 -0
- package/src/audit/deterministic-summary.js +1 -1
- package/src/audit/injection-findings.js +9 -0
- package/src/checks/ai-surface.js +76 -0
- package/src/cli/register-meta.js +2 -0
- package/src/commands/audit.js +5 -1
- package/src/commands/check.js +6 -1
- package/src/config/overrides.js +4 -1
- package/src/environment/briefs.js +1 -0
- package/src/environment/playbook.js +2 -0
- package/src/roles/audit-role.js +19 -11
- package/src/utils/role-env.js +57 -0
package/package.json
CHANGED
package/src/agents/base-agent.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { defaultEnvironment } from "../infrastructure/environment.js";
|
|
14
|
+
import { buildAgentEnv } from "../utils/role-env.js";
|
|
14
15
|
|
|
15
16
|
const MODEL_NOT_SUPPORTED_PATTERNS = [
|
|
16
17
|
/model.{0,30}is not supported/i,
|
|
@@ -49,6 +50,25 @@ export class BaseAgent {
|
|
|
49
50
|
* @returns {Promise<CommandRunResult>}
|
|
50
51
|
*/
|
|
51
52
|
async runCommand(command, args = [], options = {}) {
|
|
53
|
+
// KJC-TSK-0693: the subprocess gets an env allowlist, never the user's
|
|
54
|
+
// whole environment. security.env_allowlist:false opts out.
|
|
55
|
+
if (this.config?.security?.env_allowlist !== false) {
|
|
56
|
+
// Solomon-arbitrated contract (KJC-TSK-0693): an explicit env WITH a
|
|
57
|
+
// PATH is a COMPLETE child env (agents build those from process.env and
|
|
58
|
+
// deliberately strip vars — cleanExecaOpts drops CLAUDECODE, qwen
|
|
59
|
+
// unsets gemini's trust var) and is filtered AS GIVEN. An env WITHOUT
|
|
60
|
+
// a PATH is a partial overlay and merges over the inherited env first,
|
|
61
|
+
// so an overlay caller can never cost the child PATH/HOME.
|
|
62
|
+
const complete = options.env && ("PATH" in options.env || "Path" in options.env);
|
|
63
|
+
const base = complete ? options.env : { ...process.env, ...(options.env || {}) };
|
|
64
|
+
options = {
|
|
65
|
+
...options,
|
|
66
|
+
env: buildAgentEnv(base, {
|
|
67
|
+
agent: this.name,
|
|
68
|
+
passthrough: this.config?.security?.env_passthrough || [],
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
52
72
|
return this.environment.runner.run(command, args, options);
|
|
53
73
|
}
|
|
54
74
|
|
|
@@ -65,7 +65,7 @@ function formatAiSlopBlock(slop) {
|
|
|
65
65
|
|
|
66
66
|
function formatInjectionBlock(inj) {
|
|
67
67
|
if (!inj.available) return ["### Prompt-injection scan", `- Status: not available — ${inj.reason || "scan failed"}`, ""];
|
|
68
|
-
const lines = ["### Prompt-injection scan (
|
|
68
|
+
const lines = ["### Prompt-injection scan (agent-context surface: CLAUDE/AGENTS/GEMINI.md, templates, .rulesync, .karajan)", `- Files scanned: ${inj.scanned ?? 0}`, `- Findings: ${inj.total ?? 0}`];
|
|
69
69
|
if ((inj.total ?? 0) > 0) {
|
|
70
70
|
for (const [severity, items] of Object.entries(groupInjectionBySeverity(inj.findings || []))) {
|
|
71
71
|
if (items.length === 0) continue;
|
|
@@ -17,6 +17,15 @@ const SCAN_TARGETS = [
|
|
|
17
17
|
{ rel: ".karajan/specs", kind: "dir" },
|
|
18
18
|
{ rel: ".karajan/onboarding", kind: "dir" },
|
|
19
19
|
{ rel: ".karajan/domain.md", kind: "file" },
|
|
20
|
+
// KJC-TSK-0695: the agent-context surface — whatever lands in the host
|
|
21
|
+
// agent's context every session is the real injection vector.
|
|
22
|
+
{ rel: "CLAUDE.md", kind: "file" },
|
|
23
|
+
{ rel: "AGENTS.md", kind: "file" },
|
|
24
|
+
{ rel: "GEMINI.md", kind: "file" },
|
|
25
|
+
{ rel: ".cursorrules", kind: "file" },
|
|
26
|
+
{ rel: path.join(".github", "copilot-instructions.md"), kind: "file" },
|
|
27
|
+
{ rel: "templates", kind: "dir" },
|
|
28
|
+
{ rel: ".rulesync", kind: "dir" },
|
|
20
29
|
];
|
|
21
30
|
|
|
22
31
|
export async function collectInjectionFindings(rootDir, logger = null) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ai-surface — MCP inventory with drift detection (KJC-TSK-0694).
|
|
3
|
+
*
|
|
4
|
+
* Every new tool an agent gains is one more access. An inventory nobody
|
|
5
|
+
* reconciles is "the feeling of control without the control", so kj check
|
|
6
|
+
* flags what APPEARED since the last run and asks the user to own it.
|
|
7
|
+
* A nudge, never a gate. ~/.claude.json is scoped to its global block plus
|
|
8
|
+
* THIS project's entry — other projects' MCPs are not reachable here.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
|
|
15
|
+
const sources = (projectDir, home) => [
|
|
16
|
+
{ file: path.join(projectDir, ".mcp.json"), format: "json" },
|
|
17
|
+
{ file: path.join(home, ".claude.json"), format: "json" },
|
|
18
|
+
{ file: path.join(home, ".codex", "config.toml"), format: "toml" },
|
|
19
|
+
{ file: path.join(home, ".gemini", "settings.json"), format: "json" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function jsonServerNames(text, projectDir) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(text);
|
|
25
|
+
const names = new Set(Object.keys(parsed.mcpServers || {}));
|
|
26
|
+
for (const name of Object.keys(parsed.projects?.[projectDir]?.mcpServers || {})) names.add(name);
|
|
27
|
+
return names;
|
|
28
|
+
} catch {
|
|
29
|
+
return new Set();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const tomlServerNames = (text) =>
|
|
34
|
+
new Set([...text.matchAll(/^\s*\[mcp_servers\.["']?([^\]"']+)["']?\]/gm)].map((m) => m[1]));
|
|
35
|
+
|
|
36
|
+
/** Sorted `name (config-file)` entries for every MCP reachable in this project. */
|
|
37
|
+
export function collectAiSurface({ projectDir = process.cwd(), home = os.homedir() } = {}) {
|
|
38
|
+
const surface = new Set();
|
|
39
|
+
for (const { file, format } of sources(projectDir, home)) {
|
|
40
|
+
if (!existsSync(file)) continue;
|
|
41
|
+
let text;
|
|
42
|
+
try { text = readFileSync(file, "utf8"); } catch { continue; }
|
|
43
|
+
const names = format === "toml" ? tomlServerNames(text) : jsonServerNames(text, projectDir);
|
|
44
|
+
for (const name of names) surface.add(`${name} (${path.basename(file)})`);
|
|
45
|
+
}
|
|
46
|
+
return [...surface].sort();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Diff the current surface against the last-seen snapshot and persist the
|
|
51
|
+
* new state. First run records the baseline silently.
|
|
52
|
+
*/
|
|
53
|
+
export function checkAiSurface({ projectDir = process.cwd(), home = os.homedir(), statePath = path.join(os.homedir(), ".karajan", "ai-surface.json") } = {}) {
|
|
54
|
+
const surface = collectAiSurface({ projectDir, home });
|
|
55
|
+
let state = {};
|
|
56
|
+
try { state = JSON.parse(readFileSync(statePath, "utf8")); } catch { /* first run or corrupt → baseline */ }
|
|
57
|
+
const prev = Array.isArray(state[projectDir]) ? state[projectDir] : null;
|
|
58
|
+
const added = prev ? surface.filter((e) => !prev.includes(e)) : [];
|
|
59
|
+
const removed = prev ? prev.filter((e) => !surface.includes(e)) : [];
|
|
60
|
+
state[projectDir] = surface;
|
|
61
|
+
try {
|
|
62
|
+
mkdirSync(path.dirname(statePath), { recursive: true });
|
|
63
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
64
|
+
} catch { /* read-only FS — inventory still reports, just cannot remember */ }
|
|
65
|
+
return { surface, added, removed, first: !prev };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One line for kj check output. */
|
|
69
|
+
export function formatAiSurface({ surface, added, removed, first }) {
|
|
70
|
+
const head = `ai surface: ${surface.length} MCP(s)`;
|
|
71
|
+
if (first) return `${head} — baseline recorded`;
|
|
72
|
+
const parts = [head];
|
|
73
|
+
if (added.length) parts.push(`NEW since last check: ${added.join(", ")} — approved by you?`);
|
|
74
|
+
if (removed.length) parts.push(`gone: ${removed.join(", ")}`);
|
|
75
|
+
return parts.join(" · ");
|
|
76
|
+
}
|
package/src/cli/register-meta.js
CHANGED
|
@@ -379,6 +379,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
379
379
|
.option("--trend", "Append a sparkline of the harness score across the last 10 runs (uses .karajan/audit-history.db). KJC-TSK-0473.")
|
|
380
380
|
.option("--report-file <path>", "Write the audit report to disk in addition to stdout. <path> may be a file (extension drives format: .md or .json) or a directory (creates audit-<ISO>.<md|json> inside). $KJ_AUDIT_REPORT_DIR env var is used as default directory if no --report-file is given.")
|
|
381
381
|
.option("--deterministic-only", "Skip the LLM analysis entirely. Print/persist only the deterministic findings (basalCost, sonar, stack, growth-delta, webperf). Zero tokens spent. Compatible with --report-file and --json.")
|
|
382
|
+
.option("--security", "Focused deterministic security pass: prompt-injection scan over the agent-context surface (CLAUDE.md/AGENTS.md/templates/.rulesync/.karajan) + OSV + Semgrep + Sonar. Skips basal-cost, webperf, madge, knip, ai-slop, harness and the LLM — zero tokens. Designed for agents to self-invoke when a task touches sensitive surface (KJC-TSK-0695).")
|
|
382
383
|
.option("-y, --yes", "Auto-confirm the 'Continue with LLM analysis?' prompt. Useful in scripts that want the full audit non-interactively. CI/non-TTY paths already auto-confirm without this flag.")
|
|
383
384
|
.action(async (task, flags) => {
|
|
384
385
|
await withConfig(pkgVersion, "audit", flags, async ({ config, logger }) => {
|
|
@@ -402,6 +403,7 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
402
403
|
noAiSlop: flags.aiSlop === false,
|
|
403
404
|
reportFile: flags.reportFile || null,
|
|
404
405
|
deterministicOnly: Boolean(flags.deterministicOnly),
|
|
406
|
+
security: Boolean(flags.security),
|
|
405
407
|
yes: Boolean(flags.yes),
|
|
406
408
|
trend: Boolean(flags.trend),
|
|
407
409
|
});
|
package/src/commands/audit.js
CHANGED
|
@@ -237,7 +237,10 @@ function describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHar
|
|
|
237
237
|
return parts.join(" ");
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
export async function auditCommand({ task, config, logger, dimensions, json, agentReadiness, path: pathArg, noSonar = false, noOsv = false, noSemgrep = false, noHarness = false, noAiSlop = false, reportFile = null, deterministicOnly = false, yes = false, trend = false, promptFn = null }) {
|
|
240
|
+
export async function auditCommand({ task, config, logger, dimensions, json, agentReadiness, path: pathArg, noSonar = false, noOsv = false, noSemgrep = false, noHarness = false, noAiSlop = false, reportFile = null, deterministicOnly = false, yes = false, trend = false, promptFn = null, security = false }) {
|
|
241
|
+
// KJC-TSK-0695: --security = the focused pass an agent self-invokes.
|
|
242
|
+
// Deterministic by definition (zero tokens) and skips non-security stages.
|
|
243
|
+
if (security) { deterministicOnly = true; noHarness = true; noAiSlop = true; }
|
|
241
244
|
// --agent-readiness is a STANDALONE, deterministic, LLM-free audit
|
|
242
245
|
// dimension. It scores any third-party repo for AI-agent readability
|
|
243
246
|
// (llms.txt presence, page token budgets, robots allowlist, etc.).
|
|
@@ -281,6 +284,7 @@ export async function auditCommand({ task, config, logger, dimensions, json, age
|
|
|
281
284
|
noOsv,
|
|
282
285
|
noSemgrep,
|
|
283
286
|
noAiSlop,
|
|
287
|
+
...(security ? { securityOnly: true } : {}),
|
|
284
288
|
};
|
|
285
289
|
const deterministicCtx = await role.collectDeterministic(roleInput);
|
|
286
290
|
const deterministicMd = formatDeterministicSummary(deterministicCtx);
|
package/src/commands/check.js
CHANGED
|
@@ -6,21 +6,26 @@
|
|
|
6
6
|
|
|
7
7
|
import { checkHarden } from "../harden/check.js";
|
|
8
8
|
import { collectMethodStats, formatMethodStats } from "../checks/method.js";
|
|
9
|
+
import { checkAiSurface, formatAiSurface } from "../checks/ai-surface.js";
|
|
9
10
|
|
|
10
11
|
export async function checkCommand({ projectDir = process.cwd(), profile = "standard", json = false, logger = console } = {}) {
|
|
11
12
|
const result = await checkHarden({ projectDir, profile });
|
|
12
13
|
// KJC-TSK-0689 (MG-D): method adherence is VISIBILITY, not a gate — it
|
|
13
14
|
// rides along in check output but never affects the exit code.
|
|
14
15
|
const method = await collectMethodStats({ projectDir }).catch(() => null);
|
|
16
|
+
// KJC-TSK-0694: same deal for the MCP inventory — a nudge, never a gate.
|
|
17
|
+
let aiSurface = null;
|
|
18
|
+
try { aiSurface = checkAiSurface({ projectDir }); } catch { /* inventory is best-effort */ }
|
|
15
19
|
|
|
16
20
|
if (json) {
|
|
17
|
-
logger.info?.(JSON.stringify({ ...result, method }));
|
|
21
|
+
logger.info?.(JSON.stringify({ ...result, method, aiSurface }));
|
|
18
22
|
return result.ok ? 0 : 1;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
logger.info?.(`kj check (${profile})`);
|
|
22
26
|
for (const c of result.checks) logger.info?.(` ${c.ok ? "✓" : "✗"} ${c.id}: ${c.detail}`);
|
|
23
27
|
if (method) logger.info?.(` method: ${formatMethodStats(method)}`);
|
|
28
|
+
if (aiSurface) logger.info?.(` ${formatAiSurface(aiSurface)}`);
|
|
24
29
|
if (!result.ok) logger.info?.("Harness drift detected — run `kj harden` to repair.");
|
|
25
30
|
else logger.info?.("Harness OK.");
|
|
26
31
|
return result.ok ? 0 : 1;
|
package/src/config/overrides.js
CHANGED
|
@@ -77,8 +77,11 @@ const UNDEF_CHECK_FLAGS = [
|
|
|
77
77
|
];
|
|
78
78
|
|
|
79
79
|
function applyRoleOverrides(out, flags) {
|
|
80
|
+
// A provider override is always a string (`--security codex`). A bare
|
|
81
|
+
// boolean means the flag belongs to the command itself (`kj audit
|
|
82
|
+
// --security`, KJC-TSK-0695) and must not become a provider.
|
|
80
83
|
for (const [flag, role] of ROLE_PROVIDER_FLAGS) {
|
|
81
|
-
if (flags[flag]) out.roles[role].provider = flags[flag];
|
|
84
|
+
if (typeof flags[flag] === "string" && flags[flag]) out.roles[role].provider = flags[flag];
|
|
82
85
|
}
|
|
83
86
|
// coder/reviewer also update top-level aliases
|
|
84
87
|
if (flags.coder) out.coder = flags.coder;
|
|
@@ -79,6 +79,7 @@ const B = {
|
|
|
79
79
|
[
|
|
80
80
|
"These findings are never overridable — not even by Solomon arbitration.",
|
|
81
81
|
"PII is never logged; new dependencies need a reason and a pinned version.",
|
|
82
|
+
"Sensitive surface (auth, user input, secrets, network, deps)? Self-invoke `kj audit --security` — deterministic, zero tokens: prompt-injection over the agent-context files + OSV + Semgrep + Sonar — and remediate the findings before requesting review.",
|
|
82
83
|
],
|
|
83
84
|
"findings with severity, category and file:line — or an explicit 'no security surface touched'."),
|
|
84
85
|
},
|
|
@@ -66,6 +66,8 @@ Invariants (the git gates enforce these — they are not suggestions):
|
|
|
66
66
|
and it must be reviewed again. Disagree with a rejection? \`kj solomon\`.
|
|
67
67
|
- Security findings are never overridable — not even by arbitration. You
|
|
68
68
|
absorb the security role: \`kj brief security\` states what must be true.
|
|
69
|
+
Task touches auth, user input, secrets, network or deps? Run
|
|
70
|
+
\`kj audit --security\` (zero tokens) and remediate BEFORE the review.
|
|
69
71
|
- A design decision the card's AC don't cover belongs to the USER: record
|
|
70
72
|
it as a proposed ADR (\`kj adr add\`) and ask — never bury it in a PR
|
|
71
73
|
bullet. An oversized-diff warning is not an opinion either: partition,
|
package/src/roles/audit-role.js
CHANGED
|
@@ -48,13 +48,17 @@ export class AuditRole extends AgentRole {
|
|
|
48
48
|
* execute() would gather internally)
|
|
49
49
|
*/
|
|
50
50
|
async collectDeterministic(input) {
|
|
51
|
+
// KJC-TSK-0695: securityOnly = the focused pass an agent self-invokes
|
|
52
|
+
// when a task touches sensitive surface. Only the security collectors
|
|
53
|
+
// run (sonar, osv, semgrep, injection) — zero tokens, fast.
|
|
54
|
+
const securityOnly = typeof input === "object" ? Boolean(input?.securityOnly) : false;
|
|
51
55
|
const noSonar = typeof input === "object" ? Boolean(input?.noSonar) : false;
|
|
52
56
|
const noOsv = typeof input === "object" ? Boolean(input?.noOsv) : false;
|
|
53
57
|
const noSemgrep = typeof input === "object" ? Boolean(input?.noSemgrep) : false;
|
|
54
|
-
const noMadge = typeof input === "object" ? Boolean(input?.noMadge) : false;
|
|
55
|
-
const noKnip = typeof input === "object" ? Boolean(input?.noKnip) : false;
|
|
58
|
+
const noMadge = (typeof input === "object" ? Boolean(input?.noMadge) : false) || securityOnly;
|
|
59
|
+
const noKnip = (typeof input === "object" ? Boolean(input?.noKnip) : false) || securityOnly;
|
|
56
60
|
const noInjectionScan = typeof input === "object" ? Boolean(input?.noInjectionScan) : false;
|
|
57
|
-
const noAiSlop = typeof input === "object" ? Boolean(input?.noAiSlop) : false;
|
|
61
|
+
const noAiSlop = (typeof input === "object" ? Boolean(input?.noAiSlop) : false) || securityOnly;
|
|
58
62
|
const projectDir = this.config?.projectDir || process.cwd();
|
|
59
63
|
let basalCost = null;
|
|
60
64
|
let growthDelta = null;
|
|
@@ -67,11 +71,13 @@ export class AuditRole extends AgentRole {
|
|
|
67
71
|
let deadExports = null;
|
|
68
72
|
let injectionFindings = null;
|
|
69
73
|
let aiSlop = null;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
if (!securityOnly) {
|
|
75
|
+
try {
|
|
76
|
+
basalCost = await measureBasalCost(projectDir);
|
|
77
|
+
const previous = await loadPreviousAudit(projectDir);
|
|
78
|
+
growthDelta = computeGrowthDelta(basalCost, previous);
|
|
79
|
+
} catch { /* basal cost is best-effort */ }
|
|
80
|
+
}
|
|
75
81
|
try {
|
|
76
82
|
stack = await detectProjectStack(projectDir);
|
|
77
83
|
} catch { /* stack detect is best-effort */ }
|
|
@@ -80,9 +86,11 @@ export class AuditRole extends AgentRole {
|
|
|
80
86
|
sonarFindings = await collectSonarFindings(this.config, this.logger);
|
|
81
87
|
} catch { /* sonar fetch is best-effort */ }
|
|
82
88
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
if (!securityOnly) {
|
|
90
|
+
try {
|
|
91
|
+
webperf = collectWebPerfInput(stack, this.config);
|
|
92
|
+
} catch { /* webperf input is best-effort */ }
|
|
93
|
+
}
|
|
86
94
|
// OSV vulnerabilities — KJC-TSK-0365. Best-effort: missing
|
|
87
95
|
// osv-scanner binary returns available:false and the audit
|
|
88
96
|
// continues without the section.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* role-env — env allowlist for agent subprocesses (KJC-TSK-0693).
|
|
3
|
+
*
|
|
4
|
+
* Agents inherit only what their function requires: system essentials, the
|
|
5
|
+
* spawned CLI's own auth/config vars, and KJ_* lane vars. Long-lived secrets
|
|
6
|
+
* that no agent needs (cloud keys, registry tokens, DB URLs) never reach the
|
|
7
|
+
* child, so a compromised agent or an injected prompt cannot exfiltrate them.
|
|
8
|
+
* Escapes: `security.env_passthrough` (exact names or trailing-* globs) and
|
|
9
|
+
* `security.env_allowlist: false` to disable filtering entirely.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const EXACT = new Set([
|
|
13
|
+
"PATH", "Path", "HOME", "USER", "LOGNAME", "SHELL", "TERM", "COLORTERM", "LANG",
|
|
14
|
+
"TZ", "TMPDIR", "TMP", "TEMP", "EDITOR", "CI", "OLLAMA_HOST",
|
|
15
|
+
// Windows child-process essentials: executable resolution and profile dirs.
|
|
16
|
+
"SystemRoot", "SYSTEMROOT", "windir", "COMSPEC", "ComSpec", "PATHEXT",
|
|
17
|
+
"USERPROFILE", "HOMEDRIVE", "HOMEPATH", "APPDATA", "LOCALAPPDATA", "PROGRAMDATA",
|
|
18
|
+
// SSH agent socket: local-only handle, needed for git fetch/pull over SSH
|
|
19
|
+
// inside worktrees. Unusable off-machine, unlike a long-lived token.
|
|
20
|
+
"SSH_AUTH_SOCK", "SSH_AGENT_PID",
|
|
21
|
+
"HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
|
|
22
|
+
"http_proxy", "https_proxy", "no_proxy",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
// Per-CLI auth/config families. GOOGLE_ covers GOOGLE_APPLICATION_CREDENTIALS
|
|
26
|
+
// (gemini via Vertex); npm_ covers npm_config_* that Node tooling reads.
|
|
27
|
+
const PREFIXES = [
|
|
28
|
+
"LC_", "XDG_", "KJ_", "NODE_", "npm_",
|
|
29
|
+
"ANTHROPIC_", "CLAUDE", "OPENAI_", "CODEX", "GEMINI", "GOOGLE_",
|
|
30
|
+
"QWEN", "DASHSCOPE_", "OPENCODE", "AIDER_", "OPENROUTER_",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Vars only ONE agent authenticates with — granting them globally would
|
|
34
|
+
// defeat the point (the coder does not need your GitHub token; copilot does).
|
|
35
|
+
const AGENT_EXTRAS = {
|
|
36
|
+
copilot: ["GITHUB_", "GH_"],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const matches = (pattern, key) =>
|
|
40
|
+
pattern.endsWith("*") ? key.startsWith(pattern.slice(0, -1)) : key === pattern;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Filter an environment down to what the given agent's subprocess needs.
|
|
44
|
+
* @param {Record<string,string|undefined>} base - env to filter
|
|
45
|
+
* @param {{agent?: string|null, passthrough?: string[]}} [opts]
|
|
46
|
+
* @returns {Record<string,string|undefined>}
|
|
47
|
+
*/
|
|
48
|
+
export function buildAgentEnv(base = process.env, { agent = null, passthrough = [] } = {}) {
|
|
49
|
+
const prefixes = [...PREFIXES, ...(AGENT_EXTRAS[agent] || [])];
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const [key, value] of Object.entries(base)) {
|
|
52
|
+
if (EXACT.has(key) || prefixes.some((p) => key.startsWith(p)) || passthrough.some((p) => matches(p, key))) {
|
|
53
|
+
out[key] = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|