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