@oar-sh/oar 0.9.15 → 0.9.17
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/polling/polling-loop.mjs +14 -87
- package/CHANGELOG.md +305 -0
- package/README.md +12 -11
- package/package.json +2 -1
- package/server/claude-worker/claude-attachments.mjs +7 -4
- package/server/claude-worker/claude-remote-relay-tool.mjs +36 -1
- package/server/claude-worker/claude-sdk-adapter.mjs +167 -9
- package/server/claude-worker/claude-session-process.mjs +131 -33
- package/server/claude-worker/claude-session-worker-main.mjs +266 -0
- package/server/claude-worker/claude-session-worker.mjs +2 -253
- package/server/claude-worker/claude-turn-failure.mjs +8 -0
- package/server/claude-worker/claude-turn-publisher.mjs +282 -13
- package/server/claude-worker/claude-usage-totals.mjs +108 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +55 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
- package/server/copilot-worker/copilot-sdk-session-process.mjs +73 -7
- package/server/db-schema.mjs +115 -0
- package/server/deepseek-worker/deepseek-claude-backend.mjs +433 -0
- package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
- package/server/migrations/0019-relay-event-subscription-options.mjs +106 -0
- package/server/migrations/0020-conversation-may-create-sessions.mjs +67 -0
- package/server/migrations/0021-conversation-group-agent-marks.mjs +77 -0
- package/server/migrations/0022-relay-schedules.mjs +142 -0
- package/server/migrations/0023-relay-asks.mjs +141 -0
- package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
- package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
- package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
- package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
- package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
- package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
- package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
- package/server/public/app/api-client.js +53 -0
- package/server/public/app/ask-user-view.js +3 -0
- package/server/public/app/bootstrap.js +162 -16
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/context-usage-view.mjs +17 -0
- package/server/public/app/conversation-group-order.mjs +35 -0
- package/server/public/app/conversation-provider-indicator.mjs +7 -0
- package/server/public/app/conversation-view.js +2 -0
- package/server/public/app/deepseek-settings-ui.js +342 -0
- package/server/public/app/inbox-ui.js +367 -0
- package/server/public/app/inbox-view.mjs +370 -0
- package/server/public/app/journal-view.js +54 -5
- package/server/public/app/model-selector-options.mjs +11 -0
- package/server/public/app/model-vision-support.mjs +2 -0
- package/server/public/app/no-conversation-state.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +165 -13
- package/server/public/app/provider-readiness.mjs +4 -1
- package/server/public/app/relay-error-ctas.mjs +8 -0
- package/server/public/app/relay-event-subscriptions-ui.js +4 -2
- package/server/public/app/relay-event-view.mjs +33 -7
- package/server/public/app/relay-schedule-view.mjs +119 -0
- package/server/public/app/relay-schedules-ui.js +172 -0
- 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 +37 -3
- package/server/public/app/store.js +18 -5
- package/server/public/index.html +567 -35
- package/server/relay-tools.md +18 -3
- package/server/repositories/conversation-group-repository.mjs +110 -17
- package/server/repositories/remote-relay-repository.mjs +20 -2
- package/server/repositories/session-repository.mjs +7 -1
- package/server/routes/ask-user-routes.mjs +57 -3
- package/server/routes/cache-routes.mjs +6 -0
- package/server/routes/conversation-group-routes.mjs +19 -3
- package/server/routes/deepseek-usage-import-routes.mjs +72 -0
- package/server/routes/messages-routes.mjs +195 -18
- package/server/routes/relay-ask-routes.mjs +387 -0
- package/server/routes/relay-event-routes.mjs +3 -1
- package/server/routes/relay-schedule-routes.mjs +57 -0
- package/server/routes/sessions-routes.mjs +932 -97
- package/server/server-runtime.mjs +203 -11
- package/server/services/about-service.mjs +1 -0
- package/server/services/cache-rebuild-service.mjs +19 -2
- package/server/services/claude-session-leftovers.mjs +226 -0
- package/server/services/conversation-preferences-service.mjs +17 -0
- package/server/services/deepseek-balance-service.mjs +102 -0
- package/server/services/deepseek-settings-service.mjs +380 -0
- package/server/services/deepseek-usage-import.mjs +471 -0
- package/server/services/model-variant-catalog-service.mjs +1 -0
- package/server/services/plan-usage-contract.mjs +100 -5
- package/server/services/plan-usage-deepseek.mjs +931 -0
- package/server/services/plan-usage-service.mjs +431 -0
- package/server/services/provider-model-selection.mjs +1 -0
- package/server/services/provider-reasoning-effort.mjs +15 -2
- package/server/services/relay-ask-link-service.mjs +174 -0
- package/server/services/relay-ask-service.mjs +604 -0
- package/server/services/relay-event-service.mjs +589 -73
- package/server/services/relay-schedule-service.mjs +596 -0
- package/server/services/remote-relay-caller-context.mjs +89 -20
- package/server/services/remote-relay-dispatcher.mjs +958 -31
- package/server/services/session-worker-launch-service.mjs +233 -82
- package/server/services/worker-secret-redaction.mjs +318 -0
- package/shared/context-window-fallbacks.mjs +3 -0
- package/shared/deepseek-api.mjs +137 -0
- package/shared/deepseek-models.mjs +114 -0
- package/shared/deepseek-pricing.mjs +146 -0
- package/shared/host-claude-session-env.mjs +101 -0
- package/shared/model-id.mjs +7 -0
- package/shared/preview-tool-core.mjs +1 -1
- package/shared/provider-routing.mjs +2 -2
- package/shared/relay-asks.mjs +145 -0
- package/shared/relay-events.mjs +268 -18
- package/shared/relay-schedules.mjs +128 -0
- package/shared/remote-relay-contract.mjs +554 -26
- package/shared/remote-relay-tool-core.mjs +16 -5
- package/shared/steer-settle-failure.mjs +1 -0
- package/shared/worker-runtime/direct-openai-image.mjs +128 -0
- package/shared/worker-runtime/worker-log-file.mjs +60 -0
|
@@ -14,6 +14,11 @@ import {
|
|
|
14
14
|
isOarMcpServerDisabled,
|
|
15
15
|
} from '../mcp/oar-mcp-server.mjs';
|
|
16
16
|
import { WORKER_LOG_FILE_ENV } from '../../shared/worker-runtime/worker-log-file.mjs';
|
|
17
|
+
import {
|
|
18
|
+
HOST_CLAUDE_SESSION_ENV_VARS,
|
|
19
|
+
hostClaudeSessionUnsetStatement,
|
|
20
|
+
withoutHostClaudeSessionVariables,
|
|
21
|
+
} from '../../shared/host-claude-session-env.mjs';
|
|
17
22
|
import { missingWorkingDirectoryError } from '../../shared/worker-runtime/working-directory.mjs';
|
|
18
23
|
import {
|
|
19
24
|
describeSpawnEnvironment,
|
|
@@ -270,6 +275,42 @@ export function applyClaudeCloudProviderEnvironment(env = {}, {
|
|
|
270
275
|
return next;
|
|
271
276
|
}
|
|
272
277
|
|
|
278
|
+
/**
|
|
279
|
+
* DeepSeek: the Claude worker's runner against DeepSeek's Anthropic-compatible
|
|
280
|
+
* endpoint (server/deepseek-worker/). The key travels as DEEPSEEK_API_KEY,
|
|
281
|
+
* through the secret file under tmux, and becomes the CLI's bearer token
|
|
282
|
+
* only inside its spawn environment (deepseek-claude-backend.mjs). The worker
|
|
283
|
+
* keeps its Claude Code state in a directory of its own, which holds no
|
|
284
|
+
* Claude login.
|
|
285
|
+
*/
|
|
286
|
+
export function applyDeepSeekProviderEnvironment(env = {}, {
|
|
287
|
+
enabled = false,
|
|
288
|
+
model = '',
|
|
289
|
+
apiKey = '',
|
|
290
|
+
baseUrl = '',
|
|
291
|
+
configDir = '',
|
|
292
|
+
} = {}) {
|
|
293
|
+
const next = { ...env };
|
|
294
|
+
if (resolveWorkerKind(next) === 'deepseek') delete next.COPILOT_WEB_RELAY_WORKER_KIND;
|
|
295
|
+
delete next.DEEPSEEK_RELAY_MODEL;
|
|
296
|
+
delete next.DEEPSEEK_API_KEY;
|
|
297
|
+
delete next.DEEPSEEK_BASE_URL;
|
|
298
|
+
delete next.DEEPSEEK_CLAUDE_CONFIG_DIR;
|
|
299
|
+
if (!enabled) return next;
|
|
300
|
+
const normalizedApiKey = normalizeText(apiKey);
|
|
301
|
+
if (!normalizedApiKey) throw new Error('deepseek-api-key-not-configured');
|
|
302
|
+
const normalizedConfigDir = normalizeText(configDir);
|
|
303
|
+
if (!normalizedConfigDir) throw new Error('deepseek-config-dir-not-configured');
|
|
304
|
+
next.COPILOT_WEB_RELAY_WORKER_KIND = 'deepseek';
|
|
305
|
+
next.DEEPSEEK_API_KEY = normalizedApiKey;
|
|
306
|
+
next.DEEPSEEK_CLAUDE_CONFIG_DIR = path.resolve(normalizedConfigDir);
|
|
307
|
+
const normalizedModel = normalizeText(model);
|
|
308
|
+
if (normalizedModel) next.DEEPSEEK_RELAY_MODEL = normalizedModel;
|
|
309
|
+
const normalizedBaseUrl = normalizeText(baseUrl);
|
|
310
|
+
if (normalizedBaseUrl) next.DEEPSEEK_BASE_URL = normalizedBaseUrl;
|
|
311
|
+
return next;
|
|
312
|
+
}
|
|
313
|
+
|
|
273
314
|
/**
|
|
274
315
|
* The Copilot SDK engine: the same `github`/`openai` providers the extension
|
|
275
316
|
* serves, run as a plain Node worker against the CLI's headless runtime.
|
|
@@ -359,6 +400,10 @@ export function isClaudeCloudWorkerEnvironment(env = {}) {
|
|
|
359
400
|
return resolveWorkerKind(env) === 'claude-cloud';
|
|
360
401
|
}
|
|
361
402
|
|
|
403
|
+
export function isDeepSeekWorkerEnvironment(env = {}) {
|
|
404
|
+
return resolveWorkerKind(env) === 'deepseek';
|
|
405
|
+
}
|
|
406
|
+
|
|
362
407
|
export function isCopilotSdkWorkerEnvironment(env = {}) {
|
|
363
408
|
return resolveWorkerKind(env) === 'copilot-sdk';
|
|
364
409
|
}
|
|
@@ -403,6 +448,16 @@ export function resolveClaudeCloudWorkerScriptPath(env = {}) {
|
|
|
403
448
|
return path.join(process.cwd(), 'server', 'claude-cloud-worker', 'claude-cloud-session-worker.mjs');
|
|
404
449
|
}
|
|
405
450
|
|
|
451
|
+
export function resolveDeepSeekWorkerScriptPath(env = {}) {
|
|
452
|
+
const explicit = normalizeText(env?.COPILOT_WEB_RELAY_DEEPSEEK_WORKER_PATH);
|
|
453
|
+
if (explicit) return explicit;
|
|
454
|
+
const repoRoot = normalizeText(env?.COPILOT_WEB_RELAY_ROOT);
|
|
455
|
+
if (repoRoot) return path.join(repoRoot, 'server', 'deepseek-worker', 'deepseek-session-worker.mjs');
|
|
456
|
+
const serverDir = normalizeText(env?.COPILOT_WEB_RELAY_SERVER_DIR);
|
|
457
|
+
if (serverDir) return path.join(serverDir, 'deepseek-worker', 'deepseek-session-worker.mjs');
|
|
458
|
+
return path.join(process.cwd(), 'server', 'deepseek-worker', 'deepseek-session-worker.mjs');
|
|
459
|
+
}
|
|
460
|
+
|
|
406
461
|
export function resolveCopilotSdkWorkerScriptPath(env = {}) {
|
|
407
462
|
const explicit = normalizeText(env?.COPILOT_WEB_RELAY_COPILOT_SDK_WORKER_PATH);
|
|
408
463
|
if (explicit) return explicit;
|
|
@@ -431,6 +486,7 @@ const NODE_WORKER_DESCRIPTORS = Object.freeze({
|
|
|
431
486
|
resolveScriptPath: resolveCopilotSdkWorkerScriptPath,
|
|
432
487
|
windowsTitle: 'Copilot SDK Worker',
|
|
433
488
|
}),
|
|
489
|
+
deepseek: Object.freeze({ resolveScriptPath: resolveDeepSeekWorkerScriptPath, windowsTitle: 'DeepSeek Worker' }),
|
|
434
490
|
});
|
|
435
491
|
|
|
436
492
|
function resolveNodeWorkerDescriptor(env = {}) {
|
|
@@ -601,17 +657,42 @@ export function getTmuxPanePid(sessionName, {
|
|
|
601
657
|
}
|
|
602
658
|
}
|
|
603
659
|
|
|
604
|
-
export const WORKER_SECRET_ENV_VARS = Object.freeze([
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
660
|
+
export const WORKER_SECRET_ENV_VARS = Object.freeze([
|
|
661
|
+
'COPILOT_PROVIDER_API_KEY',
|
|
662
|
+
'CURSOR_API_KEY',
|
|
663
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
664
|
+
'DEEPSEEK_API_KEY',
|
|
665
|
+
'ANTHROPIC_API_KEY',
|
|
666
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
667
|
+
]);
|
|
668
|
+
|
|
669
|
+
// Secrets that are not every worker's: each entry names the kinds that get
|
|
670
|
+
// it, and every other kind has it taken out of its launch environment on
|
|
671
|
+
// every path (withoutForeignWorkerSecrets), and out of the tmux shell
|
|
672
|
+
// (buildTmuxWorkerShellCommand). A secret with no entry is every kind's.
|
|
673
|
+
//
|
|
674
|
+
// - The two API keys without an entry are put into the launch environment by
|
|
675
|
+
// their own provider's applier and cleared for every other one.
|
|
676
|
+
// - The Claude login token sits in the relay's own environment (a relay run on
|
|
677
|
+
// CLAUDE_CODE_OAUTH_TOKEN instead of the CLI's credentials file), so it
|
|
678
|
+
// would otherwise travel with every launch. Only the two workers of the
|
|
679
|
+
// host's own Claude account get it: the Claude Cloud worker's client reads
|
|
680
|
+
// the login from there exactly as the relay's does (credentials.mjs), and
|
|
681
|
+
// the Claude worker's CLI prefers it to the credentials file.
|
|
682
|
+
// - The DeepSeek key is put there by its own applier, but the same name could
|
|
683
|
+
// also sit in the relay's environment (a shell that exports it); it is the
|
|
684
|
+
// DeepSeek worker's alone either way.
|
|
685
|
+
// - A host's ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN are no worker's (an
|
|
686
|
+
// empty list): Claude Code prefers either to the Claude login, so a Claude
|
|
687
|
+
// chat would run, and be billed, on a key nobody chose for it, and every
|
|
688
|
+
// other kind has no use for them. A key a provider saves in the relay's
|
|
689
|
+
// settings goes to its own kind under a name of its own, with an entry
|
|
690
|
+
// here naming that kind alone.
|
|
613
691
|
const WORKER_SECRET_ENV_VAR_KINDS = Object.freeze({
|
|
614
|
-
CLAUDE_CODE_OAUTH_TOKEN: Object.freeze(['claude-cloud']),
|
|
692
|
+
CLAUDE_CODE_OAUTH_TOKEN: Object.freeze(['claude', 'claude-cloud']),
|
|
693
|
+
DEEPSEEK_API_KEY: Object.freeze(['deepseek']),
|
|
694
|
+
ANTHROPIC_API_KEY: Object.freeze([]),
|
|
695
|
+
ANTHROPIC_AUTH_TOKEN: Object.freeze([]),
|
|
615
696
|
});
|
|
616
697
|
|
|
617
698
|
/**
|
|
@@ -627,6 +708,29 @@ export function workerSecretEnvVarsFor(env = {}) {
|
|
|
627
708
|
});
|
|
628
709
|
}
|
|
629
710
|
|
|
711
|
+
/**
|
|
712
|
+
* The launch environment without the secrets that are another kind's. They
|
|
713
|
+
* would otherwise reach the worker on the detached and Windows paths, which
|
|
714
|
+
* hand the environment over as it is, and under tmux through the environment
|
|
715
|
+
* a tmux server started by the launch keeps as its global one.
|
|
716
|
+
*/
|
|
717
|
+
export function withoutForeignWorkerSecrets(env = {}) {
|
|
718
|
+
const own = new Set(workerSecretEnvVarsFor(env));
|
|
719
|
+
const next = { ...env };
|
|
720
|
+
for (const key of WORKER_SECRET_ENV_VARS) {
|
|
721
|
+
if (!own.has(key)) delete next[key];
|
|
722
|
+
}
|
|
723
|
+
return next;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// The host Claude Code session's variables (a relay started from inside a
|
|
727
|
+
// Claude Code session) are dropped from every launch path, for every kind:
|
|
728
|
+
// the names and families are in shared/host-claude-session-env.mjs, with
|
|
729
|
+
// what stays and why. Under tmux the worker's command unsets them again, by
|
|
730
|
+
// family too, from whatever the tmux server's environment holds
|
|
731
|
+
// (buildTmuxWorkerShellCommand).
|
|
732
|
+
export { HOST_CLAUDE_SESSION_ENV_VARS, withoutHostClaudeSessionVariables };
|
|
733
|
+
|
|
630
734
|
export function createWorkerSecretEnvFile(env = {}, {
|
|
631
735
|
fsImpl = fs,
|
|
632
736
|
tempRoot = os.tmpdir(),
|
|
@@ -697,6 +801,84 @@ export function prepareWorkerLogFile(targetSessionId, launchEnv = {}, {
|
|
|
697
801
|
}
|
|
698
802
|
}
|
|
699
803
|
|
|
804
|
+
// What a tmux worker is handed as `export`s in its own shell command. These
|
|
805
|
+
// are the values of one launch (the kind of worker, its model, its folder),
|
|
806
|
+
// so they never go into the environment tmux itself is started with: a tmux
|
|
807
|
+
// server started by a launch keeps that as its global environment, and every
|
|
808
|
+
// later session on the server would inherit the first launch's values where
|
|
809
|
+
// its own command exports none (launchSessionCli takes them out).
|
|
810
|
+
export const TMUX_WORKER_EXPORTED_ENV_VARS = Object.freeze([
|
|
811
|
+
'COPILOT_ALLOW_ALL',
|
|
812
|
+
'GITHUB_COPILOT_PROMPT_MODE_EXTENSIONS',
|
|
813
|
+
'COPILOT_WEB_RELAY_ROOT',
|
|
814
|
+
'COPILOT_WEB_RELAY_SERVER_DIR',
|
|
815
|
+
'COPILOT_WEB_RELAY_CONFIG',
|
|
816
|
+
'COPILOT_WEB_RELAY_TOOLS',
|
|
817
|
+
'COPILOT_WEB_RELAY_LOG_DIR',
|
|
818
|
+
// The Cursor worker keeps its agent stores in the relay's data dir
|
|
819
|
+
// (applyCursorProviderEnvironment).
|
|
820
|
+
'COPILOT_WEB_RELAY_DATA_DIR',
|
|
821
|
+
'COPILOT_WEB_RELAY_CLI_EXECUTABLE',
|
|
822
|
+
'COPILOT_WEB_RELAY_EXTENSION_BOOTSTRAP_PATH',
|
|
823
|
+
'COPILOT_SDK_PATH',
|
|
824
|
+
'EXTENSION_PATH',
|
|
825
|
+
'COPILOT_PROVIDER_TYPE',
|
|
826
|
+
'COPILOT_PROVIDER_BASE_URL',
|
|
827
|
+
'COPILOT_PROVIDER_WIRE_API',
|
|
828
|
+
'COPILOT_MODEL',
|
|
829
|
+
'COPILOT_WEB_RELAY_WORKER_KIND',
|
|
830
|
+
// The SDK worker's own model and BYOK context overrides. `COPILOT_MODEL`
|
|
831
|
+
// above is the extension engine's model variable and is read by the CLI's
|
|
832
|
+
// startup layer; the SDK worker takes its default model from
|
|
833
|
+
// `COPILOT_RELAY_MODEL`, matching every other Node worker's
|
|
834
|
+
// `*_RELAY_MODEL`. Without it here the model would silently drop to the
|
|
835
|
+
// runtime default on the tmux path while working on the detached path,
|
|
836
|
+
// which inherits the whole environment.
|
|
837
|
+
'COPILOT_RELAY_MODEL',
|
|
838
|
+
'COPILOT_PROVIDER_MAX_PROMPT_TOKENS',
|
|
839
|
+
'COPILOT_PROVIDER_MAX_OUTPUT_TOKENS',
|
|
840
|
+
// Without this the tmux path would fall back to ~/.claude while the relay
|
|
841
|
+
// (and the detached spawn path, which inherits the full environment) uses
|
|
842
|
+
// the override — worker and relay would read different config roots.
|
|
843
|
+
'CLAUDE_CONFIG_DIR',
|
|
844
|
+
// Same divergence, one variable over: the relay binds a resolved `claude`
|
|
845
|
+
// binary onto its own environment (cli-install-service.mjs §4.3), and the
|
|
846
|
+
// detached path inherits it for free. Without it here a bound binary would
|
|
847
|
+
// be silently dropped on the tmux path and the worker would fall back to
|
|
848
|
+
// whatever `claude` PATH happens to resolve — exactly the failure mode
|
|
849
|
+
// CLAUDE_CONFIG_DIR above was added for. GROK_CLI_COMMAND, its Grok
|
|
850
|
+
// counterpart, has always been on this list.
|
|
851
|
+
'CLAUDE_CODE_EXECUTABLE',
|
|
852
|
+
'CLAUDE_RELAY_MODEL',
|
|
853
|
+
// The Claude Cloud worker's model. Its login is not listed here: the
|
|
854
|
+
// worker reads the CLI's credentials file (CLAUDE_CONFIG_DIR above tells
|
|
855
|
+
// it where), and a relay-level CLAUDE_CODE_OAUTH_TOKEN is a secret
|
|
856
|
+
// (workerSecretEnvVarsFor), so it comes through the secret file.
|
|
857
|
+
'CLAUDE_CLOUD_RELAY_MODEL',
|
|
858
|
+
// Tests point the relay's cloud client at a fake API on this machine;
|
|
859
|
+
// the worker's client has to follow, or the relay would talk to the fake
|
|
860
|
+
// and its worker to the real API. Not a secret, and harmless as a value:
|
|
861
|
+
// shared/claude-cloud/base-url.mjs ignores everything but a loopback URL.
|
|
862
|
+
'OAR_CLAUDE_CLOUD_API_BASE_URL',
|
|
863
|
+
'CURSOR_RELAY_MODEL',
|
|
864
|
+
'CURSOR_AGENT_STORE_DIR',
|
|
865
|
+
// The DeepSeek worker's model, endpoint and Claude Code state directory.
|
|
866
|
+
// Its key is a secret (workerSecretEnvVarsFor) and comes through the file.
|
|
867
|
+
'DEEPSEEK_RELAY_MODEL',
|
|
868
|
+
'DEEPSEEK_BASE_URL',
|
|
869
|
+
'DEEPSEEK_CLAUDE_CONFIG_DIR',
|
|
870
|
+
'GROK_RELAY_MODEL',
|
|
871
|
+
'GROK_CLI_COMMAND',
|
|
872
|
+
'GROK_ALWAYS_APPROVE',
|
|
873
|
+
// The OAR MCP server kill switch (the Grok worker reads it too), and the
|
|
874
|
+
// extension engine's "it is attached" marker.
|
|
875
|
+
'OAR_MCP_SERVER',
|
|
876
|
+
'OAR_MCP_SERVER_ATTACHED',
|
|
877
|
+
'SESSION_ID',
|
|
878
|
+
'COPILOT_WORKSPACE_ROOT',
|
|
879
|
+
'INIT_CWD',
|
|
880
|
+
]);
|
|
881
|
+
|
|
700
882
|
export function buildTmuxWorkerShellCommand(targetSessionId, env = {}, {
|
|
701
883
|
secretEnvFilePath = '',
|
|
702
884
|
workerLogPath = '',
|
|
@@ -715,80 +897,41 @@ export function buildTmuxWorkerShellCommand(targetSessionId, env = {}, {
|
|
|
715
897
|
throw new Error('worker-secret-env-file-required');
|
|
716
898
|
}
|
|
717
899
|
const exports = [];
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
'GITHUB_COPILOT_PROMPT_MODE_EXTENSIONS',
|
|
721
|
-
'COPILOT_WEB_RELAY_ROOT',
|
|
722
|
-
'COPILOT_WEB_RELAY_SERVER_DIR',
|
|
723
|
-
'COPILOT_WEB_RELAY_CONFIG',
|
|
724
|
-
'COPILOT_WEB_RELAY_TOOLS',
|
|
725
|
-
'COPILOT_WEB_RELAY_LOG_DIR',
|
|
726
|
-
// The Cursor worker keeps its agent stores in the relay's data dir
|
|
727
|
-
// (applyCursorProviderEnvironment).
|
|
728
|
-
'COPILOT_WEB_RELAY_DATA_DIR',
|
|
729
|
-
'COPILOT_WEB_RELAY_CLI_EXECUTABLE',
|
|
730
|
-
'COPILOT_WEB_RELAY_EXTENSION_BOOTSTRAP_PATH',
|
|
731
|
-
'COPILOT_SDK_PATH',
|
|
732
|
-
'EXTENSION_PATH',
|
|
733
|
-
'COPILOT_PROVIDER_TYPE',
|
|
734
|
-
'COPILOT_PROVIDER_BASE_URL',
|
|
735
|
-
'COPILOT_PROVIDER_WIRE_API',
|
|
736
|
-
'COPILOT_MODEL',
|
|
737
|
-
'COPILOT_WEB_RELAY_WORKER_KIND',
|
|
738
|
-
// The SDK worker's own model and BYOK context overrides. `COPILOT_MODEL`
|
|
739
|
-
// above is the extension engine's model variable and is read by the CLI's
|
|
740
|
-
// startup layer; the SDK worker takes its default model from
|
|
741
|
-
// `COPILOT_RELAY_MODEL`, matching every other Node worker's
|
|
742
|
-
// `*_RELAY_MODEL`. Without it here the model would silently drop to the
|
|
743
|
-
// runtime default on the tmux path while working on the detached path,
|
|
744
|
-
// which inherits the whole environment.
|
|
745
|
-
'COPILOT_RELAY_MODEL',
|
|
746
|
-
'COPILOT_PROVIDER_MAX_PROMPT_TOKENS',
|
|
747
|
-
'COPILOT_PROVIDER_MAX_OUTPUT_TOKENS',
|
|
748
|
-
// Without this the tmux path would fall back to ~/.claude while the relay
|
|
749
|
-
// (and the detached spawn path, which inherits the full environment) uses
|
|
750
|
-
// the override — worker and relay would read different config roots.
|
|
751
|
-
'CLAUDE_CONFIG_DIR',
|
|
752
|
-
// Same divergence, one variable over: the relay binds a resolved `claude`
|
|
753
|
-
// binary onto its own environment (cli-install-service.mjs §4.3), and the
|
|
754
|
-
// detached path inherits it for free. Without it here a bound binary would
|
|
755
|
-
// be silently dropped on the tmux path and the worker would fall back to
|
|
756
|
-
// whatever `claude` PATH happens to resolve — exactly the failure mode
|
|
757
|
-
// CLAUDE_CONFIG_DIR above was added for. GROK_CLI_COMMAND, its Grok
|
|
758
|
-
// counterpart, has always been on this list.
|
|
759
|
-
'CLAUDE_CODE_EXECUTABLE',
|
|
760
|
-
'CLAUDE_RELAY_MODEL',
|
|
761
|
-
// The Claude Cloud worker's model. Its login is not listed here: the
|
|
762
|
-
// worker reads the CLI's credentials file (CLAUDE_CONFIG_DIR above tells
|
|
763
|
-
// it where), and a relay-level CLAUDE_CODE_OAUTH_TOKEN is a secret
|
|
764
|
-
// (workerSecretEnvVarsFor), so it comes through the secret file.
|
|
765
|
-
'CLAUDE_CLOUD_RELAY_MODEL',
|
|
766
|
-
// Tests point the relay's cloud client at a fake API on this machine;
|
|
767
|
-
// the worker's client has to follow, or the relay would talk to the fake
|
|
768
|
-
// and its worker to the real API. Not a secret, and harmless as a value:
|
|
769
|
-
// shared/claude-cloud/base-url.mjs ignores everything but a loopback URL.
|
|
770
|
-
'OAR_CLAUDE_CLOUD_API_BASE_URL',
|
|
771
|
-
'CURSOR_RELAY_MODEL',
|
|
772
|
-
'CURSOR_AGENT_STORE_DIR',
|
|
773
|
-
'GROK_RELAY_MODEL',
|
|
774
|
-
'GROK_CLI_COMMAND',
|
|
775
|
-
'GROK_ALWAYS_APPROVE',
|
|
776
|
-
// The OAR MCP server kill switch (the Grok worker reads it too), and the
|
|
777
|
-
// extension engine's "it is attached" marker.
|
|
778
|
-
'OAR_MCP_SERVER',
|
|
779
|
-
'OAR_MCP_SERVER_ATTACHED',
|
|
780
|
-
'SESSION_ID',
|
|
781
|
-
'COPILOT_WORKSPACE_ROOT',
|
|
782
|
-
'INIT_CWD',
|
|
783
|
-
]) {
|
|
900
|
+
const exported = new Set();
|
|
901
|
+
for (const key of TMUX_WORKER_EXPORTED_ENV_VARS) {
|
|
784
902
|
const value = String(launchEnv?.[key] || '').trim();
|
|
785
903
|
if (!value) continue;
|
|
786
904
|
exports.push(`${key}=${shellQuote(value)}`);
|
|
905
|
+
exported.add(key);
|
|
787
906
|
}
|
|
907
|
+
// A new tmux session starts from the server's global environment, which is
|
|
908
|
+
// the environment the server was started with: tmux takes the environment
|
|
909
|
+
// of `new-session` only when it starts the server. So the cleaning of that
|
|
910
|
+
// environment (launchSessionCli) reaches a worker only on the first launch;
|
|
911
|
+
// a server that was started from inside a Claude Code session, or by an
|
|
912
|
+
// older relay, hands every later worker the host session's variables and
|
|
913
|
+
// the first launch's values. The command therefore unsets, before anything
|
|
914
|
+
// else, the host session's variables and every per-launch variable this
|
|
915
|
+
// launch exports no value for (unset of an absent name is a no-op in sh).
|
|
916
|
+
// With them the worker log file a relay started from inside a worker
|
|
917
|
+
// session inherited, and every secret this launch carries no value for —
|
|
918
|
+
// other kinds' and its own alike — which a tmux server started from a shell
|
|
919
|
+
// that exports them keeps: names only. The worker's own secrets come
|
|
920
|
+
// through the secret file alone, sourced after the unset, so a key the
|
|
921
|
+
// launch does carry is restored from there. A second statement then unsets
|
|
922
|
+
// every variable of the host session's families the shell holds, names a
|
|
923
|
+
// fixed list would miss included.
|
|
924
|
+
const ownSecrets = new Set(workerSecretEnvVarsFor(launchEnv));
|
|
925
|
+
const unset = [
|
|
926
|
+
...HOST_CLAUDE_SESSION_ENV_VARS,
|
|
927
|
+
WORKER_LOG_FILE_ENV,
|
|
928
|
+
...TMUX_WORKER_EXPORTED_ENV_VARS.filter((key) => !exported.has(key)),
|
|
929
|
+
...WORKER_SECRET_ENV_VARS.filter((key) => !ownSecrets.has(key) || !String(launchEnv?.[key] ?? '').trim()),
|
|
930
|
+
];
|
|
788
931
|
// `sh -lc` builds PATH from the login profile, not from the relay's; the
|
|
789
932
|
// relay's Node folder goes at its end when it is not on it (see
|
|
790
933
|
// appendNodeDirectoryToPath).
|
|
791
|
-
const statements = exports.map((entry) => `export ${entry};`);
|
|
934
|
+
const statements = [`unset ${unset.join(' ')};`, hostClaudeSessionUnsetStatement(), ...exports.map((entry) => `export ${entry};`)];
|
|
792
935
|
const nodeDirectory = resolveWorkerNodeDirectory(launchEnv);
|
|
793
936
|
if (nodeDirectory) {
|
|
794
937
|
const quotedDir = shellQuote(nodeDirectory);
|
|
@@ -971,10 +1114,13 @@ export async function launchSessionCli({
|
|
|
971
1114
|
workspaceRoot: launchWorkspaceRoot,
|
|
972
1115
|
env,
|
|
973
1116
|
});
|
|
974
|
-
|
|
1117
|
+
// Every path below starts from this environment, so a secret that is not
|
|
1118
|
+
// this kind's, and the host Claude Code session's variables, are gone
|
|
1119
|
+
// before any of them sees it.
|
|
1120
|
+
const launchSessionEnv = withoutHostClaudeSessionVariables(withoutForeignWorkerSecrets({
|
|
975
1121
|
...appendNodeDirectoryToPath(launchEnv, resolveWorkerNodeDirectory(launchEnv, { platform }), { platform }),
|
|
976
1122
|
SESSION_ID: target,
|
|
977
|
-
};
|
|
1123
|
+
}));
|
|
978
1124
|
const explain = (message) => {
|
|
979
1125
|
try { log?.(`worker launcher: session=${target.slice(0, 8)} ${message}`); } catch {}
|
|
980
1126
|
};
|
|
@@ -1007,9 +1153,14 @@ export async function launchSessionCli({
|
|
|
1007
1153
|
const tmuxEnv = { ...launchSessionEnv };
|
|
1008
1154
|
delete tmuxEnv.TMUX;
|
|
1009
1155
|
delete tmuxEnv.TMUX_PANE;
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1012
|
-
|
|
1156
|
+
// No secret is in the environment tmux is started with (a tmux server
|
|
1157
|
+
// started by this call would keep it as its global one): this kind's go
|
|
1158
|
+
// through the secret file, the others are already gone. Nor is anything
|
|
1159
|
+
// of this launch alone (worker kind, model, folder): the shell command
|
|
1160
|
+
// exports those per session.
|
|
1161
|
+
for (const key of WORKER_SECRET_ENV_VARS) delete tmuxEnv[key];
|
|
1162
|
+
for (const key of TMUX_WORKER_EXPORTED_ENV_VARS) delete tmuxEnv[key];
|
|
1163
|
+
delete tmuxEnv.PWD;
|
|
1013
1164
|
if (allowProcessReuse) {
|
|
1014
1165
|
const existingPanePid = getTmuxPanePid(sessionName, { execFileSyncImpl });
|
|
1015
1166
|
if (existingPanePid && isPidAlive(existingPanePid)) {
|