@bitkyc08/opencodex 2.83.0 → 2.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/App-CZtqiGcw.js +56 -0
- package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
- package/gui/dist/assets/{quota-summary-DiJO6sYY.js → quota-summary-BbEeRfPY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/ollama-native.ts +29 -1
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +3 -0
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +16 -14
- package/src/bridge/errors.ts +19 -0
- package/src/bridge.ts +1 -1
- package/src/claude/context-windows.ts +54 -1
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +26 -2
- package/src/cli/account.ts +41 -5
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-base.ts +13 -1
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/claude.ts +15 -6
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +16 -3
- package/src/cli/export-command.ts +14 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +13 -4
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +5 -4
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export.ts +46 -16
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/autostart-health.ts +3 -1
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/effort.ts +2 -2
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/native-models.ts +16 -2
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-overlay.ts +8 -4
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +16 -0
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +43 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +2 -1
- package/src/config/paths.ts +3 -0
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +2 -0
- package/src/generated/compatibility-version.json +311 -171
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/reset-coupon-ledger.ts +75 -5
- package/src/grok/reset-coupons.ts +4 -2
- package/src/integrations/mutation-plan.ts +13 -7
- package/src/integrations/registry.ts +45 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +14 -6
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/shadow-call.ts +44 -5
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +24 -15
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro.ts +48 -5
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +11 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +14 -14
- package/src/providers/registry/entries-extended.ts +0 -1
- package/src/providers/registry/model-seeds.ts +17 -0
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +7 -1
- package/src/server/auth-cors.ts +42 -0
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +13 -0
- package/src/server/lifecycle.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +28 -10
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/grok-coupon-routes.ts +78 -27
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +82 -402
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +5 -3
- package/src/server/management/shared.ts +3 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native.ts +68 -42
- package/src/server/relay-eager.ts +2 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +63 -6
- package/src/server/responses/adapter-dispatch.ts +232 -79
- package/src/server/responses/code-mode-collab-surface.ts +205 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +126 -48
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +14 -0
- package/src/server/responses/core-combo.ts +7 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/passthrough-delivery.ts +4 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -4
- package/src/server/responses/request-prepare.ts +6 -4
- package/src/server/responses/request-transport.ts +6 -8
- package/src/server/responses/sidecar-execution.ts +11 -2
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/startup-action-control.ts +22 -2
- package/src/server/startup-health-cache.ts +4 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +147 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +447 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +54 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/types/config.ts +5 -0
- package/src/usage/expected-prices.ts +4 -0
- package/src/usage/log.ts +19 -1
- package/gui/dist/assets/App-Ba8uG696.js +0 -54
- package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
- package/gui/dist/assets/index-BHRZdJuv.js +0 -94
|
@@ -169,12 +169,16 @@ export const APP_OWNED_RETAINED_STORE_REGISTRATIONS = [
|
|
|
169
169
|
category: "caches",
|
|
170
170
|
snapshot: usageSummaryRetainedStoreSnapshot,
|
|
171
171
|
evictOldest: evictOldestUsageSummaryForBudget,
|
|
172
|
+
// Deletes one in-memory summary entry.
|
|
173
|
+
passiveEvictable: true,
|
|
172
174
|
},
|
|
173
175
|
{
|
|
174
176
|
id: "usage_snapshot",
|
|
175
177
|
category: "caches",
|
|
176
178
|
snapshot: usageSnapshotRetainedStoreSnapshot,
|
|
177
179
|
evictOldest: evictOldestUsageSnapshot,
|
|
180
|
+
// Drops the retained ledger snapshot or one unpinned aggregate, both in memory.
|
|
181
|
+
passiveEvictable: true,
|
|
178
182
|
},
|
|
179
183
|
{
|
|
180
184
|
id: "cursor_blobs",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isPassiveRead } from "./passive-read";
|
|
2
|
+
|
|
1
3
|
export type AppOwnedRetainedCategory = "logs" | "caches" | "blobs" | "continuation";
|
|
2
4
|
export type AppOwnedObservedCategory = "translator" | "serialized_tails";
|
|
3
5
|
|
|
@@ -14,6 +16,11 @@ export interface RetainedStoreRegistration {
|
|
|
14
16
|
category: AppOwnedRetainedCategory;
|
|
15
17
|
snapshot(): RetainedStoreSnapshot;
|
|
16
18
|
evictOldest(): number;
|
|
19
|
+
/**
|
|
20
|
+
* Eviction only releases memory: no spill, snapshot or other write. Only such stores are
|
|
21
|
+
* demoted inside a passive read (#6959); the rest wait for this process's own next enforcement.
|
|
22
|
+
*/
|
|
23
|
+
passiveEvictable?: true;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
export interface ObservedBufferRegistration {
|
|
@@ -168,11 +175,15 @@ function nextCandidate(
|
|
|
168
175
|
snapshots: ReadonlyMap<string, RetainedStoreSnapshot>,
|
|
169
176
|
ineligible: Set<string>,
|
|
170
177
|
): RetainedStoreRegistration | undefined {
|
|
178
|
+
// A passive read (a Remote Link Child's, #6959) demotes only stores whose eviction is memory-only:
|
|
179
|
+
// a continuation spills to disk and a replay cache rewrites its snapshot.
|
|
180
|
+
const passive = isPassiveRead();
|
|
171
181
|
for (const category of CATEGORY_ORDER) {
|
|
172
182
|
let candidate: RetainedStoreRegistration | undefined;
|
|
173
183
|
let candidateAt = Number.POSITIVE_INFINITY;
|
|
174
184
|
for (const registration of retainedStores.values()) {
|
|
175
185
|
if (registration.category !== category || ineligible.has(registration.id)) continue;
|
|
186
|
+
if (passive && registration.passiveEvictable !== true) continue;
|
|
176
187
|
const snapshot = snapshots.get(registration.id);
|
|
177
188
|
if (!snapshot || snapshot.evictableBytes <= 0) continue;
|
|
178
189
|
const oldestAt = snapshot.oldestAt;
|
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import { TRANSLATOR_MAX_TURN_BYTES, TranslatorBudgetExceededError } from "./translator-budget";
|
|
2
2
|
|
|
3
|
+
/** Count one JSON string formed from parts without joining, serializing or encoding it. */
|
|
4
|
+
export function jsonStringPartsUtf8Bytes(parts: readonly string[], limit = TRANSLATOR_MAX_TURN_BYTES): number {
|
|
5
|
+
let bytes = 0;
|
|
6
|
+
let highSurrogate = false;
|
|
7
|
+
const add = (count: number) => {
|
|
8
|
+
if (count > limit - bytes) throw new TranslatorBudgetExceededError("request_copies", limit);
|
|
9
|
+
bytes += count;
|
|
10
|
+
};
|
|
11
|
+
add(2); // The joined string has one pair of JSON quotes.
|
|
12
|
+
for (const text of parts) {
|
|
13
|
+
if (text.length > limit - bytes) throw new TranslatorBudgetExceededError("request_copies", limit);
|
|
14
|
+
for (let i = 0; i < text.length; i++) {
|
|
15
|
+
const code = text.charCodeAt(i);
|
|
16
|
+
if (highSurrogate) {
|
|
17
|
+
highSurrogate = false;
|
|
18
|
+
if (code >= 0xdc00 && code <= 0xdfff) { add(4); continue; }
|
|
19
|
+
add(6);
|
|
20
|
+
}
|
|
21
|
+
if (code >= 0xd800 && code <= 0xdbff) highSurrogate = true;
|
|
22
|
+
else if (code === 0x22 || code === 0x5c || code === 8 || code === 9 || code === 10 || code === 12 || code === 13) add(2);
|
|
23
|
+
else if (code < 0x20) add(6);
|
|
24
|
+
else if (code < 0x80) add(1);
|
|
25
|
+
else if (code < 0x800) add(2);
|
|
26
|
+
else if (code >= 0xdc00 && code <= 0xdfff) add(6);
|
|
27
|
+
else add(3);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (highSurrogate) add(6);
|
|
31
|
+
return bytes;
|
|
32
|
+
}
|
|
33
|
+
|
|
3
34
|
/** Measure plain JSON data without allocating its serialized string or UTF-8 copy. */
|
|
4
35
|
export function jsonUtf8Bytes(value: unknown, limit = TRANSLATOR_MAX_TURN_BYTES): number {
|
|
5
36
|
let bytes = 0;
|
|
@@ -70,5 +70,40 @@ export function runOptionalShutdownHooks(): void {
|
|
|
70
70
|
/** Test-only reset so an isolated lifecycle test does not inherit registrations. */
|
|
71
71
|
export function resetOptionalShutdownHooksForTests(): void {
|
|
72
72
|
hooks.clear();
|
|
73
|
+
drainStartHooks.clear();
|
|
74
|
+
drainStartHooksRan = false;
|
|
73
75
|
hooksRan = false;
|
|
74
76
|
}
|
|
77
|
+
|
|
78
|
+
const drainStartHooks = new Map<string, ShutdownHook>();
|
|
79
|
+
let drainStartHooksRan = false;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Register work that must run the moment a drain begins, before in-flight turns are awaited.
|
|
83
|
+
* The service health sentinel (#6671) uses it: a deliberate stop whose drain stalls must stay a
|
|
84
|
+
* stop, so the sentinel is withdrawn before the drain can look unresponsive. Same contract as
|
|
85
|
+
* {@link registerOptionalShutdownHook}: keyed, synchronous, best-effort, detachable.
|
|
86
|
+
*/
|
|
87
|
+
export function registerDrainStartHook(key: string, hook: ShutdownHook): () => void {
|
|
88
|
+
drainStartHooks.set(key, hook);
|
|
89
|
+
return () => {
|
|
90
|
+
if (drainStartHooks.get(key) === hook) drainStartHooks.delete(key);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Run every drain-start hook. Never throws. */
|
|
95
|
+
export function runDrainStartHooks(): void {
|
|
96
|
+
drainStartHooksRan = true;
|
|
97
|
+
for (const [key, hook] of [...drainStartHooks]) {
|
|
98
|
+
try {
|
|
99
|
+
hook();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.warn(`[shutdown] drain-start hook "${key}" failed:`, err instanceof Error ? err.message : err);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Whether a drain has begun in this process; work that must not start mid-drain checks this. */
|
|
107
|
+
export function didRunDrainStartHooks(): boolean {
|
|
108
|
+
return drainStartHooksRan;
|
|
109
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local record of credential values this process has handed to an upstream.
|
|
3
|
+
*
|
|
4
|
+
* Upstream diagnostics (error codes, request ids) are provider-controlled. A gateway can echo a
|
|
5
|
+
* credential it received back into those fields, and a credential with no recognizable format slips
|
|
6
|
+
* past pattern redaction. Configured credentials are read directly at capture time (see
|
|
7
|
+
* server/configured-credentials); this registry adds the values that only exist at send time, such
|
|
8
|
+
* as a client-forwarded bearer or a plugin-added header.
|
|
9
|
+
*
|
|
10
|
+
* Two tiers keep ordinary traffic from disabling diagnostics. Values of credential-named headers are
|
|
11
|
+
* strict: they match at any length, and once one is evicted coverage is incomplete for the rest of
|
|
12
|
+
* the process, so callers fail closed. Every other header value is best effort: it matches only at
|
|
13
|
+
* eight characters or more and its eviction never disables diagnostics, so per-turn metadata cannot
|
|
14
|
+
* exhaust anything that matters. Correlation ids (request ids, trace context) are left out of the
|
|
15
|
+
* best-effort tier because a gateway legitimately echoes them as its request id; a configured header
|
|
16
|
+
* of that name is still covered by the configured set. Values are held in memory only: nothing here
|
|
17
|
+
* is serialized, persisted or exposed.
|
|
18
|
+
*/
|
|
19
|
+
import { SENSITIVE_KEY_PATTERN } from "./redact";
|
|
20
|
+
|
|
21
|
+
/** Containment only for credentials long enough that a diagnostic cannot contain one by chance. */
|
|
22
|
+
export const CREDENTIAL_SUBSTRING_MIN_LENGTH = 8;
|
|
23
|
+
const CREDENTIAL_HEADER_NAME = /api[-_]?key|secret|passw|credential|auth|(?:^|[-_])token$/i;
|
|
24
|
+
const CORRELATION_HEADER_NAME = /^(?:x-(?:client-)?request-id|request-id|traceparent|tracestate)$/i;
|
|
25
|
+
const AUTH_SCHEMES = new Set(["bearer", "basic", "token", "digest", "apikey", "api-key", "key", "sso-key", "negotiate", "dpop"]);
|
|
26
|
+
const TOKEN_CHAR = /[!#$%&'*+.^_`|~0-9A-Za-z-]/;
|
|
27
|
+
const BASIC_BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
28
|
+
const PRINTABLE_USER_PASS = /^[\x20-\x7e]*:[\x20-\x7e]*$/;
|
|
29
|
+
|
|
30
|
+
/** True for a header (or config field) name whose value is a credential by its name. */
|
|
31
|
+
export function isCredentialName(name: string): boolean {
|
|
32
|
+
return SENSITIVE_KEY_PATTERN.test(name) || CREDENTIAL_HEADER_NAME.test(name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Values of RFC 9110 auth-params (token BWS "=" BWS ( token / quoted-string )), cookie pairs and
|
|
37
|
+
* other name=value pairs, in one linear pass: every character is visited a bounded number of times,
|
|
38
|
+
* so a long header value cannot stall the event loop.
|
|
39
|
+
*/
|
|
40
|
+
function parameterValues(value: string): string[] {
|
|
41
|
+
const out: string[] = [];
|
|
42
|
+
const n = value.length;
|
|
43
|
+
let unclosedFrom = n; // no closing quote exists for any opening quote at or after this index
|
|
44
|
+
let i = 0;
|
|
45
|
+
while (i < n) {
|
|
46
|
+
if (!TOKEN_CHAR.test(value[i]!)) { i++; continue; }
|
|
47
|
+
while (i < n && TOKEN_CHAR.test(value[i]!)) i++;
|
|
48
|
+
let j = i;
|
|
49
|
+
while (j < n && (value[j] === " " || value[j] === "\t")) j++;
|
|
50
|
+
if (value[j] !== "=") { i = j; continue; }
|
|
51
|
+
j++;
|
|
52
|
+
while (j < n && (value[j] === " " || value[j] === "\t")) j++;
|
|
53
|
+
if (value[j] === "\"") {
|
|
54
|
+
let k = j + 1;
|
|
55
|
+
let text = "";
|
|
56
|
+
let closed = false;
|
|
57
|
+
if (j < unclosedFrom) {
|
|
58
|
+
while (k < n) {
|
|
59
|
+
const char = value[k]!;
|
|
60
|
+
if (char === "\\" && k + 1 < n) { text += value[k + 1]; k += 2; continue; }
|
|
61
|
+
if (char === "\"") { closed = true; break; }
|
|
62
|
+
text += char;
|
|
63
|
+
k++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (closed) {
|
|
67
|
+
if (text) out.push(text);
|
|
68
|
+
i = k + 1;
|
|
69
|
+
} else {
|
|
70
|
+
unclosedFrom = Math.min(unclosedFrom, j);
|
|
71
|
+
i = j + 1;
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const start = j;
|
|
76
|
+
while (j < n && !/[\s,;"]/.test(value[j]!)) j++;
|
|
77
|
+
const raw = value.slice(start, j);
|
|
78
|
+
if (raw && !/^=+$/.test(raw)) out.push(raw);
|
|
79
|
+
i = j;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The whole value plus every part a server could authenticate with on its own: tokens split on
|
|
86
|
+
* whitespace, commas and semicolons with auth scheme words dropped, the value of each name=value
|
|
87
|
+
* parameter (cookies, digest; whitespace around "=" and quoted strings with escapes are parsed
|
|
88
|
+
* first, before any splitting), and both halves of a standard Basic credential.
|
|
89
|
+
*/
|
|
90
|
+
export function credentialComponents(value: string): string[] {
|
|
91
|
+
const whole = value.trim();
|
|
92
|
+
if (!whole) return [];
|
|
93
|
+
const out = new Set([whole]);
|
|
94
|
+
for (const param of parameterValues(whole)) out.add(param);
|
|
95
|
+
let previous: string | undefined;
|
|
96
|
+
for (const part of whole.split(/[\s,;]+/)) {
|
|
97
|
+
if (!part) continue;
|
|
98
|
+
const lower = part.toLowerCase();
|
|
99
|
+
if (AUTH_SCHEMES.has(lower)) { previous = lower; continue; }
|
|
100
|
+
out.add(part);
|
|
101
|
+
if (previous === "basic" && BASIC_BASE64.test(part)) {
|
|
102
|
+
const decoded = Buffer.from(part, "base64").toString("utf8");
|
|
103
|
+
if (PRINTABLE_USER_PASS.test(decoded) && Buffer.from(decoded, "utf8").toString("base64").replace(/=+$/, "") === part.replace(/=+$/, "")) {
|
|
104
|
+
const colon = decoded.indexOf(":");
|
|
105
|
+
out.add(decoded.slice(0, colon));
|
|
106
|
+
out.add(decoded.slice(colon + 1));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
previous = undefined;
|
|
110
|
+
}
|
|
111
|
+
out.delete("");
|
|
112
|
+
return [...out];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Exact equality at any length at or above minLength; containment for values of 8+ characters. */
|
|
116
|
+
export function credentialSetMatches(values: Iterable<string>, has: (value: string) => boolean, value: string, minLength = 1): boolean {
|
|
117
|
+
if (value.length >= minLength && has(value)) return true;
|
|
118
|
+
for (const secret of values) {
|
|
119
|
+
if (secret.length >= Math.max(minLength, CREDENTIAL_SUBSTRING_MIN_LENGTH) && value.includes(secret)) return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface OutboundCredentialRegistryOptions {
|
|
125
|
+
maxEntries?: number;
|
|
126
|
+
maxBytes?: number;
|
|
127
|
+
maxValueBytes?: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Bounded LRU of values; eviction marks it incomplete. */
|
|
131
|
+
class BoundedValueSet {
|
|
132
|
+
readonly values = new Map<string, number>();
|
|
133
|
+
private totalBytes = 0;
|
|
134
|
+
complete = true;
|
|
135
|
+
constructor(private readonly maxEntries: number, private readonly maxBytes: number, private readonly maxValueBytes: number) {}
|
|
136
|
+
|
|
137
|
+
add(value: string): void {
|
|
138
|
+
if (!value) return;
|
|
139
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
140
|
+
if (bytes > this.maxValueBytes) {
|
|
141
|
+
this.complete = false;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (this.values.has(value)) {
|
|
145
|
+
this.values.delete(value);
|
|
146
|
+
this.values.set(value, bytes);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.values.set(value, bytes);
|
|
150
|
+
this.totalBytes += bytes;
|
|
151
|
+
while (this.values.size > this.maxEntries || this.totalBytes > this.maxBytes) {
|
|
152
|
+
const oldest = this.values.keys().next().value as string;
|
|
153
|
+
this.totalBytes -= this.values.get(oldest)!;
|
|
154
|
+
this.values.delete(oldest);
|
|
155
|
+
this.complete = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
matches(value: string, minLength: number): boolean {
|
|
160
|
+
return credentialSetMatches(this.values.keys(), candidate => this.values.has(candidate), value, minLength);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export class OutboundCredentialRegistry {
|
|
165
|
+
private readonly strict: BoundedValueSet;
|
|
166
|
+
private readonly observed: BoundedValueSet;
|
|
167
|
+
|
|
168
|
+
constructor(options: OutboundCredentialRegistryOptions = {}) {
|
|
169
|
+
const entries = options.maxEntries ?? 4096;
|
|
170
|
+
const bytes = options.maxBytes ?? 1 << 20;
|
|
171
|
+
const valueBytes = options.maxValueBytes ?? 16_384;
|
|
172
|
+
this.strict = new BoundedValueSet(entries, bytes, valueBytes);
|
|
173
|
+
this.observed = new BoundedValueSet(entries, bytes, valueBytes);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Record the components of every value in headers that are about to be sent. */
|
|
177
|
+
remember(headers: HeadersInit | Headers | undefined): void {
|
|
178
|
+
if (!headers) return;
|
|
179
|
+
new Headers(headers).forEach((value, name) => {
|
|
180
|
+
const strict = isCredentialName(name);
|
|
181
|
+
if (!strict && CORRELATION_HEADER_NAME.test(name)) return;
|
|
182
|
+
const tier = strict ? this.strict : this.observed;
|
|
183
|
+
for (const part of credentialComponents(value)) tier.add(part);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** True when value repeats a recorded credential (strict at any length, best effort at 8+). */
|
|
188
|
+
matches(value: string): boolean {
|
|
189
|
+
return this.strict.matches(value, 1) || this.observed.matches(value, CREDENTIAL_SUBSTRING_MIN_LENGTH);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** False once a credential-named value was dropped; callers must then fail closed. */
|
|
193
|
+
complete(): boolean {
|
|
194
|
+
return this.strict.complete;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let active = new OutboundCredentialRegistry();
|
|
199
|
+
|
|
200
|
+
/** The registry fed by the HTTP executor boundary and the Codex WebSocket dial. */
|
|
201
|
+
export function outboundCredentials(): OutboundCredentialRegistry {
|
|
202
|
+
return active;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Swap the active registry for sequential in-file tests; returns the restore function. */
|
|
206
|
+
export function setOutboundCredentialRegistryForTests(registry: OutboundCredentialRegistry): () => void {
|
|
207
|
+
const previous = active;
|
|
208
|
+
active = registry;
|
|
209
|
+
return () => { active = previous; };
|
|
210
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A passive management read: it shows what this process already knows and does not make it act on
|
|
3
|
+
* providers, accounts, processes or files (#6959).
|
|
4
|
+
*
|
|
5
|
+
* The Remote Link management relay runs every Child read inside this scope
|
|
6
|
+
* (`src/server/index/link-management.ts`), so a Child can show its Home's state without changing
|
|
7
|
+
* it or making it act. Inside the scope:
|
|
8
|
+
*
|
|
9
|
+
* - model discovery yields no routed models (`fetchAllModels`, the management model rows): it would
|
|
10
|
+
* reach provider upstreams, refresh Codex entitlements and persist a pending provider's initial
|
|
11
|
+
* model selection;
|
|
12
|
+
* - provider quota and the Codex account list are served from what the Home has observed, with no
|
|
13
|
+
* probe, token refresh, native-main claim or publication into routing evidence;
|
|
14
|
+
* - the startup-health diagnostic, the Codex runtime version probes and the bundled-catalog refresh
|
|
15
|
+
* are not started, so no process runs and no runtime record is written;
|
|
16
|
+
* - a native-main fence raised for unknown service ownership is reported, not re-inspected;
|
|
17
|
+
* - config-directory and credential-file hardening and malformed-file backups are skipped;
|
|
18
|
+
* - memory-budget enforcement demotes only stores registered as `passiveEvictable` (eviction that
|
|
19
|
+
* only releases memory: the usage caches), never a continuation spill or a replay snapshot.
|
|
20
|
+
*
|
|
21
|
+
* The Home's own dashboard and traffic keep those caches current. A passive read may still join
|
|
22
|
+
* work the Home already started; that work keeps the Home's scope and its writes are the Home's.
|
|
23
|
+
*/
|
|
24
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
25
|
+
|
|
26
|
+
const passive = new AsyncLocalStorage<true>();
|
|
27
|
+
|
|
28
|
+
/** Run a management read passively, including everything it awaits or schedules. */
|
|
29
|
+
export function runPassiveRead<T>(run: () => Promise<T>): Promise<T> {
|
|
30
|
+
return passive.run(true, run);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isPassiveRead(): boolean {
|
|
34
|
+
return passive.getStore() === true;
|
|
35
|
+
}
|
package/src/lib/shadow-call.ts
CHANGED
|
@@ -81,19 +81,58 @@ export function shadowCallTargetsIntersect(
|
|
|
81
81
|
&& target.modelId.startsWith(source.modelId);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Preserve explicit user turns and spawned children before either intercept site.
|
|
86
|
+
* HTTP provenance is `x-codex-turn-metadata` or `x-openai-subagent: collab_spawn`.
|
|
87
|
+
* `x-openai-subagent` is connection-level on both transports: a child session opens
|
|
88
|
+
* its own connection, so a `collab_spawn` upgrade header still names every frame.
|
|
89
|
+
* Turn metadata is per-frame on WebSocket (`CODEX_WS_FRAME_HEADERS`): only the frame's
|
|
90
|
+
* `client_metadata["x-codex-turn-metadata"]` describes this turn, because the
|
|
91
|
+
* upgrade's copy can describe an earlier user or helper turn.
|
|
92
|
+
* `request_kind: "turn"` alone is not user provenance (#1684). Maintenance
|
|
93
|
+
* markers such as `x-openai-subagent: compact`/`review` and metadata naming
|
|
94
|
+
* `memory_consolidation` remain eligible helpers, subject to memory routing.
|
|
95
|
+
* Missing/malformed metadata retains the legacy opt-in source-prefix behavior.
|
|
96
|
+
*/
|
|
97
|
+
export function shadowCallPreservesSelectedModel(
|
|
98
|
+
body: unknown,
|
|
99
|
+
headers: Headers,
|
|
100
|
+
transport?: "websocket",
|
|
101
|
+
): boolean {
|
|
102
|
+
if (headers.get("x-openai-subagent") === "collab_spawn") return true;
|
|
103
|
+
let metadata: unknown = headers.get("x-codex-turn-metadata");
|
|
104
|
+
if (transport === "websocket") {
|
|
105
|
+
const client = body && typeof body === "object" && !Array.isArray(body)
|
|
106
|
+
? (body as Record<string, unknown>).client_metadata : undefined;
|
|
107
|
+
metadata = client && typeof client === "object" && !Array.isArray(client)
|
|
108
|
+
&& Object.hasOwn(client, "x-codex-turn-metadata")
|
|
109
|
+
? (client as Record<string, unknown>)["x-codex-turn-metadata"] : undefined;
|
|
110
|
+
}
|
|
111
|
+
if (typeof metadata !== "string") return false;
|
|
112
|
+
try {
|
|
113
|
+
const parsed: unknown = JSON.parse(metadata);
|
|
114
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
|
|
115
|
+
const turn = parsed as Record<string, unknown>;
|
|
116
|
+
return turn.subagent_kind === "thread_spawn"
|
|
117
|
+
|| (turn.request_kind === "turn" && turn.thread_source === "user");
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
84
123
|
/**
|
|
85
124
|
* Decide whether a matching source model should use the opt-in intercept.
|
|
86
125
|
*
|
|
87
126
|
* Before Codex 0.147.0 this checked x-codex-turn-metadata and exempted
|
|
88
127
|
* request_kind "turn". Codex 0.147.0 can label background helper calls as
|
|
89
|
-
* "turn", causing them to bypass the intercept (#1684).
|
|
90
|
-
*
|
|
128
|
+
* "turn", causing them to bypass the intercept (#1684). Request kind alone
|
|
129
|
+
* cannot exempt a configured source model: callers first preserve explicit
|
|
130
|
+
* user/child provenance with `shadowCallPreservesSelectedModel` (#6921).
|
|
91
131
|
* A replacement intersecting the same provider+model source set remains a
|
|
92
132
|
* no-op because rewriting it would only create self-interception (#2706).
|
|
93
133
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* sub-agent model, and an explicitly spawned child must keep the model it chose.
|
|
134
|
+
* `gpt-6-luna` is both the helper slug and a selectable user/sub-agent model;
|
|
135
|
+
* explicit user turns and spawned children must keep the model they chose.
|
|
97
136
|
*/
|
|
98
137
|
export function shouldInterceptShadowCall(
|
|
99
138
|
modelId: string,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Codex settings a Remote Link Home decides for its Children (#6957).
|
|
3
|
+
*
|
|
4
|
+
* A link Child relays every model request to its Home, so everything the Home's proxy applies
|
|
5
|
+
* per request already follows the Home: effort caps, the sub-agent roster and fallback, the
|
|
6
|
+
* injection prompt, sidecar and compaction routing, and the catalog itself (served by
|
|
7
|
+
* `/v1/catalog`). What does NOT follow is the part of the Home's OpenCodex config that the
|
|
8
|
+
* injector writes into the Child's own Codex `config.toml`. This module carries exactly that
|
|
9
|
+
* part, which is every OpenCodex config field the Codex injection plan reads that is a policy
|
|
10
|
+
* decision rather than a fact about the Child's own listener:
|
|
11
|
+
*
|
|
12
|
+
* - `fastMode` -> `[features] fast_mode`
|
|
13
|
+
* - `multiAgentMode` -> the native `features.multi_agent_v2` reconcile (v1 mode)
|
|
14
|
+
* - `webSearchSidecar.enabled` -> the root `web_search` switch
|
|
15
|
+
* - `syncCodexSubagentDefaults` + `injectionModel` + `injectionEffort` -> the managed
|
|
16
|
+
* `[agents]` defaults
|
|
17
|
+
*
|
|
18
|
+
* Root `model_reasoning_effort` and the `desktop.*` picker keys are Codex app settings that
|
|
19
|
+
* OpenCodex never writes on any machine, so they are deliberately not part of this document.
|
|
20
|
+
* Listener facts (port, hostname, Desktop authless and client compaction routing forms, resume
|
|
21
|
+
* history) stay the Child's own because they describe how the Child's Codex reaches the Child.
|
|
22
|
+
*
|
|
23
|
+
* It rides on its own authenticated link read rather than inside the catalog because the
|
|
24
|
+
* catalog bytes are written verbatim to `model_catalog_json`, which Codex parses itself.
|
|
25
|
+
*/
|
|
26
|
+
import { hostname } from "node:os";
|
|
27
|
+
import type { OcxConfig } from "../types";
|
|
28
|
+
|
|
29
|
+
export const HOME_CODEX_SETTINGS_VERSION = 1;
|
|
30
|
+
const MULTI_AGENT_MODES = ["v1", "default", "v2"] as const;
|
|
31
|
+
type MultiAgentMode = (typeof MULTI_AGENT_MODES)[number];
|
|
32
|
+
|
|
33
|
+
export interface HomeCodexSettings {
|
|
34
|
+
/** null: the Home leaves `fast_mode` alone. */
|
|
35
|
+
fastMode: boolean | null;
|
|
36
|
+
/** null: the Home has no explicit mode. */
|
|
37
|
+
multiAgentMode: MultiAgentMode | null;
|
|
38
|
+
webSearchSidecarEnabled: boolean;
|
|
39
|
+
subagentDefaults: { model: string; reasoningEffort: string | null } | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface HomeCodexSettingsDocument {
|
|
43
|
+
version: typeof HOME_CODEX_SETTINGS_VERSION;
|
|
44
|
+
home: { name: string; version: string };
|
|
45
|
+
settings: HomeCodexSettings;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const NAME_MAX = 255;
|
|
49
|
+
const VALUE_MAX = 256;
|
|
50
|
+
|
|
51
|
+
function safeLabel(value: string, max: number): string | null {
|
|
52
|
+
const trimmed = value.trim();
|
|
53
|
+
if (!trimmed || trimmed.length > max) return null;
|
|
54
|
+
for (const character of trimmed) {
|
|
55
|
+
const code = character.codePointAt(0) ?? 0;
|
|
56
|
+
if (code < 0x20 || code === 0x7f || (code >= 0x202a && code <= 0x202e) || (code >= 0x2066 && code <= 0x2069)) return null;
|
|
57
|
+
}
|
|
58
|
+
return trimmed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function projectHomeCodexSettings(config: OcxConfig, version: string): HomeCodexSettingsDocument {
|
|
62
|
+
const mode = config.multiAgentMode;
|
|
63
|
+
// Same predicate as `subagentDefaultSyncEffective` (src/config/diagnostics.ts), inlined so
|
|
64
|
+
// this module stays inside the src/link import boundary.
|
|
65
|
+
const sync = config.syncCodexSubagentDefaults === true && Boolean(config.injectionModel?.trim());
|
|
66
|
+
const effort = config.injectionEffort?.trim();
|
|
67
|
+
return {
|
|
68
|
+
version: HOME_CODEX_SETTINGS_VERSION,
|
|
69
|
+
home: { name: safeLabel(hostname(), NAME_MAX) ?? "Home", version },
|
|
70
|
+
settings: {
|
|
71
|
+
fastMode: typeof config.fastMode === "boolean" ? config.fastMode : null,
|
|
72
|
+
multiAgentMode: mode && (MULTI_AGENT_MODES as readonly string[]).includes(mode) ? mode : null,
|
|
73
|
+
webSearchSidecarEnabled: config.webSearchSidecar?.enabled !== false,
|
|
74
|
+
subagentDefaults: sync
|
|
75
|
+
? { model: config.injectionModel!.trim(), reasoningEffort: effort ? effort : null }
|
|
76
|
+
: null,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function exactKeys(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
|
|
82
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
83
|
+
const actual = Object.keys(value);
|
|
84
|
+
return actual.length === keys.length && actual.every(key => keys.includes(key));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Strict parse: an unknown field, a wrong type or an unsafe label rejects the whole document. */
|
|
88
|
+
export function parseHomeCodexSettingsDocument(value: unknown): HomeCodexSettingsDocument | null {
|
|
89
|
+
if (!exactKeys(value, ["version", "home", "settings"]) || value.version !== HOME_CODEX_SETTINGS_VERSION) return null;
|
|
90
|
+
const home = value.home;
|
|
91
|
+
if (!exactKeys(home, ["name", "version"]) || typeof home.name !== "string" || typeof home.version !== "string") return null;
|
|
92
|
+
const name = safeLabel(home.name, NAME_MAX);
|
|
93
|
+
const homeVersion = safeLabel(home.version, 64);
|
|
94
|
+
if (!name || !homeVersion) return null;
|
|
95
|
+
const s = value.settings;
|
|
96
|
+
if (!exactKeys(s, ["fastMode", "multiAgentMode", "webSearchSidecarEnabled", "subagentDefaults"])) return null;
|
|
97
|
+
if (s.fastMode !== null && typeof s.fastMode !== "boolean") return null;
|
|
98
|
+
if (s.multiAgentMode !== null && !(MULTI_AGENT_MODES as readonly unknown[]).includes(s.multiAgentMode)) return null;
|
|
99
|
+
if (typeof s.webSearchSidecarEnabled !== "boolean") return null;
|
|
100
|
+
let subagentDefaults: HomeCodexSettings["subagentDefaults"] = null;
|
|
101
|
+
if (s.subagentDefaults !== null) {
|
|
102
|
+
const d = s.subagentDefaults;
|
|
103
|
+
if (!exactKeys(d, ["model", "reasoningEffort"]) || typeof d.model !== "string") return null;
|
|
104
|
+
const model = safeLabel(d.model, VALUE_MAX);
|
|
105
|
+
if (!model) return null;
|
|
106
|
+
let reasoningEffort: string | null = null;
|
|
107
|
+
if (d.reasoningEffort !== null) {
|
|
108
|
+
if (typeof d.reasoningEffort !== "string" || !/^[a-z][a-z0-9_-]{0,31}$/.test(d.reasoningEffort)) return null;
|
|
109
|
+
reasoningEffort = d.reasoningEffort;
|
|
110
|
+
}
|
|
111
|
+
subagentDefaults = { model, reasoningEffort };
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
version: HOME_CODEX_SETTINGS_VERSION,
|
|
115
|
+
home: { name, version: homeVersion },
|
|
116
|
+
settings: {
|
|
117
|
+
fastMode: s.fastMode as boolean | null,
|
|
118
|
+
multiAgentMode: s.multiAgentMode as MultiAgentMode | null,
|
|
119
|
+
webSearchSidecarEnabled: s.webSearchSidecarEnabled,
|
|
120
|
+
subagentDefaults,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The Child's config as the Codex injector should see it: local listener facts, Home policy. */
|
|
126
|
+
export function overlayHomeCodexSettings(config: OcxConfig, settings: HomeCodexSettings): OcxConfig {
|
|
127
|
+
const next: OcxConfig = {
|
|
128
|
+
...config,
|
|
129
|
+
webSearchSidecar: { ...(config.webSearchSidecar ?? {}), enabled: settings.webSearchSidecarEnabled },
|
|
130
|
+
};
|
|
131
|
+
if (settings.fastMode === null) delete next.fastMode;
|
|
132
|
+
else next.fastMode = settings.fastMode;
|
|
133
|
+
if (settings.multiAgentMode === null) delete next.multiAgentMode;
|
|
134
|
+
else next.multiAgentMode = settings.multiAgentMode;
|
|
135
|
+
if (settings.subagentDefaults) {
|
|
136
|
+
next.injectionModel = settings.subagentDefaults.model;
|
|
137
|
+
next.syncCodexSubagentDefaults = true;
|
|
138
|
+
if (settings.subagentDefaults.reasoningEffort) next.injectionEffort = settings.subagentDefaults.reasoningEffort;
|
|
139
|
+
else delete next.injectionEffort;
|
|
140
|
+
} else {
|
|
141
|
+
next.syncCodexSubagentDefaults = false;
|
|
142
|
+
}
|
|
143
|
+
return next;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** One line per applied value, for `ocx sync`, `ocx status` and the dashboard. */
|
|
147
|
+
export function describeHomeCodexSettings(settings: HomeCodexSettings): string[] {
|
|
148
|
+
const d = settings.subagentDefaults;
|
|
149
|
+
return [
|
|
150
|
+
`fast mode: ${settings.fastMode === null ? "unset" : settings.fastMode ? "on" : "off"}`,
|
|
151
|
+
`multi-agent mode: ${settings.multiAgentMode ?? "default"}`,
|
|
152
|
+
`web search sidecar: ${settings.webSearchSidecarEnabled ? "on" : "off"}`,
|
|
153
|
+
`native sub-agent defaults: ${d ? `${d.model}${d.reasoningEffort ? ` (${d.reasoningEffort})` : ""}` : "not synced"}`,
|
|
154
|
+
];
|
|
155
|
+
}
|