@claudexor/harness-claude 1.0.1 → 2.1.3
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/dist/capability-profile.d.ts +3 -0
- package/dist/capability-profile.d.ts.map +1 -0
- package/dist/capability-profile.js +34 -0
- package/dist/capability-profile.js.map +1 -0
- package/dist/doctor-remedy.d.ts +9 -0
- package/dist/doctor-remedy.d.ts.map +1 -0
- package/dist/doctor-remedy.js +15 -0
- package/dist/doctor-remedy.js.map +1 -0
- package/dist/index.d.ts +71 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +545 -220
- package/dist/index.js.map +1 -1
- package/dist/interactive.d.ts +10 -3
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +31 -6
- package/dist/interactive.js.map +1 -1
- package/dist/native-home.d.ts +21 -0
- package/dist/native-home.d.ts.map +1 -0
- package/dist/native-home.js +61 -0
- package/dist/native-home.js.map +1 -0
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +206 -15
- package/dist/parse.js.map +1 -1
- package/dist/profile.d.ts +27 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +174 -0
- package/dist/profile.js.map +1 -0
- package/dist/smoke.d.ts +9 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +94 -0
- package/dist/smoke.js.map +1 -0
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -1,38 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { ConformanceReport as ConformanceReportSchema, HarnessCapabilityProfile as HarnessCapabilityProfileSchema, HarnessManifest as HarnessManifestSchema } from "@claudexor/schema";
|
|
5
|
-
import { HarnessUnavailableError, interactionChannelFromSpec, normalizeEffort, playwrightMcpArgs, providerScrubEnv, readonlyMechanism, resolveNpxBin, runCapture, runCliHarness, PROVIDER_SECRET_ENV } from "@claudexor/core";
|
|
1
|
+
import { ConformanceReport as ConformanceReportSchema, HarnessManifest as HarnessManifestSchema, } from "@claudexor/schema";
|
|
2
|
+
import { abortSignalFromSpec, browserMcpCommand, HarnessUnavailableError, interactionChannelFromSpec, labelStreams, needsScopedHomeKeychainBridge, normalizeEffort, providerScrubEnv, resolveHarnessBinary, runCapture, runCliHarness, PROVIDER_SECRET_ENV, selectStrictAuthRoute, selectedAuthAvailable, selectedAuthReady, shouldVerifyApiKey, } from "@claudexor/core";
|
|
6
3
|
import { resolveSecret } from "@claudexor/secrets";
|
|
7
4
|
import { CLAUDEXOR_VERSION, nowIso, redactSecrets } from "@claudexor/util";
|
|
5
|
+
import { CLAUDE_CAPABILITY_PROFILE } from "./capability-profile.js";
|
|
6
|
+
import { claudeNativeLoginRemedy } from "./doctor-remedy.js";
|
|
7
|
+
import { claudeNativeHomeEnv, defaultNativeClaudeConfigDir } from "./native-home.js";
|
|
8
|
+
export { defaultNativeClaudeConfigDir } from "./native-home.js";
|
|
8
9
|
import { createClaudeParser } from "./parse.js";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
preferred_source: null,
|
|
16
|
-
credential_transports: [
|
|
17
|
-
{ source: "native_session", kind: "config_file", relocatable_by: ["CONFIG_DIR"] },
|
|
18
|
-
{ source: "api_key_env", kind: "env_var", relocatable_by: ["ENV"] },
|
|
19
|
-
],
|
|
20
|
-
},
|
|
21
|
-
access_control: { readonly_mechanism: "tool_allowlist" },
|
|
22
|
-
isolation: { supported_containment: ["env_or_file_injection"] },
|
|
23
|
-
image_input: "base64_stream",
|
|
24
|
-
});
|
|
10
|
+
import { probeClaudeCredentialProfile, resolveClaudeProfileRoute } from "./profile.js";
|
|
11
|
+
export { canonicalProfileConfigDir } from "./profile.js";
|
|
12
|
+
import { smokeIsolatedApiKey, smokeIsolatedOAuthToken } from "./smoke.js";
|
|
13
|
+
import { claudeAttachmentBlocks, handleControlRequestFrame, initialSessionFrames, isControlRequestFrame, isResultFrame, } from "./interactive.js";
|
|
14
|
+
export const BIN = process.env.CLAUDEXOR_CLAUDE_BIN || "claude";
|
|
15
|
+
export const CLAUDE_PROVIDER_ENV_DENYLIST = PROVIDER_SECRET_ENV.filter((k) => k !== "ANTHROPIC_API_KEY");
|
|
25
16
|
/**
|
|
26
17
|
* Ordered (weakest→strongest) reasoning-effort levels `claude --effort` accepts.
|
|
27
18
|
* Verified against the installed CLI (`claude --help`, v2.1.165): the full
|
|
28
|
-
* ladder is low|medium|high|
|
|
19
|
+
* ladder is low|medium|high|max. SINGLE source for the manifest's
|
|
29
20
|
* `effort_levels` and the run-time normalizer (which now clamps nothing away).
|
|
30
21
|
*/
|
|
31
|
-
const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "
|
|
22
|
+
const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "max"];
|
|
23
|
+
/** Exported for focused route-policy tests; runtime uses this exact selector. */
|
|
24
|
+
export const selectClaudeRunAuthRoute = selectStrictAuthRoute;
|
|
32
25
|
function permissionArgs(access) {
|
|
33
26
|
switch (access) {
|
|
34
27
|
case "readonly":
|
|
35
|
-
|
|
28
|
+
// Defense in depth: plan mode rejects mutation requests, setting sources
|
|
29
|
+
// prevent user/project policy from widening the route, strict MCP ignores
|
|
30
|
+
// project servers, and slash commands/Chrome are independent tool ingress.
|
|
31
|
+
return [
|
|
32
|
+
"--permission-mode",
|
|
33
|
+
"plan",
|
|
34
|
+
"--setting-sources",
|
|
35
|
+
"",
|
|
36
|
+
"--strict-mcp-config",
|
|
37
|
+
"--disable-slash-commands",
|
|
38
|
+
"--no-chrome",
|
|
39
|
+
];
|
|
36
40
|
case "workspace_write":
|
|
37
41
|
return ["--permission-mode", "acceptEdits"];
|
|
38
42
|
case "full":
|
|
@@ -42,34 +46,120 @@ function permissionArgs(access) {
|
|
|
42
46
|
return [];
|
|
43
47
|
}
|
|
44
48
|
}
|
|
45
|
-
|
|
49
|
+
const CLAUDE_READONLY_REQUIRED_FLAGS = [
|
|
50
|
+
"--tools",
|
|
51
|
+
"--setting-sources",
|
|
52
|
+
"--strict-mcp-config",
|
|
53
|
+
"--permission-mode",
|
|
54
|
+
"--disable-slash-commands",
|
|
55
|
+
"--no-chrome",
|
|
56
|
+
];
|
|
57
|
+
let readonlyProbePromise = null;
|
|
58
|
+
export function probeClaudeReadonlyProfile(abortSignal) {
|
|
59
|
+
if (readonlyProbePromise)
|
|
60
|
+
return readonlyProbePromise;
|
|
61
|
+
readonlyProbePromise = (async () => {
|
|
62
|
+
try {
|
|
63
|
+
const result = await runCapture(BIN, ["--help"], {
|
|
64
|
+
timeoutMs: 10_000,
|
|
65
|
+
abortSignal,
|
|
66
|
+
cancelSignal: "SIGTERM",
|
|
67
|
+
cancelKillDelayMs: 0,
|
|
68
|
+
});
|
|
69
|
+
const help = `${result.stdout}\n${result.stderr}`;
|
|
70
|
+
const missingFlags = CLAUDE_READONLY_REQUIRED_FLAGS.filter((flag) => !help.includes(flag));
|
|
71
|
+
const hasPlanMode = help.includes('"plan"') || help.includes("plan,") || help.includes(", plan");
|
|
72
|
+
if (!hasPlanMode)
|
|
73
|
+
missingFlags.push("--permission-mode=plan");
|
|
74
|
+
return {
|
|
75
|
+
supported: result.code === 0 && missingFlags.length === 0,
|
|
76
|
+
missingFlags,
|
|
77
|
+
detail: result.code === 0 && missingFlags.length === 0
|
|
78
|
+
? "installed Claude CLI exposes the complete restrictive readonly flag set"
|
|
79
|
+
: `readonly enforcement unavailable; missing ${missingFlags.join(", ") || `help exited ${result.code}`}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return {
|
|
84
|
+
supported: false,
|
|
85
|
+
missingFlags: [...CLAUDE_READONLY_REQUIRED_FLAGS],
|
|
86
|
+
detail: `readonly enforcement probe failed: ${redactSecrets(error instanceof Error ? error.message : String(error))}`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
return readonlyProbePromise;
|
|
91
|
+
}
|
|
92
|
+
async function detectVersion(abortSignal) {
|
|
46
93
|
try {
|
|
47
|
-
const r = await runCapture(BIN, ["--version"], {
|
|
94
|
+
const r = await runCapture(BIN, ["--version"], {
|
|
95
|
+
timeoutMs: 10_000,
|
|
96
|
+
abortSignal,
|
|
97
|
+
cancelSignal: "SIGTERM",
|
|
98
|
+
cancelKillDelayMs: 0,
|
|
99
|
+
});
|
|
48
100
|
return r.stdout.trim() || `${BIN} (version unknown)`;
|
|
49
101
|
}
|
|
50
102
|
catch {
|
|
51
103
|
return null;
|
|
52
104
|
}
|
|
53
105
|
}
|
|
54
|
-
|
|
106
|
+
export function claudeNativeEnv(base, configDir) {
|
|
107
|
+
const raw = {
|
|
108
|
+
...(base ?? {}),
|
|
109
|
+
...providerScrubEnv(),
|
|
110
|
+
};
|
|
111
|
+
const native = needsScopedHomeKeychainBridge(CLAUDE_CAPABILITY_PROFILE)
|
|
112
|
+
? claudeNativeHomeEnv(raw)
|
|
113
|
+
: raw;
|
|
114
|
+
return {
|
|
115
|
+
...native,
|
|
116
|
+
CLAUDE_CONFIG_DIR: configDir ?? defaultNativeClaudeConfigDir(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export async function probeAuthStatus(bin = BIN, options = {}) {
|
|
55
120
|
try {
|
|
56
|
-
const env =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
121
|
+
const env = claudeNativeEnv(options.env, options.configDir);
|
|
122
|
+
const r = await (options.runCapture ?? runCapture)(bin, ["auth", "status"], {
|
|
123
|
+
env,
|
|
124
|
+
timeoutMs: 10_000,
|
|
125
|
+
abortSignal: options.abortSignal,
|
|
126
|
+
cancelSignal: "SIGTERM",
|
|
127
|
+
cancelKillDelayMs: 0,
|
|
128
|
+
});
|
|
129
|
+
try {
|
|
130
|
+
const verdict = JSON.parse(r.stdout.trim());
|
|
131
|
+
if (typeof verdict.loggedIn === "boolean" && typeof verdict.authMethod === "string") {
|
|
132
|
+
return {
|
|
133
|
+
loggedIn: verdict.loggedIn,
|
|
134
|
+
authed: verdict.loggedIn && verdict.authMethod === "claude.ai",
|
|
135
|
+
authMethod: verdict.authMethod,
|
|
136
|
+
probeError: null,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
/* no typed JSON verdict: fall through to probe-error disclosure */
|
|
142
|
+
}
|
|
143
|
+
const detail = labelStreams(r.stderr, r.stdout, { transform: redactSecrets }) ??
|
|
144
|
+
`claude auth status exited with ${r.code ?? r.signal ?? "unknown result"}`;
|
|
145
|
+
return { loggedIn: false, authed: false, authMethod: null, probeError: detail };
|
|
66
146
|
}
|
|
67
|
-
catch {
|
|
68
|
-
return
|
|
147
|
+
catch (err) {
|
|
148
|
+
return {
|
|
149
|
+
loggedIn: false,
|
|
150
|
+
authed: false,
|
|
151
|
+
authMethod: null,
|
|
152
|
+
probeError: [...redactSecrets(err instanceof Error ? err.message : String(err))]
|
|
153
|
+
.slice(0, 300)
|
|
154
|
+
.join(""),
|
|
155
|
+
};
|
|
69
156
|
}
|
|
70
157
|
}
|
|
71
|
-
function anthropicApiKey() {
|
|
72
|
-
return process.env.CLAUDEXOR_ANTHROPIC_API_KEY ||
|
|
158
|
+
export function anthropicApiKey() {
|
|
159
|
+
return (process.env.CLAUDEXOR_ANTHROPIC_API_KEY ||
|
|
160
|
+
resolveSecret("anthropic") ||
|
|
161
|
+
process.env.ANTHROPIC_API_KEY ||
|
|
162
|
+
null);
|
|
73
163
|
}
|
|
74
164
|
/** A stored/long-lived Claude Code OAuth (`claude setup-token`) for headless
|
|
75
165
|
* subscription auth. The hermetic kill switch is honored inside resolveSecret
|
|
@@ -77,92 +167,75 @@ function anthropicApiKey() {
|
|
|
77
167
|
function claudeOAuthToken() {
|
|
78
168
|
return resolveSecret("claude_oauth") || process.env.CLAUDE_CODE_OAUTH_TOKEN || null;
|
|
79
169
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/** True when a native Claude session exists and can be seeded into an envelope. */
|
|
124
|
-
function nativeClaudeSeedable() {
|
|
125
|
-
return existsSync(join(defaultNativeClaudeConfigDir(), ".credentials.json"));
|
|
126
|
-
}
|
|
127
|
-
async function smokeIsolatedApiKey() {
|
|
128
|
-
const key = anthropicApiKey();
|
|
129
|
-
if (!key)
|
|
130
|
-
return { ok: false, detail: "no API key fallback available" };
|
|
131
|
-
const dir = mkdtempSync(`${tmpdir()}/claudexor-claude-smoke-`);
|
|
132
|
-
try {
|
|
133
|
-
const env = Object.fromEntries(CLAUDE_PROVIDER_ENV_DENYLIST.map((name) => [name, null]));
|
|
134
|
-
env.HOME = dir;
|
|
135
|
-
env.XDG_CONFIG_HOME = `${dir}/.config`;
|
|
136
|
-
env.CLAUDE_CONFIG_DIR = dir;
|
|
137
|
-
env.ANTHROPIC_API_KEY = key;
|
|
138
|
-
const r = await runCapture(BIN, ["-p", "Reply exactly OK", "--output-format", "stream-json", "--verbose", "--permission-mode", "plan"], { cwd: dir, env, timeoutMs: 60_000 });
|
|
139
|
-
const text = `${r.stdout}\n${r.stderr}`;
|
|
140
|
-
if (r.code === 0 && text.includes("OK"))
|
|
141
|
-
return { ok: true, detail: "isolated CLAUDE_CONFIG_DIR smoke passed" };
|
|
142
|
-
return { ok: false, detail: redactClaudeDoctorDetail(text || `claude exited with code ${r.code}`) };
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
return { ok: false, detail: redactClaudeDoctorDetail(err instanceof Error ? err.message : String(err)) };
|
|
146
|
-
}
|
|
147
|
-
finally {
|
|
148
|
-
rmSync(dir, { recursive: true, force: true });
|
|
149
|
-
}
|
|
170
|
+
export function claudeAuthSourceReadiness(input) {
|
|
171
|
+
const nativeReady = input.native.authed && input.native.probeError === null;
|
|
172
|
+
const nativeAvailability = input.native.probeError
|
|
173
|
+
? "unknown"
|
|
174
|
+
: input.native.loggedIn
|
|
175
|
+
? "available"
|
|
176
|
+
: "unavailable";
|
|
177
|
+
const nativeVerification = nativeReady
|
|
178
|
+
? "passed"
|
|
179
|
+
: input.native.probeError || !input.native.loggedIn
|
|
180
|
+
? "not_run"
|
|
181
|
+
: "failed";
|
|
182
|
+
return [
|
|
183
|
+
{
|
|
184
|
+
source: "native_session",
|
|
185
|
+
availability: nativeAvailability,
|
|
186
|
+
verification: nativeVerification,
|
|
187
|
+
detail: nativeReady
|
|
188
|
+
? "vendor status confirmed authMethod=claude.ai in the exact run environment"
|
|
189
|
+
: input.native.probeError
|
|
190
|
+
? `auth-status probe failed: ${redactClaudeDoctorDetail(input.native.probeError)}`
|
|
191
|
+
: input.native.loggedIn
|
|
192
|
+
? `Claude is logged in via ${input.native.authMethod ?? "unknown"}, not claude.ai`
|
|
193
|
+
: "official native Claude session is not logged in",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
source: "oauth_token_env",
|
|
197
|
+
availability: input.oauthAvailable ? "available" : "unavailable",
|
|
198
|
+
verification: input.oauthVerification,
|
|
199
|
+
detail: input.oauthDetail,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
source: "api_key_env",
|
|
203
|
+
availability: input.apiKeyAvailable ? "available" : "unavailable",
|
|
204
|
+
verification: input.apiKeyVerification,
|
|
205
|
+
detail: input.apiKeyDetail,
|
|
206
|
+
},
|
|
207
|
+
];
|
|
150
208
|
}
|
|
151
|
-
function redactClaudeDoctorDetail(text) {
|
|
209
|
+
export function redactClaudeDoctorDetail(text) {
|
|
152
210
|
return redactSecrets(text).slice(0, 500);
|
|
153
211
|
}
|
|
154
|
-
export function createClaudeAdapter() {
|
|
212
|
+
export function createClaudeAdapter(deps = {}) {
|
|
213
|
+
const runtime = {
|
|
214
|
+
detectVersion,
|
|
215
|
+
probeAuthStatus,
|
|
216
|
+
anthropicApiKey,
|
|
217
|
+
claudeOAuthToken,
|
|
218
|
+
resolveProfileSecret: (ref) => resolveSecret(ref),
|
|
219
|
+
smokeIsolatedApiKey,
|
|
220
|
+
smokeIsolatedOAuthToken,
|
|
221
|
+
probeReadonlyProfile: probeClaudeReadonlyProfile,
|
|
222
|
+
runCliHarness,
|
|
223
|
+
...deps,
|
|
224
|
+
};
|
|
155
225
|
return {
|
|
156
226
|
id: "claude",
|
|
157
227
|
async discover() {
|
|
158
|
-
const version = await detectVersion();
|
|
228
|
+
const version = await runtime.detectVersion();
|
|
159
229
|
if (version === null) {
|
|
160
230
|
throw new HarnessUnavailableError("claude CLI not found on PATH (set CLAUDEXOR_CLAUDE_BIN to override)");
|
|
161
231
|
}
|
|
162
|
-
const apiKey = anthropicApiKey() !== null;
|
|
163
|
-
const
|
|
232
|
+
const apiKey = runtime.anthropicApiKey() !== null;
|
|
233
|
+
const readonlyProfile = await runtime.probeReadonlyProfile();
|
|
234
|
+
const native = await runtime.probeAuthStatus(BIN, { env: claudeNativeEnv() });
|
|
235
|
+
const authed = native.authed;
|
|
236
|
+
const oauthTokenAvailable = runtime.claudeOAuthToken() !== null;
|
|
164
237
|
const authModes = [
|
|
165
|
-
...(authed ? ["local_session"] : []),
|
|
238
|
+
...(authed || oauthTokenAvailable ? ["local_session"] : []),
|
|
166
239
|
...(apiKey ? ["api_key"] : []),
|
|
167
240
|
];
|
|
168
241
|
return HarnessManifestSchema.parse({
|
|
@@ -215,17 +288,31 @@ export function createClaudeAdapter() {
|
|
|
215
288
|
},
|
|
216
289
|
capability_profile: {
|
|
217
290
|
...CLAUDE_CAPABILITY_PROFILE,
|
|
291
|
+
access_control: {
|
|
292
|
+
readonly_mechanism: readonlyProfile.supported ? "tool_allowlist" : "none",
|
|
293
|
+
},
|
|
218
294
|
auth: {
|
|
219
295
|
...CLAUDE_CAPABILITY_PROFILE.auth,
|
|
220
|
-
preferred_source:
|
|
296
|
+
preferred_source: authed
|
|
297
|
+
? "native_session"
|
|
298
|
+
: oauthTokenAvailable
|
|
299
|
+
? "oauth_token_env"
|
|
300
|
+
: apiKey
|
|
301
|
+
? "api_key_env"
|
|
302
|
+
: null,
|
|
221
303
|
},
|
|
222
304
|
},
|
|
223
305
|
auth_modes: authModes,
|
|
224
|
-
access_profiles_supported: [
|
|
306
|
+
access_profiles_supported: [
|
|
307
|
+
...(readonlyProfile.supported ? ["readonly"] : []),
|
|
308
|
+
"workspace_write",
|
|
309
|
+
"full",
|
|
310
|
+
"inherit_native",
|
|
311
|
+
],
|
|
225
312
|
});
|
|
226
313
|
},
|
|
227
314
|
async doctor(_spec) {
|
|
228
|
-
const version = await detectVersion();
|
|
315
|
+
const version = await runtime.detectVersion(_spec.abortSignal);
|
|
229
316
|
if (version === null) {
|
|
230
317
|
return ConformanceReportSchema.parse({
|
|
231
318
|
harness_id: "claude",
|
|
@@ -234,45 +321,206 @@ export function createClaudeAdapter() {
|
|
|
234
321
|
reasons: ["claude CLI not found (install Claude Code or set CLAUDEXOR_CLAUDE_BIN)"],
|
|
235
322
|
});
|
|
236
323
|
}
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
324
|
+
const readonlyProfile = await runtime.probeReadonlyProfile(_spec.abortSignal);
|
|
325
|
+
const requestedSource = _spec.authSource;
|
|
326
|
+
const probeNative = requestedSource === undefined || requestedSource === "native_session";
|
|
327
|
+
const probeOAuth = requestedSource === undefined || requestedSource === "oauth_token_env";
|
|
328
|
+
const probeApi = requestedSource === undefined || requestedSource === "api_key_env";
|
|
329
|
+
const nativeEnv = probeNative ? claudeNativeEnv(_spec.env) : _spec.env;
|
|
330
|
+
const login = probeNative
|
|
331
|
+
? await runtime.probeAuthStatus(BIN, {
|
|
332
|
+
env: nativeEnv,
|
|
333
|
+
abortSignal: _spec.abortSignal,
|
|
334
|
+
})
|
|
335
|
+
: { loggedIn: false, authed: false, authMethod: null, probeError: null };
|
|
336
|
+
const nativeCliReady = login.authed;
|
|
337
|
+
// Native-session and stored setup-token proofs are separate sources.
|
|
338
|
+
const oauthToken = probeOAuth ? runtime.claudeOAuthToken() : null;
|
|
339
|
+
const oauthTokenAvailable = oauthToken !== null;
|
|
340
|
+
const apiKey = probeApi && runtime.anthropicApiKey() !== null;
|
|
341
|
+
const preference = requestedSource === "native_session" || requestedSource === "oauth_token_env"
|
|
342
|
+
? "subscription"
|
|
343
|
+
: requestedSource === "api_key_env"
|
|
344
|
+
? "api_key"
|
|
345
|
+
: (_spec.authPreference ?? "auto");
|
|
346
|
+
const shouldSmokeOAuth = probeOAuth && oauthToken !== null && !nativeCliReady && preference !== "api_key";
|
|
347
|
+
const oauthSmoke = shouldSmokeOAuth && oauthToken
|
|
348
|
+
? await runtime.smokeIsolatedOAuthToken(oauthToken, _spec.abortSignal)
|
|
349
|
+
: {
|
|
350
|
+
ok: false,
|
|
351
|
+
detail: oauthTokenAvailable
|
|
352
|
+
? "verification not run for the unselected setup-token route"
|
|
353
|
+
: "no Claude setup-token available",
|
|
354
|
+
};
|
|
355
|
+
const nativeAvailable = login.loggedIn || oauthTokenAvailable;
|
|
356
|
+
const subscriptionReady = nativeCliReady || oauthSmoke.ok;
|
|
357
|
+
const shouldSmokeKey = probeApi &&
|
|
358
|
+
shouldVerifyApiKey({ preference, apiKeyAvailable: apiKey, nativeReady: subscriptionReady });
|
|
359
|
+
const apiSmoke = shouldSmokeKey
|
|
360
|
+
? await runtime.smokeIsolatedApiKey(_spec.abortSignal)
|
|
361
|
+
: {
|
|
362
|
+
ok: false,
|
|
363
|
+
detail: apiKey
|
|
364
|
+
? "verification not run for the unselected API-key route"
|
|
365
|
+
: "no API key fallback available",
|
|
366
|
+
};
|
|
367
|
+
const ok = selectedAuthReady({
|
|
368
|
+
preference,
|
|
369
|
+
nativeReady: subscriptionReady,
|
|
370
|
+
apiKeyReady: apiSmoke.ok,
|
|
371
|
+
});
|
|
372
|
+
const selectedAvailable = selectedAuthAvailable({
|
|
373
|
+
preference,
|
|
374
|
+
nativeAvailable,
|
|
375
|
+
apiKeyAvailable: apiKey,
|
|
376
|
+
});
|
|
377
|
+
const probeUnknown = preference !== "api_key" && login.probeError !== null && !oauthTokenAvailable;
|
|
378
|
+
// INV-067: name the real cause + designed remedy (see doctor-remedy.ts).
|
|
379
|
+
const nativeLoginRemedy = claudeNativeLoginRemedy(nativeEnv);
|
|
380
|
+
const allIntents = [
|
|
381
|
+
"plan",
|
|
382
|
+
"spec",
|
|
383
|
+
"implement",
|
|
384
|
+
"repair",
|
|
385
|
+
"create_from_scratch",
|
|
386
|
+
"review",
|
|
387
|
+
"verify",
|
|
388
|
+
"synthesize",
|
|
389
|
+
"explain",
|
|
390
|
+
"audit",
|
|
391
|
+
"orchestrate",
|
|
392
|
+
];
|
|
393
|
+
const binPath = resolveHarnessBinary(BIN);
|
|
394
|
+
const producedSources = claudeAuthSourceReadiness({
|
|
395
|
+
native: login,
|
|
396
|
+
oauthAvailable: oauthTokenAvailable,
|
|
397
|
+
oauthVerification: oauthSmoke.ok ? "passed" : shouldSmokeOAuth ? "failed" : "not_run",
|
|
398
|
+
oauthDetail: oauthSmoke.detail,
|
|
399
|
+
apiKeyAvailable: apiKey,
|
|
400
|
+
apiKeyVerification: apiSmoke.ok ? "passed" : shouldSmokeKey ? "failed" : "not_run",
|
|
401
|
+
apiKeyDetail: apiSmoke.detail,
|
|
402
|
+
});
|
|
403
|
+
const authSources = requestedSource === undefined
|
|
404
|
+
? producedSources
|
|
405
|
+
: producedSources.filter((source) => source.source === requestedSource);
|
|
406
|
+
if (requestedSource !== undefined && authSources.length === 0) {
|
|
407
|
+
authSources.push({
|
|
408
|
+
source: requestedSource,
|
|
409
|
+
availability: "unavailable",
|
|
410
|
+
verification: "not_run",
|
|
411
|
+
detail: `Claude does not support ${requestedSource}`,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
const authReasons = ok
|
|
415
|
+
? []
|
|
416
|
+
: preference === "subscription"
|
|
417
|
+
? [
|
|
418
|
+
login.probeError && !oauthTokenAvailable
|
|
419
|
+
? `Claude native-session probe failed: ${redactClaudeDoctorDetail(login.probeError)}`
|
|
420
|
+
: oauthTokenAvailable
|
|
421
|
+
? `Claude setup-token verification failed: ${oauthSmoke.detail}`
|
|
422
|
+
: `Claude subscription route is not ready: ${nativeLoginRemedy}`,
|
|
423
|
+
]
|
|
424
|
+
: preference === "api_key"
|
|
425
|
+
? [
|
|
426
|
+
apiKey
|
|
427
|
+
? `isolated Claude API-key smoke failed: ${apiSmoke.detail}`
|
|
428
|
+
: "Claude API-key route is not configured",
|
|
429
|
+
]
|
|
430
|
+
: apiKey
|
|
431
|
+
? [`isolated Claude API-key smoke failed: ${apiSmoke.detail}`]
|
|
432
|
+
: [`not authenticated: ${nativeLoginRemedy}`];
|
|
246
433
|
return ConformanceReportSchema.parse({
|
|
247
434
|
harness_id: "claude",
|
|
248
|
-
status: ok
|
|
435
|
+
status: ok
|
|
436
|
+
? readonlyProfile.supported
|
|
437
|
+
? "ok"
|
|
438
|
+
: "degraded"
|
|
439
|
+
: selectedAvailable || probeUnknown
|
|
440
|
+
? "degraded"
|
|
441
|
+
: "unavailable",
|
|
249
442
|
checks: [
|
|
250
|
-
{
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
443
|
+
{
|
|
444
|
+
id: "installed",
|
|
445
|
+
status: "pass",
|
|
446
|
+
detail: binPath ? `${version} at ${binPath}` : version,
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: "readonly_enforcement",
|
|
450
|
+
status: readonlyProfile.supported ? "pass" : "fail",
|
|
451
|
+
detail: readonlyProfile.detail,
|
|
452
|
+
},
|
|
453
|
+
...(probeNative
|
|
454
|
+
? [
|
|
455
|
+
{
|
|
456
|
+
id: "native_session",
|
|
457
|
+
status: nativeCliReady ? "pass" : "fail",
|
|
458
|
+
detail: nativeCliReady
|
|
459
|
+
? "vendor status confirmed authMethod=claude.ai in the exact run environment"
|
|
460
|
+
: login.probeError
|
|
461
|
+
? `auth-status probe failed (NOT an auth verdict): ${redactClaudeDoctorDetail(login.probeError)}`
|
|
462
|
+
: login.loggedIn
|
|
463
|
+
? `logged in via ${login.authMethod ?? "unknown"}, not claude.ai`
|
|
464
|
+
: "not logged in (run `claude auth login --claudeai`)",
|
|
465
|
+
},
|
|
466
|
+
]
|
|
467
|
+
: []),
|
|
468
|
+
...(probeOAuth
|
|
469
|
+
? [
|
|
470
|
+
{
|
|
471
|
+
id: "oauth_setup_token",
|
|
472
|
+
status: oauthSmoke.ok ? "pass" : shouldSmokeOAuth ? "fail" : "skip",
|
|
473
|
+
detail: oauthSmoke.detail,
|
|
474
|
+
},
|
|
475
|
+
]
|
|
476
|
+
: []),
|
|
477
|
+
...(probeApi
|
|
478
|
+
? [
|
|
479
|
+
{
|
|
480
|
+
id: "stored_key",
|
|
481
|
+
status: apiKey ? "pass" : "fail",
|
|
482
|
+
detail: apiKey
|
|
483
|
+
? "anthropic secret/env available (API-key fallback)"
|
|
484
|
+
: "no anthropic key fallback",
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
id: "isolated_api_smoke",
|
|
488
|
+
status: apiSmoke.ok ? "pass" : shouldSmokeKey ? "fail" : "skip",
|
|
489
|
+
detail: apiSmoke.detail,
|
|
490
|
+
},
|
|
491
|
+
]
|
|
492
|
+
: []),
|
|
254
493
|
],
|
|
494
|
+
auth_sources: authSources,
|
|
255
495
|
enabled_intents: ok ? allIntents : [],
|
|
256
496
|
disabled_intents: ok ? [] : allIntents,
|
|
257
|
-
reasons:
|
|
258
|
-
? []
|
|
259
|
-
: apiKey
|
|
260
|
-
? [`isolated Claude API-key smoke failed: ${smoke.detail}`]
|
|
261
|
-
: ["not authenticated (run `claude /login` for native/subscription use, or store an anthropic API key fallback)"],
|
|
497
|
+
reasons: [...authReasons, ...(readonlyProfile.supported ? [] : [readonlyProfile.detail])],
|
|
262
498
|
});
|
|
263
499
|
},
|
|
264
500
|
run(spec) {
|
|
265
|
-
return runClaude(spec);
|
|
501
|
+
return runClaude(spec, runtime);
|
|
266
502
|
},
|
|
267
503
|
review(spec) {
|
|
268
|
-
return runClaude(spec);
|
|
504
|
+
return runClaude(spec, runtime);
|
|
505
|
+
},
|
|
506
|
+
probeCredentialProfile(profile, abortSignal) {
|
|
507
|
+
return probeClaudeCredentialProfile(profile, runtime, abortSignal);
|
|
269
508
|
},
|
|
270
509
|
};
|
|
271
510
|
}
|
|
272
511
|
/** Claude's native names for web-permissioned tools. This knowledge lives ONLY in the adapter. */
|
|
273
512
|
const CLAUDE_WEB_TOOLS = ["WebSearch", "WebFetch"];
|
|
274
|
-
const CLAUDE_READONLY_ALLOWED_TOOLS = ["Read", "Glob", "Grep"
|
|
275
|
-
const
|
|
513
|
+
const CLAUDE_READONLY_ALLOWED_TOOLS = ["Read", "Glob", "Grep"];
|
|
514
|
+
const CLAUDE_READONLY_BUILTIN_TOOLS = [...CLAUDE_READONLY_ALLOWED_TOOLS, ...CLAUDE_WEB_TOOLS];
|
|
515
|
+
const CLAUDE_READONLY_DENIED_TOOLS = [
|
|
516
|
+
"Bash",
|
|
517
|
+
"Write",
|
|
518
|
+
"Edit",
|
|
519
|
+
"MultiEdit",
|
|
520
|
+
"NotebookEdit",
|
|
521
|
+
"Agent",
|
|
522
|
+
"Skill",
|
|
523
|
+
];
|
|
276
524
|
export function claudeArgsForSpec(spec, interactive = false, suppressBare = false) {
|
|
277
525
|
// Interactive sessions deliver the prompt as a stream-json user message on
|
|
278
526
|
// stdin (the control protocol's transport); one-shot runs keep the prompt arg.
|
|
@@ -280,10 +528,30 @@ export function claudeArgsForSpec(spec, interactive = false, suppressBare = fals
|
|
|
280
528
|
// permission prompts (AskUserQuestion included) onto the control channel as
|
|
281
529
|
// control_request frames instead of headless auto-denial.
|
|
282
530
|
const args = interactive
|
|
283
|
-
? [
|
|
284
|
-
|
|
531
|
+
? [
|
|
532
|
+
"-p",
|
|
533
|
+
"--output-format",
|
|
534
|
+
"stream-json",
|
|
535
|
+
"--input-format",
|
|
536
|
+
"stream-json",
|
|
537
|
+
"--verbose",
|
|
538
|
+
"--permission-prompt-tool",
|
|
539
|
+
"stdio",
|
|
540
|
+
...permissionArgs(spec.access),
|
|
541
|
+
]
|
|
542
|
+
: [
|
|
543
|
+
"-p",
|
|
544
|
+
spec.prompt,
|
|
545
|
+
"--output-format",
|
|
546
|
+
"stream-json",
|
|
547
|
+
"--verbose",
|
|
548
|
+
...permissionArgs(spec.access),
|
|
549
|
+
];
|
|
285
550
|
if (spec.model_hint)
|
|
286
551
|
args.push("--model", spec.model_hint);
|
|
552
|
+
// W-C4 live deltas (engine-gated to single-candidate lanes; parser tags payload.delta).
|
|
553
|
+
if (spec.stream_deltas)
|
|
554
|
+
args.push("--include-partial-messages");
|
|
287
555
|
// Clamp onto claude's declared effort ladder; null = not
|
|
288
556
|
// requested OR not tunable -> pass no flag. Never sends an invalid level.
|
|
289
557
|
const eff = normalizeEffort(spec.effort_hint, CLAUDE_EFFORT_LEVELS);
|
|
@@ -291,6 +559,11 @@ export function claudeArgsForSpec(spec, interactive = false, suppressBare = fals
|
|
|
291
559
|
args.push("--effort", eff);
|
|
292
560
|
if (spec.max_turns !== null && spec.max_turns > 0)
|
|
293
561
|
args.push("--max-turns", String(spec.max_turns));
|
|
562
|
+
// Per-run caller instructions APPEND to (never replace) the default system
|
|
563
|
+
// prompt, current-invocation-only. The engine withholds them from synthesis,
|
|
564
|
+
// reviewers, and the auth smoke.
|
|
565
|
+
if (spec.instructions && spec.instructions.trim())
|
|
566
|
+
args.push("--append-system-prompt", spec.instructions);
|
|
294
567
|
// Structured output: constrain the FINAL message to the caller's JSON
|
|
295
568
|
// Schema. LIVE-VERIFIED (2.1.165): `--json-schema <inline JSON>` with
|
|
296
569
|
// --output-format stream-json. Passed only when the engine set it (the
|
|
@@ -319,6 +592,10 @@ export function claudeArgsForSpec(spec, interactive = false, suppressBare = fals
|
|
|
319
592
|
function toolPermissionArgs(spec) {
|
|
320
593
|
const { allow, deny } = toolPermissionSets(spec);
|
|
321
594
|
const args = [];
|
|
595
|
+
if (spec.access === "readonly") {
|
|
596
|
+
const builtins = CLAUDE_READONLY_BUILTIN_TOOLS.filter((tool) => allow.has(tool));
|
|
597
|
+
args.push("--tools", builtins.join(","));
|
|
598
|
+
}
|
|
322
599
|
if (allow.size > 0)
|
|
323
600
|
args.push("--allowedTools", [...allow].join(","));
|
|
324
601
|
if (deny.size > 0)
|
|
@@ -327,9 +604,11 @@ function toolPermissionArgs(spec) {
|
|
|
327
604
|
}
|
|
328
605
|
function toolPermissionSets(spec) {
|
|
329
606
|
const policy = spec.external_context_policy;
|
|
330
|
-
|
|
607
|
+
// A run may narrow readonly access but can never widen it. User/project
|
|
608
|
+
// Claude settings are independently suppressed by the readonly argv profile.
|
|
609
|
+
const allow = new Set(spec.access === "readonly" ? [] : spec.tool_permission_policy.allow);
|
|
331
610
|
const deny = new Set(spec.tool_permission_policy.deny);
|
|
332
|
-
if (spec.access === "readonly"
|
|
611
|
+
if (spec.access === "readonly") {
|
|
333
612
|
for (const tool of CLAUDE_READONLY_ALLOWED_TOOLS) {
|
|
334
613
|
if (!deny.has(tool))
|
|
335
614
|
allow.add(tool);
|
|
@@ -368,109 +647,155 @@ function toolPermissionSets(spec) {
|
|
|
368
647
|
function claudeBrowserArgs(spec) {
|
|
369
648
|
if (!spec.browser || spec.external_context_policy === "off")
|
|
370
649
|
return [];
|
|
371
|
-
const
|
|
650
|
+
const mcp = browserMcpCommand(spec.browser);
|
|
651
|
+
const cfg = JSON.stringify({
|
|
652
|
+
mcpServers: { browser: mcp },
|
|
653
|
+
});
|
|
372
654
|
return ["--mcp-config", cfg];
|
|
373
655
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
656
|
+
async function* runClaude(spec, runtime) {
|
|
657
|
+
const abortSignal = abortSignalFromSpec(spec);
|
|
658
|
+
if (spec.access === "readonly") {
|
|
659
|
+
const readonlyProfile = await runtime.probeReadonlyProfile(abortSignal);
|
|
660
|
+
if (!readonlyProfile.supported) {
|
|
661
|
+
yield {
|
|
662
|
+
type: "error",
|
|
663
|
+
session_id: spec.session_id,
|
|
664
|
+
ts: nowIso(),
|
|
665
|
+
error: `Claude readonly enforcement is unavailable: ${readonlyProfile.detail}`,
|
|
666
|
+
payload: {
|
|
667
|
+
code: "readonly_enforcement_unavailable",
|
|
668
|
+
missing_flags: readonlyProfile.missingFlags,
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
yield { type: "completed", session_id: spec.session_id, ts: nowIso() };
|
|
672
|
+
return;
|
|
385
673
|
}
|
|
386
674
|
}
|
|
387
|
-
return blocks;
|
|
388
|
-
}
|
|
389
|
-
async function* runClaude(spec) {
|
|
390
675
|
const channel = interactionChannelFromSpec(spec);
|
|
391
|
-
const
|
|
676
|
+
const attachmentBlocks = claudeAttachmentBlocks(spec.attachments);
|
|
392
677
|
// Images ride ONLY the stdin stream-json transport, so an attachment forces
|
|
393
678
|
// the interactive path even with no interaction channel (control frames then
|
|
394
679
|
// auto-decline). claudeArgsForSpec(interactive) selects --input-format stream-json.
|
|
395
|
-
const interactive = channel !== undefined ||
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
return true;
|
|
680
|
+
const interactive = channel !== undefined || attachmentBlocks.length > 0;
|
|
681
|
+
const profile = spec.credential_profile;
|
|
682
|
+
const authPreference = spec.auth_preference ?? "auto";
|
|
683
|
+
let nativeEnv = claudeNativeEnv(spec.env);
|
|
684
|
+
let key = null;
|
|
685
|
+
let oauthToken = null;
|
|
686
|
+
let subscriptionSource = null;
|
|
687
|
+
let route;
|
|
688
|
+
if (profile) {
|
|
689
|
+
const resolved = await resolveClaudeProfileRoute(profile, spec.env, runtime, abortSignal);
|
|
690
|
+
if (resolved.refusal !== null) {
|
|
691
|
+
yield { type: "error", session_id: spec.session_id, ts: nowIso(), error: resolved.refusal };
|
|
692
|
+
yield { type: "completed", session_id: spec.session_id, ts: nowIso() };
|
|
693
|
+
return;
|
|
410
694
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const canKey = key !== null;
|
|
414
|
-
const route = preferApi
|
|
415
|
-
? canKey
|
|
416
|
-
? "api_key"
|
|
417
|
-
: trySub()
|
|
418
|
-
? "subscription"
|
|
419
|
-
: null
|
|
420
|
-
: trySub()
|
|
421
|
-
? "subscription"
|
|
422
|
-
: canKey
|
|
423
|
-
? "api_key"
|
|
424
|
-
: null;
|
|
425
|
-
// An EXPLICIT auth preference that could not be honored is disclosed as a
|
|
426
|
-
// typed marker; the orchestrator lifts it into route.fallback.auth_switched.
|
|
427
|
-
const preferredRoute = preferApi ? "api_key" : "subscription";
|
|
428
|
-
if (spec.auth_preference !== "auto" && route !== null && route !== preferredRoute) {
|
|
429
|
-
yield {
|
|
430
|
-
type: "message",
|
|
431
|
-
session_id: spec.session_id,
|
|
432
|
-
ts: nowIso(),
|
|
433
|
-
text: `[auth] ${preferredRoute} route unavailable; fell back to ${route}`,
|
|
434
|
-
payload: {
|
|
435
|
-
auth_switched: true,
|
|
436
|
-
from_auth_mode: preferredRoute === "subscription" ? "local_session" : "api_key",
|
|
437
|
-
to_auth_mode: route === "subscription" ? "local_session" : "api_key",
|
|
438
|
-
},
|
|
439
|
-
};
|
|
695
|
+
({ nativeEnv, key, oauthToken, subscriptionSource } = resolved);
|
|
696
|
+
route = resolved.route;
|
|
440
697
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
698
|
+
else {
|
|
699
|
+
const native = authPreference === "api_key"
|
|
700
|
+
? { loggedIn: false, authed: false, authMethod: null, probeError: null }
|
|
701
|
+
: await runtime.probeAuthStatus(BIN, {
|
|
702
|
+
env: nativeEnv,
|
|
703
|
+
abortSignal,
|
|
704
|
+
});
|
|
705
|
+
// Explicit routes are strict; auto is subscription-first and alone may fall
|
|
706
|
+
// back to API-key auth. Preserve the exact selected subscription source so a
|
|
707
|
+
// native session can never be silently replaced by an OAuth-token env route.
|
|
708
|
+
const trySub = () => {
|
|
709
|
+
if (native.authed) {
|
|
710
|
+
subscriptionSource = "native_session";
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
if (authPreference === "auto")
|
|
714
|
+
oauthToken ??= runtime.claudeOAuthToken();
|
|
715
|
+
if (authPreference === "auto" && oauthToken !== null) {
|
|
716
|
+
subscriptionSource = "oauth_token_env";
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
return false;
|
|
447
720
|
};
|
|
448
|
-
|
|
449
|
-
|
|
721
|
+
route = selectClaudeRunAuthRoute(authPreference, trySub, () => {
|
|
722
|
+
key ??= runtime.anthropicApiKey();
|
|
723
|
+
return key !== null;
|
|
724
|
+
});
|
|
725
|
+
// Auto selecting its API-key fallback is a paid-route switch and must remain
|
|
726
|
+
// typed/visible; explicit routes never fall back.
|
|
727
|
+
if (authPreference === "auto" && route === "api_key") {
|
|
728
|
+
yield {
|
|
729
|
+
type: "message",
|
|
730
|
+
session_id: spec.session_id,
|
|
731
|
+
ts: nowIso(),
|
|
732
|
+
text: "[auth] native subscription route unavailable; auto selected api_key",
|
|
733
|
+
payload: {
|
|
734
|
+
auth_switched: true,
|
|
735
|
+
from_auth_mode: "local_session",
|
|
736
|
+
to_auth_mode: "api_key",
|
|
737
|
+
reason: "readiness_preferred",
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
if (route === null) {
|
|
742
|
+
yield {
|
|
743
|
+
type: "error",
|
|
744
|
+
session_id: spec.session_id,
|
|
745
|
+
ts: nowIso(),
|
|
746
|
+
error: authPreference === "subscription"
|
|
747
|
+
? "Claude subscription auth was explicitly requested but a verified claude.ai native session is not ready"
|
|
748
|
+
: authPreference === "api_key"
|
|
749
|
+
? "Claude API-key auth was explicitly requested but no Anthropic API key route is ready"
|
|
750
|
+
: "no usable Claude auth: native/setup-token subscription routes and API-key fallback are unavailable",
|
|
751
|
+
};
|
|
752
|
+
yield { type: "completed", session_id: spec.session_id, ts: nowIso() };
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
450
755
|
}
|
|
451
756
|
const useSubscription = route === "subscription";
|
|
452
757
|
const args = claudeArgsForSpec(spec, interactive, useSubscription);
|
|
453
758
|
// Scrub EVERY provider secret (incl. OpenAI/others — the cross-provider leak
|
|
454
759
|
// fix) via the single core table, then re-add only the var this route needs.
|
|
455
|
-
const env = { ...spec.env, ...providerScrubEnv() };
|
|
760
|
+
const env = subscriptionSource === "native_session" ? nativeEnv : { ...spec.env, ...providerScrubEnv() };
|
|
456
761
|
if (route === "api_key" && key) {
|
|
457
762
|
env.ANTHROPIC_API_KEY = key;
|
|
458
763
|
}
|
|
459
|
-
else if (
|
|
764
|
+
else if (subscriptionSource === "oauth_token_env" && oauthToken) {
|
|
460
765
|
env.CLAUDE_CODE_OAUTH_TOKEN = oauthToken;
|
|
461
766
|
}
|
|
462
|
-
|
|
767
|
+
// Route evidence: disclose the ACTUAL auth route on the started event
|
|
768
|
+
// (typed `auth_route` payload); quota attribution consumes it.
|
|
769
|
+
const credentialRoute = useSubscription
|
|
770
|
+
? "vendor_native"
|
|
771
|
+
: "managed_api_key";
|
|
772
|
+
const credentialSource = useSubscription ? subscriptionSource : "api_key_env";
|
|
773
|
+
const baseParser = createClaudeParser({ deniedTools: toolPermissionSets(spec).deny });
|
|
774
|
+
yield* runtime.runCliHarness({
|
|
463
775
|
bin: BIN,
|
|
464
776
|
args,
|
|
465
777
|
spec,
|
|
466
778
|
env,
|
|
467
779
|
label: "claude",
|
|
468
780
|
redact: redactSecrets,
|
|
469
|
-
parseEvent:
|
|
781
|
+
parseEvent: (obj, sessionId) => {
|
|
782
|
+
const out = baseParser(obj, sessionId);
|
|
783
|
+
if (out) {
|
|
784
|
+
for (const ev of out) {
|
|
785
|
+
// The auth route is fixed before spawn. Carry it on every event so
|
|
786
|
+
// a later api_retry/quota record remains independently attributable.
|
|
787
|
+
ev.credential_route = credentialRoute;
|
|
788
|
+
ev.credential_source = credentialSource;
|
|
789
|
+
if (profile)
|
|
790
|
+
ev.credential_profile_id = profile.profile_id;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return out;
|
|
794
|
+
},
|
|
470
795
|
...(interactive
|
|
471
796
|
? {
|
|
472
797
|
session: {
|
|
473
|
-
initialStdin: initialSessionFrames(spec.prompt,
|
|
798
|
+
initialStdin: initialSessionFrames(spec.prompt, attachmentBlocks),
|
|
474
799
|
matches: isControlRequestFrame,
|
|
475
800
|
handle: (obj, io) => handleControlRequestFrame(obj, io, spec.session_id, channel),
|
|
476
801
|
closeStdinOn: isResultFrame,
|