@oar-sh/oar 0.9.17 → 0.9.19
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/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +13 -1
- package/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
- package/CHANGELOG.md +234 -0
- package/README.md +13 -12
- package/package.json +1 -1
- package/server/claude-api-worker/claude-api-backend.mjs +345 -0
- package/server/claude-api-worker/claude-api-session-worker.mjs +29 -0
- package/server/claude-worker/claude-cli-pinned-env.mjs +150 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +5 -5
- package/server/claude-worker/claude-sdk-adapter.mjs +35 -9
- package/server/claude-worker/claude-session-process.mjs +166 -21
- package/server/claude-worker/claude-session-worker-main.mjs +3 -0
- package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
- package/server/claude-worker/claude-turn-publisher.mjs +117 -5
- package/server/claude-worker/claude-usage-totals.mjs +53 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +9 -9
- package/server/cursor-worker/cursor-remote-relay-tool.mjs +3 -3
- package/server/cursor-worker/cursor-turn-runner.mjs +4 -4
- package/server/cursor-worker/sdk-message-normalizer.mjs +2 -2
- package/server/db-schema.mjs +33 -1
- package/server/deepseek-worker/deepseek-claude-backend.mjs +14 -130
- package/server/grok-worker/acp-client.mjs +1 -1
- package/server/grok-worker/grok-turn-runner.mjs +4 -4
- package/server/grok-worker/sdk-message-normalizer.mjs +1 -1
- package/server/mcp/oar-mcp-server.mjs +11 -5
- package/server/migrations/0031-queue-provider.mjs +143 -0
- package/server/migrations/0032-runtime-session-billing-switch.mjs +94 -0
- package/server/migrations/0033-claude-api-usage-checkpoints.mjs +138 -0
- package/server/migrations/0034-claude-api-usage-totals.mjs +112 -0
- package/server/public/app/activity-replay-state.mjs +4 -3
- package/server/public/app/agent-sessions-settings-ui.js +1 -1
- package/server/public/app/agent-sessions-settings.mjs +1 -1
- package/server/public/app/api-client.js +52 -0
- package/server/public/app/bootstrap.js +153 -27
- package/server/public/app/claude-api-settings-ui.js +362 -0
- package/server/public/app/claude-billing-toggle-ui.js +224 -0
- package/server/public/app/claude-billing-toggle.mjs +112 -0
- package/server/public/app/context-tier-options.mjs +6 -2
- package/server/public/app/context-usage-view.mjs +1 -1
- package/server/public/app/conversation-provider-indicator.mjs +11 -0
- package/server/public/app/conversation-view.js +29 -3
- package/server/public/app/inbox-view.mjs +1 -1
- package/server/public/app/journal-view.js +127 -10
- package/server/public/app/model-selector-options.mjs +17 -1
- package/server/public/app/new-conversation-model-choice.mjs +21 -0
- package/server/public/app/provider-readiness.mjs +5 -2
- package/server/public/app/relay-error-ctas.mjs +50 -0
- package/server/public/app/relay-schedules-ui.js +1 -1
- package/server/public/app/settings-modal.js +11 -0
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +18 -1
- package/server/public/app/tool-activity-emoji.mjs +4 -3
- package/server/public/app/usage-limit-ui.js +136 -5
- package/server/public/index.html +189 -3
- package/server/relay-heap-guard.mjs +338 -0
- package/server/relay-self-restart.mjs +33 -7
- package/server/relay-tools.md +6 -6
- package/server/repositories/conversation-group-repository.mjs +1 -1
- package/server/repositories/message-repository.mjs +7 -0
- package/server/repositories/session-repository.mjs +8 -1
- package/server/routes/ask-user-routes.mjs +1 -1
- package/server/routes/conversation-group-routes.mjs +1 -1
- package/server/routes/messages-routes.mjs +678 -31
- package/server/routes/relay-ask-routes.mjs +3 -3
- package/server/routes/relay-event-routes.mjs +2 -2
- package/server/routes/relay-schedule-routes.mjs +1 -1
- package/server/routes/remote-relay-routes.mjs +3 -3
- package/server/routes/sessions-routes.mjs +365 -53
- package/server/server-runtime.mjs +373 -26
- package/server/services/about-service.mjs +1 -0
- package/server/services/background-work-view.mjs +2 -1
- package/server/services/claude-api-settings-service.mjs +575 -0
- package/server/services/claude-api-usage-service.mjs +361 -0
- package/server/services/claude-billing-swap-service.mjs +274 -0
- package/server/services/claude-billing-switch-service.mjs +343 -0
- package/server/services/plan-usage-claude-api.mjs +445 -0
- package/server/services/plan-usage-service.mjs +5 -0
- package/server/services/provider-model-selection.mjs +3 -2
- package/server/services/provider-reasoning-effort.mjs +9 -0
- package/server/services/relay-ask-service.mjs +1 -1
- package/server/services/relay-event-service.mjs +3 -2
- package/server/services/relay-schedule-service.mjs +1 -1
- package/server/services/remote-relay-caller-context.mjs +7 -0
- package/server/services/remote-relay-dispatcher.mjs +111 -17
- package/server/services/remote-relay-registry-service.mjs +1 -1
- package/server/services/session-worker-launch-service.mjs +61 -2
- package/server/services/session-worker-registry-service.mjs +28 -1
- package/server/services/session-worker-websocket-service.mjs +17 -0
- package/server/services/usage-limit-pause-service.mjs +55 -4
- package/server/services/worker-secret-redaction.mjs +2 -0
- package/shared/anthropic-api.mjs +202 -0
- package/shared/provider-routing.mjs +5 -3
- package/shared/relay-asks.mjs +11 -5
- package/shared/relay-events.mjs +2 -2
- package/shared/relay-schedules.mjs +2 -2
- package/shared/remote-relay-contract.mjs +54 -12
- package/shared/remote-relay-tool-core.mjs +23 -6
- package/shared/steer-settle-failure.mjs +1 -0
- package/shared/worker-runtime/api-client.mjs +2 -2
- package/shared/worker-runtime/heartbeat.mjs +5 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +6 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// The Claude API backend of the Claude session runner: Claude Code on the
|
|
2
|
+
// Anthropic API, billed to the API key saved in Settings → Providers → Claude
|
|
3
|
+
// API instead of the host's Claude login.
|
|
4
|
+
//
|
|
5
|
+
// - The CLI's environment: the key as ANTHROPIC_API_KEY, the Anthropic API as
|
|
6
|
+
// ANTHROPIC_BASE_URL, and nothing of the host's Claude login, another
|
|
7
|
+
// Anthropic configuration or another provider's switch. The host's Claude
|
|
8
|
+
// Code state directory stays (CLAUDE_CONFIG_DIR is left as the relay set
|
|
9
|
+
// it): the chats are Claude chats, with the Claude provider's sessions,
|
|
10
|
+
// transcripts, resume and task output.
|
|
11
|
+
// - The flag settings layer: the user's settings and the chat folder's
|
|
12
|
+
// (.claude/settings.json, .claude/settings.local.json) can set `env`
|
|
13
|
+
// (another ANTHROPIC_BASE_URL, an ANTHROPIC_API_KEY that replaces the key,
|
|
14
|
+
// an ANTHROPIC_AUTH_TOKEN that beats it, a proxy, another provider, custom
|
|
15
|
+
// headers, a CA) and `apiKeyHelper` (a script whose output beats it). The
|
|
16
|
+
// flag layer outranks them all, so it pins the base URL and the key, blanks
|
|
17
|
+
// the bearer token and the helper, and holds every other variable that
|
|
18
|
+
// routes the requests or decides whom the CLI trusts at the CLI's own value
|
|
19
|
+
// (CLAUDE_CLI_PINNED_ENV, shared with the DeepSeek backend). The spawn's
|
|
20
|
+
// `--settings` argument, which other users of the host can read, carries
|
|
21
|
+
// the endpoint and the blanks (claudeApiFlagSettings); the key and the
|
|
22
|
+
// worker's own proxy and TLS values go into the same layer over the CLI's
|
|
23
|
+
// stdin before the first message (liveFlagSettings, claude-sdk-adapter.mjs;
|
|
24
|
+
// scripts/claude-api-cli-proof.mjs shows it).
|
|
25
|
+
// - The check: the CLI's `system/init` names where it took its API key from.
|
|
26
|
+
// Anything but ANTHROPIC_API_KEY stops the turn as soon as the init is read
|
|
27
|
+
// (checkInit). The CLI does not wait for that verdict: it sends its first
|
|
28
|
+
// request some 50 to 90 ms after the init (scripts/claude-api-cli-proof.mjs),
|
|
29
|
+
// so one request may already be out on the other credential when the stop
|
|
30
|
+
// lands. The check limits a wrong credential to that one request; it does
|
|
31
|
+
// not prevent it.
|
|
32
|
+
// - The model, effort, thinking and images: Claude's, exactly as the Claude
|
|
33
|
+
// worker has them (`claudeModels`).
|
|
34
|
+
// - What it reports: no Claude plan usage (there is no plan) and no Claude
|
|
35
|
+
// usage limit (the API answers with its own errors, which describeFailure
|
|
36
|
+
// reads in plain words).
|
|
37
|
+
//
|
|
38
|
+
// - Hooks: none run, neither the user's nor the chat folder's, and no
|
|
39
|
+
// statusLine command (CLAUDE_CLI_KEY_CHAT_SETTINGS, on the command line).
|
|
40
|
+
//
|
|
41
|
+
// The agent's shell inherits the CLI's environment, the key included; the
|
|
42
|
+
// relay takes the saved key out of what the worker posts and logs.
|
|
43
|
+
|
|
44
|
+
import { resolveAnthropicApiBaseUrl } from '../../shared/anthropic-api.mjs';
|
|
45
|
+
import { isHostClaudeSessionVariable } from '../../shared/host-claude-session-env.mjs';
|
|
46
|
+
import { CLAUDE_CLI_KEY_CHAT_SETTINGS, claudeCliPinnedEnv } from '../claude-worker/claude-cli-pinned-env.mjs';
|
|
47
|
+
|
|
48
|
+
export const CLAUDE_API_BACKEND_ID = 'claude-api';
|
|
49
|
+
export const CLAUDE_API_BACKEND_LABEL = 'Claude API';
|
|
50
|
+
// Where the relay's launch puts the key (session-worker-launch-service.mjs).
|
|
51
|
+
export const CLAUDE_API_KEY_ENV = 'OAR_CLAUDE_API_KEY';
|
|
52
|
+
// The stable code of a turn stopped because the CLI took another credential.
|
|
53
|
+
export const CLAUDE_API_KEY_SOURCE_CODE = 'claude-api-key-source';
|
|
54
|
+
// The only API key source the check lets through: the key in the CLI's
|
|
55
|
+
// environment, which is this backend's.
|
|
56
|
+
export const EXPECTED_API_KEY_SOURCE = 'ANTHROPIC_API_KEY';
|
|
57
|
+
// The sources the SDK's `system/init` can name (sdk.d.ts, ApiKeySource). A
|
|
58
|
+
// note names one of these, never a text of the CLI's own.
|
|
59
|
+
const KNOWN_API_KEY_SOURCES = Object.freeze([
|
|
60
|
+
'ANTHROPIC_API_KEY',
|
|
61
|
+
'apiKeyHelper',
|
|
62
|
+
'/login managed key',
|
|
63
|
+
'none',
|
|
64
|
+
'user',
|
|
65
|
+
'project',
|
|
66
|
+
'org',
|
|
67
|
+
'temporary',
|
|
68
|
+
'oauth',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const BILLING_URL = 'https://platform.claude.com/settings/billing';
|
|
72
|
+
const LIMITS_URL = 'https://platform.claude.com/settings/limits';
|
|
73
|
+
const SETTINGS_PLACE = 'Settings → Providers → Claude API';
|
|
74
|
+
const KEY_SOURCE_GUIDANCE = 'Remove the apiKeyHelper or other credential from the host\'s Claude Code settings '
|
|
75
|
+
+ `(settings.json), check the key in ${SETTINGS_PLACE}, then send the message again.`;
|
|
76
|
+
|
|
77
|
+
// Variables that would hand the CLI another credential or point it at another
|
|
78
|
+
// endpoint or cloud (the bundled CLI, 2026-10-25). Every ANTHROPIC_* is
|
|
79
|
+
// dropped as well (FOREIGN_CLI_PREFIXES).
|
|
80
|
+
const FOREIGN_CLI_VARIABLES = Object.freeze([
|
|
81
|
+
// The host's Claude login, and the other ways to hand the CLI a login or
|
|
82
|
+
// a key.
|
|
83
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
84
|
+
'CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR',
|
|
85
|
+
'CLAUDE_CODE_OAUTH_REFRESH_TOKEN',
|
|
86
|
+
'CLAUDE_CODE_OAUTH_SCOPES',
|
|
87
|
+
'CLAUDE_CODE_OAUTH_CLIENT_ID',
|
|
88
|
+
'CLAUDE_CODE_CUSTOM_OAUTH_URL',
|
|
89
|
+
'CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR',
|
|
90
|
+
'CLAUDE_CODE_API_KEY_HELPER_TTL_MS',
|
|
91
|
+
// Another provider for the CLI's requests, and their auth bypasses.
|
|
92
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
93
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
94
|
+
'CLAUDE_CODE_USE_FOUNDRY',
|
|
95
|
+
'CLAUDE_CODE_USE_ANTHROPIC_AWS',
|
|
96
|
+
'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD',
|
|
97
|
+
'CLAUDE_CODE_USE_GATEWAY',
|
|
98
|
+
'CLAUDE_CODE_USE_MANTLE',
|
|
99
|
+
'CLAUDE_CODE_USE_CCR_V2',
|
|
100
|
+
'CLAUDE_CODE_SKIP_BEDROCK_AUTH',
|
|
101
|
+
'CLAUDE_CODE_SKIP_VERTEX_AUTH',
|
|
102
|
+
'CLAUDE_CODE_SKIP_FOUNDRY_AUTH',
|
|
103
|
+
'CLAUDE_CODE_SKIP_MANTLE_AUTH',
|
|
104
|
+
'CLAUDE_CODE_SKIP_ANTHROPIC_AWS_AUTH',
|
|
105
|
+
'CLAUDE_CODE_SKIP_ANTHROPIC_GOOGLE_CLOUD_AUTH',
|
|
106
|
+
'CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST',
|
|
107
|
+
// A host's model and effort pins: the turn's model and effort come from
|
|
108
|
+
// the relay, which bills them to the key.
|
|
109
|
+
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
110
|
+
'CLAUDE_CODE_SUBAGENT_MODEL_FORCE',
|
|
111
|
+
'CLAUDE_CODE_WORKFLOW_SUBAGENT_MODEL',
|
|
112
|
+
'CLAUDE_CODE_EFFORT_LEVEL',
|
|
113
|
+
'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
|
|
114
|
+
// Never set, and dropped when inherited: it forces the permission mode to
|
|
115
|
+
// default (plan mode is lost) and sandboxes Bash.
|
|
116
|
+
'CLAUDE_CODE_SUBPROCESS_ENV_SCRUB',
|
|
117
|
+
// What the relay hands its worker (session-worker-launch-service.mjs,
|
|
118
|
+
// TMUX_WORKER_EXPORTED_ENV_VARS): the worker's own settings, which neither
|
|
119
|
+
// Claude Code nor anything the agent runs reads. A shell of the agent's
|
|
120
|
+
// that starts a relay of its own would take the worker's kind, folders and
|
|
121
|
+
// tools file for its own. The relay's Claude configuration
|
|
122
|
+
// (CLAUDE_CONFIG_DIR, CLAUDE_CODE_EXECUTABLE) is not on it.
|
|
123
|
+
'COPILOT_ALLOW_ALL',
|
|
124
|
+
'GITHUB_COPILOT_PROMPT_MODE_EXTENSIONS',
|
|
125
|
+
'COPILOT_SDK_PATH',
|
|
126
|
+
'EXTENSION_PATH',
|
|
127
|
+
'COPILOT_MODEL',
|
|
128
|
+
'COPILOT_RELAY_MODEL',
|
|
129
|
+
'CLAUDE_RELAY_MODEL',
|
|
130
|
+
'CLAUDE_CLOUD_RELAY_MODEL',
|
|
131
|
+
'CLAUDE_API_RELAY_MODEL',
|
|
132
|
+
'CURSOR_RELAY_MODEL',
|
|
133
|
+
'CURSOR_AGENT_STORE_DIR',
|
|
134
|
+
'CURSOR_API_KEY',
|
|
135
|
+
'GROK_RELAY_MODEL',
|
|
136
|
+
'GROK_CLI_COMMAND',
|
|
137
|
+
'GROK_ALWAYS_APPROVE',
|
|
138
|
+
'SESSION_ID',
|
|
139
|
+
'COPILOT_WORKSPACE_ROOT',
|
|
140
|
+
'INIT_CWD',
|
|
141
|
+
]);
|
|
142
|
+
// The same by prefix: every Anthropic variable (the two the CLI needs are set
|
|
143
|
+
// again below), the relay's folders, config, tools file, worker kind and log
|
|
144
|
+
// file (COPILOT_WEB_RELAY_*), the Copilot provider settings, the DeepSeek
|
|
145
|
+
// worker's settings, every Claude gateway setting and the relay's own OAR_*
|
|
146
|
+
// switches (the key's own variable among them).
|
|
147
|
+
const FOREIGN_CLI_PREFIXES = Object.freeze([
|
|
148
|
+
'ANTHROPIC_',
|
|
149
|
+
'COPILOT_WEB_RELAY_',
|
|
150
|
+
'COPILOT_PROVIDER_',
|
|
151
|
+
'DEEPSEEK_',
|
|
152
|
+
'CLAUDE_GATEWAY_',
|
|
153
|
+
'OAR_',
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Whether a variable of the worker's stays out of the CLI's environment:
|
|
158
|
+
* another credential, endpoint or provider, the relay's settings for its
|
|
159
|
+
* worker, or the host Claude Code session's variables when the relay was
|
|
160
|
+
* started from inside one (shared/host-claude-session-env.mjs; the launcher
|
|
161
|
+
* drops those for every worker, the backend again, however the worker was
|
|
162
|
+
* started).
|
|
163
|
+
*/
|
|
164
|
+
function isForeignCliVariable(key) {
|
|
165
|
+
const name = String(key).toUpperCase();
|
|
166
|
+
return FOREIGN_CLI_VARIABLES.includes(name)
|
|
167
|
+
|| FOREIGN_CLI_PREFIXES.some((prefix) => name.startsWith(prefix))
|
|
168
|
+
|| isHostClaudeSessionVariable(name);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function text(value) {
|
|
172
|
+
return String(value ?? '').trim();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The environment the CLI runs with: the worker's own without anything
|
|
177
|
+
* foreign, then the key and the endpoint. `OAR_ANTHROPIC_API_BASE_URL` names
|
|
178
|
+
* a fake API for tests; only a loopback URL is taken from it
|
|
179
|
+
* (resolveAnthropicApiBaseUrl), anything else is the Anthropic API.
|
|
180
|
+
*/
|
|
181
|
+
export function claudeApiCliEnv(baseEnv = process.env, { apiKey = '', baseUrl = '' } = {}) {
|
|
182
|
+
const key = text(apiKey);
|
|
183
|
+
if (!key) throw new Error('claude-api-key-not-configured');
|
|
184
|
+
const next = {};
|
|
185
|
+
for (const [name, value] of Object.entries(baseEnv || {})) {
|
|
186
|
+
if (isForeignCliVariable(name)) continue;
|
|
187
|
+
next[name] = value;
|
|
188
|
+
}
|
|
189
|
+
next.ANTHROPIC_API_KEY = key;
|
|
190
|
+
next.ANTHROPIC_BASE_URL = text(baseUrl) || resolveAnthropicApiBaseUrl(baseEnv || {});
|
|
191
|
+
return next;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The flag settings layer every CLI of this backend is spawned with, on its
|
|
196
|
+
* command line: no key in it. It outranks the user's, the project's and the
|
|
197
|
+
* local settings, so their `env` cannot send the requests elsewhere or swap
|
|
198
|
+
* the key for a bearer token, and their `apiKeyHelper` cannot swap it for
|
|
199
|
+
* its own (nor run at the CLI's startup). No hooks and no statusLine command
|
|
200
|
+
* run (CLAUDE_CLI_KEY_CHAT_SETTINGS). Skills, plugins and MCP servers are
|
|
201
|
+
* left as the other layers have them.
|
|
202
|
+
*/
|
|
203
|
+
export function claudeApiFlagSettings(baseUrl) {
|
|
204
|
+
return {
|
|
205
|
+
...CLAUDE_CLI_KEY_CHAT_SETTINGS,
|
|
206
|
+
env: {
|
|
207
|
+
ANTHROPIC_BASE_URL: text(baseUrl),
|
|
208
|
+
ANTHROPIC_AUTH_TOKEN: '',
|
|
209
|
+
},
|
|
210
|
+
apiKeyHelper: '',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The same layer with the key, which the CLI takes over stdin before the
|
|
216
|
+
* first message (claude-sdk-adapter.mjs): a settings file's
|
|
217
|
+
* env.ANTHROPIC_API_KEY would otherwise replace the CLI's environment's.
|
|
218
|
+
* Every other variable that routes the CLI's requests, and with them the key,
|
|
219
|
+
* or decides whom it trusts on the way (the proxies, the other providers'
|
|
220
|
+
* switches and endpoints, the custom headers, the CA and TLS settings) holds
|
|
221
|
+
* the value of `cliEnv`, the environment the CLI was spawned with, or is
|
|
222
|
+
* blanked (claudeCliPinnedEnv).
|
|
223
|
+
*/
|
|
224
|
+
export function claudeApiLiveFlagSettings(baseUrl, apiKey, cliEnv = {}) {
|
|
225
|
+
const key = text(apiKey);
|
|
226
|
+
if (!key) throw new Error('claude-api-key-not-configured');
|
|
227
|
+
const settings = claudeApiFlagSettings(baseUrl);
|
|
228
|
+
return { ...settings, env: { ...claudeCliPinnedEnv(cliEnv), ...settings.env, ANTHROPIC_API_KEY: key } };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The source the CLI named, as a note may quote it: a known name, or a stand-in. */
|
|
232
|
+
function quotedApiKeySource(value) {
|
|
233
|
+
const source = typeof value === 'string' ? value : '';
|
|
234
|
+
if (KNOWN_API_KEY_SOURCES.includes(source)) return `"${source}"`;
|
|
235
|
+
return source ? 'a source this relay does not know' : 'no source';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Null when the CLI's `system/init` says it took the key from
|
|
240
|
+
* ANTHROPIC_API_KEY (the backend's), else the Error that stops the turn. Its
|
|
241
|
+
* message names the source the CLI reported, never a key. `afterTraffic`: the
|
|
242
|
+
* init came while the turn was already talking to the API, so one request
|
|
243
|
+
* may have gone out on the other credential.
|
|
244
|
+
*/
|
|
245
|
+
export function checkClaudeApiInit(sdkMessage, { afterTraffic = false } = {}) {
|
|
246
|
+
if (!sdkMessage || sdkMessage.type !== 'system' || sdkMessage.subtype !== 'init') return null;
|
|
247
|
+
const source = sdkMessage.apiKeySource;
|
|
248
|
+
if (source === EXPECTED_API_KEY_SOURCE) return null;
|
|
249
|
+
const error = new Error(
|
|
250
|
+
`Claude Code reported ${quotedApiKeySource(source)} as its API key source instead of the saved Claude API key, so the turn was stopped.`
|
|
251
|
+
+ (afterTraffic ? ' At most one request may have gone out on that credential.' : ''),
|
|
252
|
+
);
|
|
253
|
+
error.code = CLAUDE_API_KEY_SOURCE_CODE;
|
|
254
|
+
error.apiKeySource = KNOWN_API_KEY_SOURCES.includes(source) ? source : null;
|
|
255
|
+
error.guidance = KEY_SOURCE_GUIDANCE;
|
|
256
|
+
return error;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* `{ code, guidance }` for a failed request the Claude API has its own advice
|
|
261
|
+
* for, or null. Read from the error the runner stopped the turn with, the
|
|
262
|
+
* HTTP status the result reports, else from the status and the words the CLI
|
|
263
|
+
* quotes in its error text ("API Error: 401 …"). The account refusals come
|
|
264
|
+
* first: a spend limit can come as a 429, which is not a rate limit.
|
|
265
|
+
*/
|
|
266
|
+
export function describeClaudeApiFailure(errorText, { status = null, error = null } = {}) {
|
|
267
|
+
if (error?.code === CLAUDE_API_KEY_SOURCE_CODE) {
|
|
268
|
+
return { code: CLAUDE_API_KEY_SOURCE_CODE, guidance: error.guidance || KEY_SOURCE_GUIDANCE };
|
|
269
|
+
}
|
|
270
|
+
const message = text(errorText);
|
|
271
|
+
const code = Number(status);
|
|
272
|
+
if (/credit balance is too low/i.test(message)) {
|
|
273
|
+
return {
|
|
274
|
+
code: 'claude-api-credit',
|
|
275
|
+
guidance: `The Claude API account's credit balance is too low. Add credits in the Claude Console (${BILLING_URL}), then send the message again.`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (/API usage limits|enforced_spend_limit_reached/i.test(message)) {
|
|
279
|
+
return {
|
|
280
|
+
code: 'claude-api-spend-limit',
|
|
281
|
+
guidance: `The Claude API account has reached its spend limit. Raise the limit in the Claude Console (${LIMITS_URL}) or wait until it resets, then send the message again.`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (code === 402 || /\b402\b|billing_error/i.test(message)) {
|
|
285
|
+
return {
|
|
286
|
+
code: 'claude-api-billing',
|
|
287
|
+
guidance: `The Claude API refused the request for a billing reason. Check the payment details in the Claude Console (${BILLING_URL}), then send the message again.`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (
|
|
291
|
+
code === 401
|
|
292
|
+
|| code === 403
|
|
293
|
+
|| /\b40[13]\b|authentication_error|permission_error|invalid x-api-key|invalid api key/i.test(message)
|
|
294
|
+
) {
|
|
295
|
+
return {
|
|
296
|
+
code: 'claude-api-key',
|
|
297
|
+
guidance: `The Claude API did not accept the API key. Check it in ${SETTINGS_PLACE}, then send the message again.`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (code === 429 || code === 529 || /\b(?:429|529)\b|rate_limit_error|overloaded_error/i.test(message)) {
|
|
301
|
+
return {
|
|
302
|
+
code: 'claude-api-rate-limit',
|
|
303
|
+
guidance: `The Claude API is rate-limiting this key or is overloaded. Send the message again in a moment; the key's rate limits are in the Claude Console (${LIMITS_URL}).`,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function createClaudeApiClaudeBackend({ env = process.env } = {}) {
|
|
310
|
+
const apiKey = text(env[CLAUDE_API_KEY_ENV]);
|
|
311
|
+
const baseUrl = resolveAnthropicApiBaseUrl(env);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
id: CLAUDE_API_BACKEND_ID,
|
|
315
|
+
providerType: CLAUDE_API_BACKEND_ID,
|
|
316
|
+
label: CLAUDE_API_BACKEND_LABEL,
|
|
317
|
+
// Claude's models, effort, thinking, `[1m]`, images and sub-agent models,
|
|
318
|
+
// exactly as the Claude worker has them: the runner and the adapter take
|
|
319
|
+
// none of those from this backend.
|
|
320
|
+
claudeModels: true,
|
|
321
|
+
// No plan windows and no subscription usage limit on an API key, and no
|
|
322
|
+
// usage route of its own.
|
|
323
|
+
reportsPlanUsage: false,
|
|
324
|
+
reportsUsageLimit: false,
|
|
325
|
+
usageRoute: null,
|
|
326
|
+
describeFailure: describeClaudeApiFailure,
|
|
327
|
+
checkInit: checkClaudeApiInit,
|
|
328
|
+
/** The settings layer pin, merged into the spawn's `settings` (no key in it). */
|
|
329
|
+
flagSettings: () => claudeApiFlagSettings(baseUrl),
|
|
330
|
+
/**
|
|
331
|
+
* The layer again with the key and the routing and trust pins for a CLI
|
|
332
|
+
* spawned with `cliEnv`, over stdin before the first message.
|
|
333
|
+
*/
|
|
334
|
+
liveFlagSettings: (cliEnv = claudeApiCliEnv(env, { apiKey, baseUrl })) => claudeApiLiveFlagSettings(baseUrl, apiKey, cliEnv),
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The CLI's environment, built from the worker's own (which must not
|
|
338
|
+
* reach the CLI as it is: it carries OAR_CLAUDE_API_KEY and possibly the
|
|
339
|
+
* host's login or Anthropic settings).
|
|
340
|
+
*/
|
|
341
|
+
spawnEnv(baseEnv = process.env) {
|
|
342
|
+
return claudeApiCliEnv(baseEnv, { apiKey, baseUrl });
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude API session worker: the Claude session worker with its CLI on the
|
|
3
|
+
// Anthropic API and the key saved in Settings → Providers → Claude API
|
|
4
|
+
// (claude-api-backend.mjs). The relay's launch puts the key in
|
|
5
|
+
// OAR_CLAUDE_API_KEY (through the owner-only secret file under tmux) and the
|
|
6
|
+
// chat's model in CLAUDE_API_RELAY_MODEL. The CLI keeps the host's Claude
|
|
7
|
+
// Code state directory: the chats are Claude chats, resumed and read where
|
|
8
|
+
// the Claude worker reads them.
|
|
9
|
+
import { installWorkerLogFile, installWorkerLogRedaction } from '../../shared/worker-runtime/worker-log-file.mjs';
|
|
10
|
+
import { runClaudeSessionWorker } from '../claude-worker/claude-session-worker-main.mjs';
|
|
11
|
+
import { CLAUDE_API_KEY_ENV, createClaudeApiClaudeBackend } from './claude-api-backend.mjs';
|
|
12
|
+
|
|
13
|
+
installWorkerLogFile();
|
|
14
|
+
// The key never shows in this worker's console or log, whatever the CLI or
|
|
15
|
+
// the agent prints (installed after the log file, so its copy is redacted too).
|
|
16
|
+
installWorkerLogRedaction({ secrets: [process.env[CLAUDE_API_KEY_ENV]] });
|
|
17
|
+
|
|
18
|
+
const backend = createClaudeApiClaudeBackend({ env: process.env });
|
|
19
|
+
|
|
20
|
+
runClaudeSessionWorker({
|
|
21
|
+
logTag: 'claude-api-worker',
|
|
22
|
+
workerName: 'claude-api-session-worker',
|
|
23
|
+
// None set: the CLI's own default, as for a Claude chat without a model.
|
|
24
|
+
defaultModel: String(process.env.CLAUDE_API_RELAY_MODEL || '').trim(),
|
|
25
|
+
backend,
|
|
26
|
+
}).catch((error) => {
|
|
27
|
+
console.error('claude-api-session-worker fatal:', error?.stack || error?.message || error);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// The CLI's routing and trust variables a chat folder's settings must not
|
|
2
|
+
// set, shared by the backends that run Claude Code on a key of the relay's
|
|
3
|
+
// (DeepSeek, Claude API): each pins them in its live flag settings. And the
|
|
4
|
+
// settings both spawn the CLI with: no hooks (CLAUDE_CLI_KEY_CHAT_SETTINGS).
|
|
5
|
+
//
|
|
6
|
+
// The list comes from the bundled CLI and only from it. On every SDK or CLI
|
|
7
|
+
// bump, re-run both proofs (scripts/deepseek-cli-proof.mjs,
|
|
8
|
+
// scripts/claude-api-cli-proof.mjs) and the env-name grep over that binary
|
|
9
|
+
// (fixed names, grep -a, under timeout and nice; never a regex over the
|
|
10
|
+
// whole binary), and update the list and its source line below.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Every variable through which the env of the chat folder's settings
|
|
14
|
+
* (.claude/settings.json, settings.local.json) could send the CLI's requests,
|
|
15
|
+
* and with them the key, somewhere else, or change whom the CLI trusts on the
|
|
16
|
+
* way: the endpoints, the provider switches, the credentials, the proxies and
|
|
17
|
+
* the TLS trust. The DeepSeek and the Claude API backends pin each one in
|
|
18
|
+
* their live flag settings (liveFlagSettings), which the CLI applies over the
|
|
19
|
+
* folder's, to the value the CLI was spawned with: the worker's own, else the
|
|
20
|
+
* other case's for a proxy variable, else ''.
|
|
21
|
+
* '' is unset to the CLI for each of these (truthy or trimmed reads), except
|
|
22
|
+
* SSL_CERT_FILE and SSL_CERT_DIR: '' empties OpenSSL's and Node's system
|
|
23
|
+
* store, so those two are pinned only when the worker sets them.
|
|
24
|
+
*
|
|
25
|
+
* Source: the names Claude Code 2.1.304 (the bundled CLI) reads, by a grep
|
|
26
|
+
* of the binary; the CLI's own lists of endpoints (with their selection
|
|
27
|
+
* switches and companions) and credentials are in it as they are there. A
|
|
28
|
+
* deny-list, because a settings layer can only pin names known in advance.
|
|
29
|
+
* Left out, each for its reason: the model variables (same endpoint, same
|
|
30
|
+
* key), what the CLI already drops from a folder's settings or from all
|
|
31
|
+
* settings (CLAUDE_CONFIG_DIR, CLAUDE_CODE_CUSTOM_OAUTH_URL, the OTEL
|
|
32
|
+
* exporter endpoints, ANTHROPIC_UNIX_SOCKET, the host-managed provider
|
|
33
|
+
* switches, the file-descriptor credentials), a provider's own SDK settings
|
|
34
|
+
* (AWS_*, GOOGLE_*, AZURE_*, read only for the provider the switches here
|
|
35
|
+
* turn off), NODE_OPTIONS (at runtime the CLI reads only its system-CA
|
|
36
|
+
* flags, which add no CA), and what routes only other programs' traffic
|
|
37
|
+
* (NPM_CONFIG_*, GIT_*, the resolver's variables).
|
|
38
|
+
*/
|
|
39
|
+
export const CLAUDE_CLI_PINNED_ENV = Object.freeze([
|
|
40
|
+
// Endpoints: the CLI's list, its fallback and the headers sent with them.
|
|
41
|
+
'ANTHROPIC_BASE_URL',
|
|
42
|
+
'_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL',
|
|
43
|
+
'ANTHROPIC_CUSTOM_HEADERS',
|
|
44
|
+
'ANTHROPIC_BEDROCK_BASE_URL',
|
|
45
|
+
'ANTHROPIC_VERTEX_BASE_URL',
|
|
46
|
+
'ANTHROPIC_FOUNDRY_BASE_URL',
|
|
47
|
+
'ANTHROPIC_AWS_BASE_URL',
|
|
48
|
+
'ANTHROPIC_GOOGLE_CLOUD_BASE_URL',
|
|
49
|
+
'ANTHROPIC_BEDROCK_MANTLE_BASE_URL',
|
|
50
|
+
'CLAUDE_CODE_API_BASE_URL',
|
|
51
|
+
'CLAUDE_CODE_ARTIFACTS_API_BASE_URL',
|
|
52
|
+
'CLAUDE_CODE_ARTIFACTS_API_TOKEN',
|
|
53
|
+
'CLAUDE_CODE_ARTIFACT_ASSET_BASE_URL',
|
|
54
|
+
'CLAUDE_CODE_ARTIFACT_LIVE_BASE_URL',
|
|
55
|
+
'CLAUDE_CODE_ARTIFACT_SYNC_BASE_URL',
|
|
56
|
+
'CLAUDE_CODE_ARTIFACT_VIEWER_BASE_URL',
|
|
57
|
+
'CLAUDE_CODE_MEMORY_API_BASE_URL',
|
|
58
|
+
'CLAUDE_CODE_MEMORY_API_TOKEN',
|
|
59
|
+
// Provider switches.
|
|
60
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
61
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
62
|
+
'CLAUDE_CODE_USE_FOUNDRY',
|
|
63
|
+
'CLAUDE_CODE_USE_ANTHROPIC_AWS',
|
|
64
|
+
'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD',
|
|
65
|
+
'CLAUDE_CODE_USE_MANTLE',
|
|
66
|
+
'CLAUDE_CODE_USE_GATEWAY',
|
|
67
|
+
'CLAUDE_CODE_SKIP_BEDROCK_AUTH',
|
|
68
|
+
'CLAUDE_CODE_SKIP_VERTEX_AUTH',
|
|
69
|
+
'CLAUDE_CODE_SKIP_FOUNDRY_AUTH',
|
|
70
|
+
'CLAUDE_CODE_SKIP_ANTHROPIC_AWS_AUTH',
|
|
71
|
+
'CLAUDE_CODE_SKIP_ANTHROPIC_GOOGLE_CLOUD_AUTH',
|
|
72
|
+
'CLAUDE_CODE_SKIP_MANTLE_AUTH',
|
|
73
|
+
// Credentials: the CLI's list, the OAuth login, the federation profile and
|
|
74
|
+
// the proxy credential helper.
|
|
75
|
+
'ANTHROPIC_API_KEY',
|
|
76
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
77
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
78
|
+
'AWS_BEARER_TOKEN_BEDROCK',
|
|
79
|
+
'ANTHROPIC_FOUNDRY_API_KEY',
|
|
80
|
+
'ANTHROPIC_FOUNDRY_AUTH_TOKEN',
|
|
81
|
+
'ANTHROPIC_AWS_API_KEY',
|
|
82
|
+
'CLAUDE_CODE_OAUTH_REFRESH_TOKEN',
|
|
83
|
+
'CLAUDE_CODE_OAUTH_SCOPES',
|
|
84
|
+
'CLAUDE_CODE_OAUTH_CLIENT_ID',
|
|
85
|
+
'ANTHROPIC_PROFILE',
|
|
86
|
+
'ANTHROPIC_IDENTITY_TOKEN',
|
|
87
|
+
'ANTHROPIC_IDENTITY_TOKEN_FILE',
|
|
88
|
+
'ANTHROPIC_FEDERATION_RULE_ID',
|
|
89
|
+
'ANTHROPIC_ORGANIZATION_ID',
|
|
90
|
+
'ANTHROPIC_SERVICE_ACCOUNT_ID',
|
|
91
|
+
'ANTHROPIC_SCOPE',
|
|
92
|
+
'ANTHROPIC_WORKSPACE_ID',
|
|
93
|
+
'CLAUDE_CODE_ENABLE_PROXY_AUTH_HELPER',
|
|
94
|
+
// Proxies: the CLI reads both cases, the lower one first.
|
|
95
|
+
'HTTPS_PROXY',
|
|
96
|
+
'https_proxy',
|
|
97
|
+
'HTTP_PROXY',
|
|
98
|
+
'http_proxy',
|
|
99
|
+
'ALL_PROXY',
|
|
100
|
+
'all_proxy',
|
|
101
|
+
'NO_PROXY',
|
|
102
|
+
'no_proxy',
|
|
103
|
+
'CLAUDE_CODE_HTTPS_PROXY',
|
|
104
|
+
'CLAUDE_CODE_HTTP_PROXY',
|
|
105
|
+
// TLS trust and the client certificate.
|
|
106
|
+
'NODE_TLS_REJECT_UNAUTHORIZED',
|
|
107
|
+
'NODE_EXTRA_CA_CERTS',
|
|
108
|
+
'CLAUDE_CODE_CERT_STORE',
|
|
109
|
+
'CLAUDE_CODE_CLIENT_CERT',
|
|
110
|
+
'CLAUDE_CODE_CLIENT_KEY',
|
|
111
|
+
'CLAUDE_CODE_CLIENT_KEY_PASSPHRASE',
|
|
112
|
+
'SSL_CERT_FILE',
|
|
113
|
+
'SSL_CERT_DIR',
|
|
114
|
+
]);
|
|
115
|
+
// The two of CLAUDE_CLI_PINNED_ENV for which '' is not unset (see there).
|
|
116
|
+
const PINNED_ONLY_WHEN_SET = new Set(['SSL_CERT_FILE', 'SSL_CERT_DIR']);
|
|
117
|
+
// The proxy variables the CLI and the tools it runs read in either case.
|
|
118
|
+
const OTHER_CASE = new Map(
|
|
119
|
+
['HTTPS_PROXY', 'HTTP_PROXY', 'ALL_PROXY', 'NO_PROXY'].flatMap((name) => [
|
|
120
|
+
[name, name.toLowerCase()],
|
|
121
|
+
[name.toLowerCase(), name],
|
|
122
|
+
]),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The settings both backends spawn every CLI with, on its command line (their
|
|
127
|
+
* flagSettings): no hooks and no statusLine command, neither the user's nor
|
|
128
|
+
* the chat folder's nor a plugin's. Those run as the relay's user with the
|
|
129
|
+
* CLI's environment, the key in it, and a folder's SessionStart hook would run
|
|
130
|
+
* before the live pins land. The SDK has the spawn settings in force before
|
|
131
|
+
* any hook (sdk.d.ts, Options.settings; scripts/claude-cli-hooks-proof.mjs
|
|
132
|
+
* shows it). Hooks in managed settings still run: those are the host admin's.
|
|
133
|
+
* OAR's Claude worker registers no SDK hooks of its own.
|
|
134
|
+
*/
|
|
135
|
+
export const CLAUDE_CLI_KEY_CHAT_SETTINGS = Object.freeze({ disableAllHooks: true });
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The pins of CLAUDE_CLI_PINNED_ENV for a CLI spawned with `cliEnv`: the
|
|
139
|
+
* value it was spawned with, else the other case's (a proxy the worker sets
|
|
140
|
+
* as https_proxy holds for HTTPS_PROXY as well), else ''.
|
|
141
|
+
*/
|
|
142
|
+
export function claudeCliPinnedEnv(cliEnv = {}) {
|
|
143
|
+
const pins = {};
|
|
144
|
+
for (const name of CLAUDE_CLI_PINNED_ENV) {
|
|
145
|
+
const own = cliEnv[name] ?? cliEnv[OTHER_CASE.get(name)];
|
|
146
|
+
if (own !== undefined) pins[name] = String(own);
|
|
147
|
+
else if (!PINNED_ONLY_WHEN_SET.has(name)) pins[name] = '';
|
|
148
|
+
}
|
|
149
|
+
return pins;
|
|
150
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The relay's `
|
|
2
|
+
* The relay's `relay_tool` tool for the Claude worker, shaped as an SDK MCP
|
|
3
3
|
* tool definition (`{ name, description, inputSchema, handler }`) so the
|
|
4
4
|
* adapter can hand it to `createSdkMcpServer` next to the preview tool. On the
|
|
5
|
-
* wire it is `
|
|
5
|
+
* wire it is `mcp__relay__relay_tool`; the module never imports the SDK.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import * as z from 'zod';
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
REMOTE_RELAY_ACTIONS,
|
|
12
12
|
REMOTE_RELAY_IF_BUSY,
|
|
13
13
|
REMOTE_RELAY_MODES,
|
|
14
|
-
REMOTE_RELAY_PROVIDERS,
|
|
15
14
|
REMOTE_RELAY_SESSION_SCOPES,
|
|
16
15
|
REMOTE_RELAY_THINKING_DISPLAYS,
|
|
17
16
|
} from '../../shared/remote-relay-contract.mjs';
|
|
@@ -61,7 +60,8 @@ export const REMOTE_RELAY_TOOL_ZOD_SHAPE = {
|
|
|
61
60
|
before: text('before'),
|
|
62
61
|
include_activity: z.boolean().optional().describe(FIELDS.include_activity.description),
|
|
63
62
|
max_chars: number('max_chars'),
|
|
64
|
-
|
|
63
|
+
// Free text: the worker outlives relay updates, the relay checks the id.
|
|
64
|
+
provider: text('provider'),
|
|
65
65
|
model: text('model'),
|
|
66
66
|
cwd: text('cwd'),
|
|
67
67
|
repo: text('repo'),
|
|
@@ -140,7 +140,7 @@ export function createRemoteRelayToolDefinition({ api, getConversationId = () =>
|
|
|
140
140
|
// A tool exception would surface as a failed MCP call and can abort
|
|
141
141
|
// the turn; a refusal the model can read cannot.
|
|
142
142
|
const message = error?.message || String(error);
|
|
143
|
-
dbg('
|
|
143
|
+
dbg('relay_tool failed', message);
|
|
144
144
|
result = { ok: false, code: REMOTE_RELAY_CALL_FAILED, error: message };
|
|
145
145
|
}
|
|
146
146
|
return { content: [{ type: 'text', text: formatRemoteRelayToolResult(result) }] };
|
|
@@ -11,7 +11,7 @@ import { CLAUDE_LIVE_FLAG_SETTINGS_CODE, CLAUDE_LIVE_FLAG_SETTINGS_GUIDANCE } fr
|
|
|
11
11
|
export { CLAUDE_LIVE_FLAG_SETTINGS_CODE };
|
|
12
12
|
|
|
13
13
|
// The name the in-process MCP server is registered under; it prefixes every
|
|
14
|
-
// tool it carries on the wire (`mcp__relay__preview`, `
|
|
14
|
+
// tool it carries on the wire (`mcp__relay__preview`, `mcp__relay__relay_tool`).
|
|
15
15
|
export const RELAY_MCP_SERVER_NAME = 'relay';
|
|
16
16
|
|
|
17
17
|
// The per-server tool-call timeout the CLI applies to the relay's tools.
|
|
@@ -19,7 +19,7 @@ export const RELAY_MCP_SERVER_NAME = 'relay';
|
|
|
19
19
|
// value, else by the MCP_TOOL_TIMEOUT environment variable, else by 1e8 ms —
|
|
20
20
|
// and SDK servers are exempt from the idle timeout. Pinning it here keeps a
|
|
21
21
|
// host-wide MCP_TOOL_TIMEOUT (set for some other server) from cutting off a
|
|
22
|
-
//
|
|
22
|
+
// relay_tool wait: those run up to wait_seconds (≤ 600 s) plus however long
|
|
23
23
|
// an approval card sits unanswered.
|
|
24
24
|
export const RELAY_MCP_TOOL_TIMEOUT_MS = REMOTE_RELAY_TOOL_CALL_TIMEOUT_MS;
|
|
25
25
|
|
|
@@ -122,6 +122,23 @@ export function claudeSpawnSettings({ ultracode = false, autoCompactWindow = nul
|
|
|
122
122
|
return Object.keys(settings).length ? settings : null;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The spawn's `settings` with a backend's flag settings on top: null when
|
|
127
|
+
* neither has anything, the relay's object untouched when the backend has
|
|
128
|
+
* nothing to add. `env` is merged by name, so neither side drops the other's
|
|
129
|
+
* variables.
|
|
130
|
+
*/
|
|
131
|
+
export function mergeSpawnSettings(spawnSettings, backendSettings) {
|
|
132
|
+
const own = spawnSettings && typeof spawnSettings === 'object' ? spawnSettings : null;
|
|
133
|
+
const extra = backendSettings && typeof backendSettings === 'object' && Object.keys(backendSettings).length
|
|
134
|
+
? backendSettings
|
|
135
|
+
: null;
|
|
136
|
+
if (!extra) return own;
|
|
137
|
+
const merged = { ...(own || {}), ...extra };
|
|
138
|
+
if (own?.env || extra.env) merged.env = { ...(own?.env || {}), ...(extra.env || {}) };
|
|
139
|
+
return merged;
|
|
140
|
+
}
|
|
141
|
+
|
|
125
142
|
/**
|
|
126
143
|
* Live twin of the attribution spawn setting, for a value (OAR or off). A
|
|
127
144
|
* running CLI takes a new text through the flag layer (it arrives as a fresh
|
|
@@ -209,7 +226,7 @@ export function systemPromptForRelayMode(relayMode, { backgroundCommandLimitMs =
|
|
|
209
226
|
* payload. `getConversationId` is called per tool call because a process
|
|
210
227
|
* outlives the turn it was spawned for.
|
|
211
228
|
*
|
|
212
|
-
* `
|
|
229
|
+
* `relay_tool` is the exception: it rides along only when the worker found
|
|
213
230
|
* at least one paired remote (`remoteRelayTool`), because a tool that can do
|
|
214
231
|
* nothing on a relay without remotes would only cost context on every turn.
|
|
215
232
|
*/
|
|
@@ -375,7 +392,7 @@ export function startClaudeSession({
|
|
|
375
392
|
pathToClaudeCodeExecutable = '',
|
|
376
393
|
api = null,
|
|
377
394
|
getConversationId = () => '',
|
|
378
|
-
// Whether this process carries `
|
|
395
|
+
// Whether this process carries `relay_tool` (the worker's registration
|
|
379
396
|
// decision). Fixed for the process: the tool list is part of the spawn.
|
|
380
397
|
remoteRelayTool = false,
|
|
381
398
|
// Another endpoint for the CLI (null = Anthropic through the host's Claude
|
|
@@ -387,15 +404,24 @@ export function startClaudeSession({
|
|
|
387
404
|
dbg = () => {},
|
|
388
405
|
} = {}) {
|
|
389
406
|
const normalizedModel = String(model || '').trim().toLowerCase();
|
|
390
|
-
|
|
407
|
+
// A backend on Claude's own models (the Claude API backend) takes the
|
|
408
|
+
// model and the effort exactly as Claude does; only another provider's
|
|
409
|
+
// backend maps them into its terms.
|
|
410
|
+
const modelBackend = backend && backend.claudeModels !== true ? backend : null;
|
|
411
|
+
const effort = modelBackend ? '' : normalizeClaudeEffort(reasoningEffort);
|
|
391
412
|
const ultracode = effort === CLAUDE_ULTRACODE_EFFORT;
|
|
392
|
-
const effortOption =
|
|
393
|
-
const cliModel =
|
|
413
|
+
const effortOption = modelBackend ? modelBackend.effortOption(reasoningEffort) : (ultracode ? 'xhigh' : effort);
|
|
414
|
+
const cliModel = modelBackend ? modelBackend.cliModel(model) : String(model || '').trim();
|
|
394
415
|
// Spawn-time twin of the flag-settings helpers: the settings layer is the
|
|
395
416
|
// only way to hand a fresh CLI the session-scoped ultracode flag, the
|
|
396
417
|
// auto-compact window and the thinking pin, and all three share one
|
|
397
|
-
// `settings` object.
|
|
398
|
-
|
|
418
|
+
// `settings` object. A backend's own pins (no hooks for both, the Claude
|
|
419
|
+
// API backend's endpoint and blanked credentials) join them; theirs win a
|
|
420
|
+
// clash.
|
|
421
|
+
const spawnSettings = mergeSpawnSettings(
|
|
422
|
+
claudeSpawnSettings({ ultracode, autoCompactWindow, thinkingEnabled, attribution }),
|
|
423
|
+
typeof backend?.flagSettings === 'function' ? backend.flagSettings() : null,
|
|
424
|
+
);
|
|
399
425
|
// Relay tools need the worker's authenticated API helper; a caller that
|
|
400
426
|
// passes none (tests, probes) gets a session without them.
|
|
401
427
|
const relayMcpServer = typeof api === 'function'
|