@bitkyc08/opencodex 2.63.0-preview.20260923 → 2.64.0-preview.20260923
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/AGENTS_INSTALL.md +4 -3
- package/README.md +46 -33
- package/assets/download-linux.svg +10 -0
- package/assets/download-macos.svg +10 -0
- package/assets/download-windows.svg +10 -0
- package/bin/ocx.mjs +12 -4
- package/gui/dist/assets/App-D5eN1TID.js +50 -0
- package/gui/dist/assets/App-I5AnaSLh.css +1 -0
- package/gui/dist/assets/{Tray-B03pW-Uf.js → Tray-Bh_ErQDh.js} +1 -1
- package/gui/dist/assets/index-BEq4OCOz.js +86 -0
- package/gui/dist/assets/index-DiBRuK-d.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/src/protocol.rs +5 -0
- package/package.json +4 -1
- package/src/adapters/anthropic.ts +66 -7
- package/src/adapters/codebuddy/adapter.ts +112 -16
- package/src/adapters/codebuddy/mcp-server.ts +180 -0
- package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
- package/src/adapters/codebuddy/tool-bridge.ts +597 -0
- package/src/adapters/coding-agent/protocol.ts +123 -10
- package/src/adapters/coding-agent/turn.ts +324 -2
- package/src/adapters/command-code-restored-schema.ts +112 -0
- package/src/adapters/command-code-tool-text.ts +589 -0
- package/src/adapters/command-code.ts +80 -6
- package/src/adapters/cursor/catalog.ts +12 -0
- package/src/adapters/cursor/current-request.ts +46 -0
- package/src/adapters/cursor/discovery.ts +1 -1
- package/src/adapters/cursor/effort-map.ts +4 -0
- package/src/adapters/cursor/native-exec.ts +15 -0
- package/src/adapters/cursor/protobuf-events.ts +40 -12
- package/src/adapters/cursor/protobuf-request.ts +83 -18
- package/src/adapters/cursor/request-builder.ts +5 -4
- package/src/adapters/cursor/tool-guidance.ts +1 -1
- package/src/adapters/devin/live-models.ts +7 -0
- package/src/adapters/inline-think-tags.ts +251 -0
- package/src/adapters/kiro/adapter.ts +1 -0
- package/src/adapters/kiro/stream.ts +5 -3
- package/src/adapters/kiro/usage.ts +4 -3
- package/src/adapters/mimo-free.ts +37 -18
- package/src/adapters/openai-chat/messages.ts +5 -5
- package/src/adapters/openai-chat/tool-schema.ts +124 -2
- package/src/adapters/openai-chat.ts +13 -3
- package/src/adapters/openai-responses/passthrough.ts +15 -2
- package/src/adapters/openai-responses/reasoning.ts +14 -1
- package/src/adapters/openai-responses/request-strips.ts +29 -3
- package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
- package/src/adapters/qoder/adapter.ts +15 -12
- package/src/adapters/qoder/scaffold-guard.ts +45 -13
- package/src/bridge/internal.ts +20 -0
- package/src/bridge/sse.ts +14 -5
- package/src/claude/agents-inject.ts +2 -1
- package/src/claude/desktop-applied-marker.ts +44 -0
- package/src/claude/desktop-profile.ts +22 -12
- package/src/claude/inbound.ts +8 -3
- package/src/cli/access.ts +22 -5
- package/src/cli/account-api.ts +6 -0
- package/src/cli/account-auth.ts +5 -2
- package/src/cli/account.ts +5 -6
- package/src/cli/aside-profiles.ts +61 -1
- package/src/cli/claude-desktop.ts +3 -2
- package/src/cli/claude.ts +3 -1
- package/src/cli/codex-cli-update.ts +2 -2
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +12 -4
- package/src/cli/index.ts +98 -10
- package/src/cli/registry.ts +1 -1
- package/src/cli/resolve.ts +113 -1
- package/src/cli/root.ts +5 -0
- package/src/cli/stop-approval.ts +186 -0
- package/src/cli/stop-report.ts +1 -1
- package/src/cli/system-restart-client.ts +19 -4
- package/src/client/hub-client.ts +25 -18
- package/src/clients/config-export.ts +12 -4
- package/src/codex/account-auto-switch.ts +59 -0
- package/src/codex/account-lifecycle.ts +7 -0
- package/src/codex/account-priority.ts +10 -0
- package/src/codex/account-usability.ts +9 -0
- package/src/codex/auth-api/account-list.ts +5 -0
- package/src/codex/auth-api/login-flow.ts +15 -5
- package/src/codex/auth-api/login-state.ts +5 -16
- package/src/codex/auth-api/pool-mode-gate.ts +30 -7
- package/src/codex/auth-api/routes.ts +39 -3
- package/src/codex/auth-context.ts +145 -17
- package/src/codex/catalog/build-entries.ts +2 -1
- package/src/codex/catalog/effort.ts +8 -2
- package/src/codex/catalog/metadata.ts +33 -7
- package/src/codex/catalog/native-models.ts +102 -5
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +19 -6
- package/src/codex/cli-installation-targets.ts +44 -40
- package/src/codex/codex-write-lock.ts +32 -2
- package/src/codex/inject/multi-agent-v2.ts +90 -0
- package/src/codex/inject/plan.ts +365 -0
- package/src/codex/inject.ts +237 -362
- package/src/codex/log-guard/maintenance.ts +30 -13
- package/src/codex/project-config-warnings.ts +47 -7
- package/src/codex/prompt-layers/encoding.ts +41 -0
- package/src/codex/prompt-layers/toml-read.ts +1 -1
- package/src/codex/prompt-layers.ts +17 -13
- package/src/codex/prompt-text-probe.ts +20 -10
- package/src/codex/quota-observation-freshness.ts +34 -0
- package/src/codex/quota-rejection.ts +8 -4
- package/src/codex/quota.ts +4 -2
- package/src/codex/routing/selection.ts +32 -9
- package/src/codex/routing.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +4 -3
- package/src/codex/sync.ts +11 -0
- package/src/codex/windows-installation-files.ts +33 -11
- package/src/codex/write-coordination.ts +4 -0
- package/src/combos/request.ts +6 -2
- package/src/companion/settings.ts +6 -1
- package/src/config/atomic-write.ts +12 -1
- package/src/config/derived-registries.ts +29 -0
- package/src/config/diagnostics.ts +17 -0
- package/src/config/live-reconcile.ts +11 -2
- package/src/config/load-degrade.ts +9 -2
- package/src/config/persist-unlocked.ts +4 -3
- package/src/config/persisted-mutation.ts +94 -0
- package/src/config/provider-validation.ts +1 -1
- package/src/config/proxy-env.ts +62 -12
- package/src/config/rebase-provenance.ts +80 -1
- package/src/config/schema/config-schema.ts +5 -0
- package/src/config/schema/leaf-validators.ts +50 -1
- package/src/config/subagent-models.ts +41 -15
- package/src/config.ts +18 -95
- package/src/generated/compatibility-version.json +330 -218
- package/src/generated/model-metadata.ts +8 -5
- package/src/github/star-state.ts +46 -6
- package/src/grok/inject.ts +74 -40
- package/src/grok/status.ts +11 -4
- package/src/integrations/cursor-effort-table.ts +50 -13
- package/src/integrations/raycast-detect.ts +19 -4
- package/src/integrations/serialize.ts +11 -1
- package/src/lib/bounded-body.ts +30 -0
- package/src/lib/crash-guard.ts +52 -4
- package/src/lib/local-aside-sync-contract.ts +41 -0
- package/src/lib/package-tree-integrity.ts +181 -9
- package/src/lib/proxy-env.ts +18 -1
- package/src/lib/request-failure-attribution.ts +1 -0
- package/src/lib/request-failure-model.ts +3 -0
- package/src/lib/service-secrets.ts +99 -2
- package/src/lib/socks5-fetch.ts +43 -14
- package/src/lib/token-estimate.ts +17 -2
- package/src/oauth/command-code.ts +3 -2
- package/src/oauth/generic-account-failover.ts +29 -1
- package/src/oauth/index.ts +24 -16
- package/src/oauth/login-flow-state.ts +5 -5
- package/src/oauth/meta-muse-device.ts +49 -7
- package/src/oauth/store.ts +74 -9
- package/src/providers/alibaba-region-backup.ts +16 -1
- package/src/providers/anthropic-fast.ts +89 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
- package/src/providers/anthropic-reset-grants.ts +329 -0
- package/src/providers/claude-cli-identity.ts +12 -0
- package/src/providers/command-code-efforts.ts +184 -102
- package/src/providers/derive.ts +11 -3
- package/src/providers/fastwire.ts +16 -3
- package/src/providers/label.ts +8 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota/vendor-probes-oauth.ts +2 -1
- package/src/providers/reasoning-metadata.ts +38 -15
- package/src/providers/registry/entries-core.ts +95 -7
- package/src/providers/registry/entries-extended.ts +21 -10
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +31 -3
- package/src/providers/registry/types.ts +3 -1
- package/src/providers/resolved-model-policy-merge.ts +38 -8
- package/src/providers/resolved-model-policy.ts +4 -2
- package/src/providers/service-tier.ts +3 -1
- package/src/reasoning-effort.ts +6 -5
- package/src/responses/code-mode-helper-compat.ts +14 -4
- package/src/responses/code-mode-shell-input.ts +54 -0
- package/src/responses/custom-tool-compat.ts +173 -5
- package/src/responses/parser.ts +38 -2
- package/src/responses/reasoning-replay-cache.ts +26 -0
- package/src/router.ts +11 -1
- package/src/routing/history/indexer.ts +7 -2
- package/src/routing/history/schema.ts +3 -1
- package/src/server/adapter-resolve.ts +2 -2
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +14 -1
- package/src/server/chat-native.ts +17 -0
- package/src/server/claude-messages.ts +4 -3
- package/src/server/direct-local-http.ts +45 -19
- package/src/server/gui-session.ts +6 -15
- package/src/server/index/package-tree-guard.ts +53 -0
- package/src/server/index/serve-options.ts +17 -3
- package/src/server/index/startup-warnings.ts +17 -0
- package/src/server/index.ts +15 -17
- package/src/server/lifecycle.ts +8 -0
- package/src/server/live.ts +54 -4
- package/src/server/management/agent-settings-routes.ts +44 -8
- package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
- package/src/server/management/codex-prompt-routes.ts +5 -1
- package/src/server/management/config-routes.ts +14 -4
- package/src/server/management/oauth-account-routes.ts +17 -1
- package/src/server/management/provider-overwrite-carry.ts +164 -0
- package/src/server/management/provider-routes.ts +36 -6
- package/src/server/management/remote-workspace-routes.ts +2 -2
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/management/shadow-call-validation.ts +39 -0
- package/src/server/management/system-restart.ts +72 -6
- package/src/server/management-api.ts +16 -2
- package/src/server/management-auth.ts +52 -8
- package/src/server/proxy-liveness.ts +110 -4
- package/src/server/request-log.ts +26 -9
- package/src/server/request-metrics.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +6 -3
- package/src/server/responses/adapter-dispatch.ts +65 -1
- package/src/server/responses/compact.ts +31 -1
- package/src/server/responses/compaction-routing.ts +28 -3
- package/src/server/responses/core-codex-account.ts +85 -26
- package/src/server/responses/core-combo-failure.ts +16 -15
- package/src/server/responses/core-normalize.ts +5 -1
- package/src/server/responses/core-opaque-recovery.ts +50 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +7 -0
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/encrypted-payload.ts +23 -9
- package/src/server/responses/passthrough-delivery.ts +55 -35
- package/src/server/responses/passthrough-dispatch.ts +12 -13
- package/src/server/responses/policy-fallback.ts +12 -1
- package/src/server/responses/request-prepare.ts +46 -7
- package/src/server/responses/request-spend.ts +31 -15
- package/src/server/responses/run-turn-execution.ts +6 -5
- package/src/server/responses/shadow-target-availability.ts +61 -0
- package/src/server/responses-custom-tool-repair.ts +2 -0
- package/src/service/claim.ts +185 -0
- package/src/service/cli.ts +10 -1
- package/src/service/guarded-manager-target.ts +151 -0
- package/src/service/guards.ts +47 -48
- package/src/service/managing-cli.ts +170 -0
- package/src/service/orchestration.ts +3 -1
- package/src/service/state.ts +4 -0
- package/src/service/systemd.ts +16 -1
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -0
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +10 -0
- package/src/types/tools.ts +9 -1
- package/src/types/wire.ts +68 -4
- package/src/types.ts +1 -0
- package/src/update/job.ts +10 -16
- package/src/update/npm-invocation.mjs +17 -16
- package/src/update/transactional-install.d.mts +22 -1
- package/src/update/transactional-install.mjs +176 -19
- package/src/update/update-failure-guidance.d.mts +8 -0
- package/src/update/update-failure-guidance.mjs +47 -0
- package/src/usage/expected-prices.ts +54 -12
- package/src/usage/log.ts +117 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/timeline.ts +34 -6
- package/src/usage/user-cost-overlay-reconciler.ts +3 -3
- package/src/vision/reasoning.ts +2 -4
- package/src/web-search/xai-executor.ts +14 -4
- package/gui/dist/assets/App-EJxiUFMq.js +0 -50
- package/gui/dist/assets/index-BmJwNBHL.js +0 -86
- package/gui/dist/assets/index-DdDunwDb.css +0 -1
- package/gui/dist/assets/usage-companion-chart-IftE60UK.js +0 -1
- package/src/adapters/kiro-thinking.ts +0 -112
|
@@ -36,6 +36,7 @@ export interface ClaudeAgentDef {
|
|
|
36
36
|
const OWNED_PREFIX = "ocx-";
|
|
37
37
|
/** Ownership proof (audit 071 #2): a file without this marker is NEVER touched. */
|
|
38
38
|
const GENERATED_MARKER = "generated-by: opencodex";
|
|
39
|
+
const SAFE_AGENT_MODEL_ID = /^[a-z0-9][a-z0-9._:/@+\[\]~-]*$/i;
|
|
39
40
|
|
|
40
41
|
function sanitizeName(value: string): string {
|
|
41
42
|
const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -156,7 +157,7 @@ export function buildClaudeAgentDefs(
|
|
|
156
157
|
const roster = rosterOverride
|
|
157
158
|
?? (config.subagentModels === undefined ? DEFAULT_SUBAGENT_MODELS : config.subagentModels);
|
|
158
159
|
for (const entry of roster.slice(0, 5)) {
|
|
159
|
-
if (typeof entry !== "string" || entry.trim()
|
|
160
|
+
if (typeof entry !== "string" || !SAFE_AGENT_MODEL_ID.test(entry.trim())) continue;
|
|
160
161
|
const { alias, id, provider } = entryParts(entry.trim(), config);
|
|
161
162
|
push(sanitizeName(id), alias, `Delegate work to ${id} (${provider}) via opencodex routing. General-purpose worker/explorer on that model. ${NO_MODEL_ARG}`);
|
|
162
163
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { mutatePersistedConfig, type PersistedConfigMutationOutcome } from "../config";
|
|
2
|
+
import { emptyDesktopProfile, sameProfileContent, type DesktopProfile } from "./desktop-profile";
|
|
3
|
+
|
|
4
|
+
export interface DesktopAppliedMarkerBaseline {
|
|
5
|
+
profile: DesktopProfile | undefined;
|
|
6
|
+
appliedFingerprint: string | undefined;
|
|
7
|
+
appliedAt: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function captureDesktopAppliedMarker(
|
|
11
|
+
profile: DesktopProfile | undefined,
|
|
12
|
+
): DesktopAppliedMarkerBaseline {
|
|
13
|
+
const snapshot = profile == null ? undefined : structuredClone(profile);
|
|
14
|
+
return {
|
|
15
|
+
profile: snapshot,
|
|
16
|
+
appliedFingerprint: snapshot?.appliedFingerprint,
|
|
17
|
+
appliedAt: snapshot?.appliedAt,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function commitDesktopAppliedMarker(
|
|
22
|
+
baseline: DesktopAppliedMarkerBaseline,
|
|
23
|
+
fingerprint: string,
|
|
24
|
+
): PersistedConfigMutationOutcome<boolean> {
|
|
25
|
+
const appliedAt = new Date().toISOString();
|
|
26
|
+
return mutatePersistedConfig<boolean>(persisted => {
|
|
27
|
+
const profile = persisted.claudeCode?.desktopProfile;
|
|
28
|
+
if ((profile == null) !== (baseline.profile === undefined)
|
|
29
|
+
|| (profile && baseline.profile && !sameProfileContent(profile, baseline.profile))
|
|
30
|
+
|| profile?.appliedFingerprint !== baseline.appliedFingerprint
|
|
31
|
+
|| profile?.appliedAt !== baseline.appliedAt) {
|
|
32
|
+
return { changed: false, value: false };
|
|
33
|
+
}
|
|
34
|
+
persisted.claudeCode = {
|
|
35
|
+
...(persisted.claudeCode ?? {}),
|
|
36
|
+
desktopProfile: {
|
|
37
|
+
...(profile ?? emptyDesktopProfile()),
|
|
38
|
+
appliedFingerprint: fingerprint,
|
|
39
|
+
appliedAt,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
return { changed: true, value: true };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -77,15 +77,6 @@ function assertExactKeys(value: Record<string, unknown>, keys: readonly string[]
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
/**
|
|
81
|
-
* Applied-state markers survive every profile rebuild.
|
|
82
|
-
*
|
|
83
|
-
* `parseDesktopProfile`, `reconcileDesktopProfile` and `moveDesktopRoute` each construct a
|
|
84
|
-
* fresh `{ version, assignments, defaults }`, and the management routes persist whatever they
|
|
85
|
-
* return. Without this carry-through, saving an assignment — or merely dragging a model to
|
|
86
|
-
* another family — would erase the fingerprint the apply route wrote, and the GUI would report
|
|
87
|
-
* "not applied" for a config that is applied on disk.
|
|
88
|
-
*/
|
|
89
80
|
function appliedMarkers(source: { appliedFingerprint?: unknown; appliedAt?: unknown }): {
|
|
90
81
|
appliedFingerprint?: string;
|
|
91
82
|
appliedAt?: string;
|
|
@@ -96,6 +87,19 @@ function appliedMarkers(source: { appliedFingerprint?: unknown; appliedAt?: unkn
|
|
|
96
87
|
};
|
|
97
88
|
}
|
|
98
89
|
|
|
90
|
+
export function sameProfileContent(left: DesktopProfile, right: DesktopProfile): boolean {
|
|
91
|
+
return DESKTOP_FAMILIES.every(family => left.defaults[family] === right.defaults[family])
|
|
92
|
+
&& JSON.stringify(Object.entries(left.assignments).sort(([a], [b]) => a.localeCompare(b)))
|
|
93
|
+
=== JSON.stringify(Object.entries(right.assignments).sort(([a], [b]) => a.localeCompare(b)));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Retain applied-state bookkeeping only when the desired Desktop config is unchanged. */
|
|
97
|
+
export function preserveDesktopAppliedState(source: DesktopProfile, rebuilt: DesktopProfile): DesktopProfile {
|
|
98
|
+
return sameProfileContent(source, rebuilt)
|
|
99
|
+
? { ...rebuilt, ...appliedMarkers(source) }
|
|
100
|
+
: rebuilt;
|
|
101
|
+
}
|
|
102
|
+
|
|
99
103
|
function isFamily(value: unknown): value is DesktopFamily {
|
|
100
104
|
return typeof value === "string" && (DESKTOP_FAMILIES as readonly string[]).includes(value);
|
|
101
105
|
}
|
|
@@ -244,7 +248,8 @@ export function reconcileDesktopProfile(
|
|
|
244
248
|
const current = defaults[family];
|
|
245
249
|
defaults[family] = current && assignments[current]?.family === family ? current : (members[0] ?? null);
|
|
246
250
|
}
|
|
247
|
-
|
|
251
|
+
const rebuilt = parseDesktopProfile({ version: 1, assignments, defaults });
|
|
252
|
+
return preserveDesktopAppliedState(profile, rebuilt);
|
|
248
253
|
}
|
|
249
254
|
|
|
250
255
|
export function moveDesktopRoute(
|
|
@@ -268,7 +273,7 @@ export function moveDesktopRoute(
|
|
|
268
273
|
const destinationMembers = Object.keys(assignments).filter(key => assignments[key]!.family === family).sort();
|
|
269
274
|
if (makeDefault || !defaults[family] || assignments[defaults[family]!]?.family !== family) defaults[family] = route;
|
|
270
275
|
if (!defaults[family] && destinationMembers.length > 0) defaults[family] = destinationMembers[0]!;
|
|
271
|
-
return parseDesktopProfile({ version: 1, assignments, defaults
|
|
276
|
+
return parseDesktopProfile({ version: 1, assignments, defaults });
|
|
272
277
|
}
|
|
273
278
|
|
|
274
279
|
export function setDesktopFamilyDefault(
|
|
@@ -280,7 +285,12 @@ export function setDesktopFamilyDefault(
|
|
|
280
285
|
const members = Object.keys(parsed.assignments).filter(key => parsed.assignments[key]!.family === family);
|
|
281
286
|
if (route === null && members.length > 0) throw new DesktopProfileError("cannot clear a non-empty family default", `profile.defaults.${family}`);
|
|
282
287
|
if (route !== null && parsed.assignments[route]?.family !== family) throw new DesktopProfileError("route is not a member of this family", `profile.defaults.${family}`);
|
|
283
|
-
|
|
288
|
+
const rebuilt = parseDesktopProfile({
|
|
289
|
+
version: 1,
|
|
290
|
+
assignments: parsed.assignments,
|
|
291
|
+
defaults: { ...parsed.defaults, [family]: route },
|
|
292
|
+
});
|
|
293
|
+
return preserveDesktopAppliedState(parsed, rebuilt);
|
|
284
294
|
}
|
|
285
295
|
|
|
286
296
|
export function renderDesktopProfile(
|
package/src/claude/inbound.ts
CHANGED
|
@@ -118,6 +118,7 @@ export function effectiveBlockedSkillNames(cc?: Pick<OcxClaudeCodeConfig, "block
|
|
|
118
118
|
/** Injected-skill payloads below this size are never stubbed (not worth it). */
|
|
119
119
|
const SKILL_ELISION_MIN_CHARS = 10_000;
|
|
120
120
|
const SKILL_TEXT_MARKER = "Base directory for this skill: ";
|
|
121
|
+
const SKILL_TEXT_PATH_MAX_CHARS = 4_096;
|
|
121
122
|
|
|
122
123
|
interface SkillElisionContext {
|
|
123
124
|
/** Skill-tool call ids whose input names a blocked skill (result-body carrier). */
|
|
@@ -138,11 +139,15 @@ const NO_ELISION: SkillElisionContext = { callIds: new Set(), names: [] };
|
|
|
138
139
|
function maybeElideSkillText(text: string, names: readonly string[]): string {
|
|
139
140
|
if (names.length === 0 || text.length < SKILL_ELISION_MIN_CHARS) return text;
|
|
140
141
|
if (!text.startsWith(SKILL_TEXT_MARKER)) return text;
|
|
141
|
-
const
|
|
142
|
-
const
|
|
142
|
+
const pathStart = SKILL_TEXT_MARKER.length;
|
|
143
|
+
const pathPrefix = text.slice(pathStart, pathStart + SKILL_TEXT_PATH_MAX_CHARS + 1);
|
|
144
|
+
const firstLineEnd = pathPrefix.indexOf("\n");
|
|
145
|
+
if (firstLineEnd === -1 && pathPrefix.length > SKILL_TEXT_PATH_MAX_CHARS) return text;
|
|
146
|
+
const dir = pathPrefix.slice(0, firstLineEnd === -1 ? pathPrefix.length : firstLineEnd).trim();
|
|
143
147
|
// Windows clients send `C:\Users\...\claude-api`; normalize separators before
|
|
144
148
|
// basenaming (repo precedent: src/codex/inject.ts isOpencodexCatalogPath).
|
|
145
|
-
const
|
|
149
|
+
const normalizedDir = dir.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
150
|
+
const base = normalizedDir.slice(normalizedDir.lastIndexOf("/") + 1).toLowerCase();
|
|
146
151
|
if (!names.includes(base)) return text;
|
|
147
152
|
return `[opencodex] '${base}' skill document bundle (${text.length} chars) elided for routed models `
|
|
148
153
|
+ "(claudeCode.blockedSkills). The skill is loaded; answer from general knowledge instead of citing the bundle.";
|
package/src/cli/access.ts
CHANGED
|
@@ -22,6 +22,19 @@ const USAGE = `Usage:
|
|
|
22
22
|
ocx access models [--json]
|
|
23
23
|
ocx access test <model> [--protocol <chat|responses|messages>] [--json]`;
|
|
24
24
|
|
|
25
|
+
const UTC_ISO_INSTANT_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The server emits `attributionSince` with `toISOString()`. `Date.parse` alone also accepts
|
|
29
|
+
* strings such as "0", so require the ISO-8601 UTC shape and an instant that round-trips to the
|
|
30
|
+
* same second, which also rejects impossible dates the parser would roll over.
|
|
31
|
+
*/
|
|
32
|
+
function isUtcIsoInstant(value: unknown): value is string {
|
|
33
|
+
if (typeof value !== "string" || !UTC_ISO_INSTANT_RE.test(value)) return false;
|
|
34
|
+
const parsed = Date.parse(value);
|
|
35
|
+
return Number.isFinite(parsed) && new Date(parsed).toISOString().slice(0, 19) === value.slice(0, 19);
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
/**
|
|
26
39
|
* Render the key table with the usage fields the API already returns (#2705).
|
|
27
40
|
*
|
|
@@ -38,6 +51,10 @@ const USAGE = `Usage:
|
|
|
38
51
|
*/
|
|
39
52
|
function formatKeyRows(payload: Record<string, unknown>, keys: Array<Record<string, unknown>>): string[] {
|
|
40
53
|
const cells: string[][] = [["ID", "NAME", "PREFIX", "REQ 7D", "TOTAL", "LAST USED"]];
|
|
54
|
+
// A string that is not an ISO-8601 UTC instant is not attribution data: treat it like an
|
|
55
|
+
// absent field so malformed payloads still render "unavailable" instead of usage values.
|
|
56
|
+
const attributionSince = isUtcIsoInstant(payload.attributionSince) ? payload.attributionSince : undefined;
|
|
57
|
+
const usageAvailable = attributionSince !== undefined;
|
|
41
58
|
for (const entry of keys) {
|
|
42
59
|
const usage = (entry.usage ?? {}) as Record<string, unknown>;
|
|
43
60
|
const ambiguous = usage.ambiguous === true;
|
|
@@ -47,16 +64,16 @@ function formatKeyRows(payload: Record<string, unknown>, keys: Array<Record<stri
|
|
|
47
64
|
String(entry.name ?? ""),
|
|
48
65
|
String(entry.prefix ?? ""),
|
|
49
66
|
// One marker spanning both numeric columns: the union guarantees neither exists.
|
|
50
|
-
ambiguous ? "ambiguous" : num(usage.requests7d),
|
|
51
|
-
ambiguous ? "" : num(usage.totalRequests),
|
|
52
|
-
ambiguous ? "" : (typeof usage.lastUsedAt === "string" ? usage.lastUsedAt : "never"),
|
|
67
|
+
!usageAvailable ? "unavailable" : ambiguous ? "ambiguous" : num(usage.requests7d),
|
|
68
|
+
!usageAvailable || ambiguous ? "" : num(usage.totalRequests),
|
|
69
|
+
!usageAvailable || ambiguous ? "" : (typeof usage.lastUsedAt === "string" ? usage.lastUsedAt : "never"),
|
|
53
70
|
]);
|
|
54
71
|
}
|
|
55
72
|
const widths = cells[0]!.map((_, column) => Math.max(...cells.map(row => (row[column] ?? "").length)));
|
|
56
73
|
const lines = cells.map(row => row.map((cell, i) => (cell ?? "").padEnd(widths[i]!)).join(" ").trimEnd());
|
|
57
74
|
const footer: string[] = [];
|
|
58
|
-
if (
|
|
59
|
-
footer.push(`attribution since ${
|
|
75
|
+
if (attributionSince !== undefined) {
|
|
76
|
+
footer.push(`attribution since ${attributionSince}`);
|
|
60
77
|
}
|
|
61
78
|
if (payload.historyTruncated === true) {
|
|
62
79
|
footer.push("older history truncated");
|
package/src/cli/account-api.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface AccountRow {
|
|
|
33
33
|
validationPending?: boolean;
|
|
34
34
|
/** Codex pool selection order, higher used earlier. Absent where ordering does not apply. */
|
|
35
35
|
priority?: number;
|
|
36
|
+
/** Null means the account inherits the global usage-switch threshold. */
|
|
37
|
+
autoSwitchThresholdOverride?: number | null;
|
|
36
38
|
quota?: CodexQuotaDto | null;
|
|
37
39
|
quotaRefresh?: CodexQuotaRefreshOutcome;
|
|
38
40
|
quotaUnavailable?: boolean;
|
|
@@ -252,6 +254,7 @@ interface CodexAccountDto {
|
|
|
252
254
|
selectionExcludedPlan?: string;
|
|
253
255
|
health?: { reason?: string };
|
|
254
256
|
priority?: number;
|
|
257
|
+
autoSwitchThresholdOverride?: number | null;
|
|
255
258
|
quota?: CodexQuotaDto | null;
|
|
256
259
|
quotaRefresh?: unknown;
|
|
257
260
|
paused?: boolean;
|
|
@@ -322,6 +325,9 @@ export async function fetchCodexRows(
|
|
|
322
325
|
} : {}),
|
|
323
326
|
...(a.health?.reason === "validation_pending" ? { validationPending: true } : {}),
|
|
324
327
|
priority: typeof a.priority === "number" ? a.priority : 0,
|
|
328
|
+
autoSwitchThresholdOverride: typeof a.autoSwitchThresholdOverride === "number"
|
|
329
|
+
? a.autoSwitchThresholdOverride
|
|
330
|
+
: null,
|
|
325
331
|
paused: a.paused === true,
|
|
326
332
|
...(includeQuota ? {
|
|
327
333
|
quota: projectQuota(a.quota),
|
package/src/cli/account-auth.ts
CHANGED
|
@@ -36,7 +36,7 @@ function writeStdoutFully(text: string): void {
|
|
|
36
36
|
const USAGE = `Usage:
|
|
37
37
|
ocx account login <provider> [--id <account-id>] [--reauth] [--device] [--code -] [--no-wait] [--json]
|
|
38
38
|
ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
|
|
39
|
-
ocx account cancel <provider> [--flow <flow-id>] [--json]
|
|
39
|
+
ocx account cancel <provider> [--flow <flow-id>] [--json] (--flow required for codex)
|
|
40
40
|
ocx account reset-credits <account-id|main> [--consume --yes [--operation-id <uuid>]] [--json]
|
|
41
41
|
ocx account grok-reset-coupons [<account-id>] [--consume --yes [--token-id <token-id>] [--operation-id <uuid>]] [--json]
|
|
42
42
|
|
|
@@ -281,10 +281,13 @@ async function cancel(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
281
281
|
const args = [...argv];
|
|
282
282
|
const provider = args.shift()?.trim().toLowerCase();
|
|
283
283
|
const wantsJson = takeFlag(args, "--json");
|
|
284
|
-
const flowId = takeOption(args, "--flow");
|
|
284
|
+
const flowId = takeOption(args, "--flow")?.trim();
|
|
285
285
|
if (!provider) throw new CliUsageError("provider is required", USAGE);
|
|
286
286
|
rejectArgs(args, USAGE);
|
|
287
287
|
const codex = CODEX_NAMES.has(provider);
|
|
288
|
+
if (codex && !flowId) {
|
|
289
|
+
throw new CliUsageError("Codex login cancel requires --flow <flow-id> (printed by 'ocx account login').", USAGE);
|
|
290
|
+
}
|
|
288
291
|
const result = await runtimeRequest(codex ? "/api/codex-auth/login/cancel" : "/api/oauth/login/cancel", {
|
|
289
292
|
method: "POST",
|
|
290
293
|
body: JSON.stringify(codex ? { flowId } : { provider }),
|
package/src/cli/account.ts
CHANGED
|
@@ -60,7 +60,7 @@ const ACCOUNT_USAGE = `Usage:
|
|
|
60
60
|
ocx account import-orca --source <orca-data-directory> --registry <orca-data.json> [--apply] [--json]
|
|
61
61
|
ocx account login <provider> [--id <account-id>] [--reauth] [--code -] [--no-wait] [--json]
|
|
62
62
|
ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
|
|
63
|
-
ocx account cancel <provider> [--flow <flow-id>] [--json]
|
|
63
|
+
ocx account cancel <provider> [--flow <flow-id>] [--json] (--flow required for codex)
|
|
64
64
|
ocx account reset-credits <account-id|main> [--consume --yes] [--json]
|
|
65
65
|
ocx account grok-reset-coupons [<account-id>] [--consume --yes] [--token-id <token-id>] [--json]
|
|
66
66
|
ocx account main <doctor|list|register|add|reauth|switch|recover> ...
|
|
@@ -340,10 +340,9 @@ async function cmdUse(rest: string[], deps: AccountDeps): Promise<number> {
|
|
|
340
340
|
}
|
|
341
341
|
if (c.type === "codex") {
|
|
342
342
|
console.error("Takes effect immediately; running threads move on their next request, and in-flight requests keep the account they captured.");
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
: undefined;
|
|
343
|
+
const state = await fetchRows(deps, baseUrl, name, "codex");
|
|
344
|
+
const selected = state.rows.find(row => row.id === activeId);
|
|
345
|
+
const threshold = selected?.autoSwitchThresholdOverride ?? state.autoSwitchThreshold;
|
|
347
346
|
if (pinDrainReason !== undefined) {
|
|
348
347
|
// "may override" is the right caveat for a pin that is currently fine and could be
|
|
349
348
|
// overtaken later. It is the wrong sentence for one the next request will discard, and
|
|
@@ -352,7 +351,7 @@ async function cmdUse(rest: string[], deps: AccountDeps): Promise<number> {
|
|
|
352
351
|
? `is at or above the auto-switch threshold${threshold !== undefined ? ` (${threshold}%)` : ""}`
|
|
353
352
|
: `cannot currently be selected (${pinDrainReason})`;
|
|
354
353
|
console.error(`Note: ${displayId(activeId)} ${because}, so routing releases this pin on its next request.`);
|
|
355
|
-
} else if (threshold
|
|
354
|
+
} else if (state.status === 200 && typeof threshold === "number" && threshold > 0) {
|
|
356
355
|
console.error(`Note: auto-switch (threshold ${threshold}%) may override this pin.`);
|
|
357
356
|
}
|
|
358
357
|
}
|
|
@@ -1,10 +1,70 @@
|
|
|
1
1
|
import type { OwnedIntegrationRefreshOutcome } from "../integrations/owned-refresh";
|
|
2
|
+
import { readRuntimePort } from "../config/process-state";
|
|
3
|
+
import { createLocalAttestationChallenge, LOCAL_ATTESTATION_CHALLENGE_HEADER, LOCAL_ATTESTATION_PROOF_HEADER, verifyLocalAttestationProof } from "../lib/local-management-attestation";
|
|
4
|
+
import { createLocalAsideSyncCapability, LOCAL_ASIDE_SYNC_CAPABILITY_HEADER, LOCAL_ASIDE_SYNC_CAPABILITY_TTL_MS, LOCAL_ASIDE_SYNC_CAPABILITY_VERSION, LOCAL_ASIDE_SYNC_EXPECTED_PID_HEADER, LOCAL_ASIDE_SYNC_EXPIRES_AT_HEADER, LOCAL_ASIDE_SYNC_METHOD, LOCAL_ASIDE_SYNC_NONCE_HEADER, LOCAL_ASIDE_SYNC_PATH } from "../lib/local-aside-sync-contract";
|
|
5
|
+
import { directLocalHttpFetch } from "../server/direct-local-http";
|
|
6
|
+
import { findLiveProxy, isOpencodexHealthz, probeHostname } from "../server/proxy-liveness";
|
|
2
7
|
import { runtimeRequest, RuntimeApiError, type RuntimeApiDeps } from "./runtime-api";
|
|
3
8
|
|
|
4
9
|
/** Aside policy and file writes share the running server's mutation owner. Never fall back locally. */
|
|
5
10
|
export async function refreshAsideProfilesThroughServer(
|
|
6
|
-
|
|
11
|
+
// The optional transport seam observes the real direct-local exchange in listener tests.
|
|
12
|
+
deps: RuntimeApiDeps & {
|
|
13
|
+
directLocalFetch?: typeof directLocalHttpFetch;
|
|
14
|
+
exchangeDeadlineMs?: number;
|
|
15
|
+
scheduleExchangeDeadline?: (onTimeout: () => void, delayMs: number) => () => void;
|
|
16
|
+
} = {},
|
|
7
17
|
): Promise<OwnedIntegrationRefreshOutcome[]> {
|
|
18
|
+
// An explicit URL is an opt-in transport used by connected callers and tests.
|
|
19
|
+
if (!deps.baseUrl) {
|
|
20
|
+
const localFetch = deps.directLocalFetch ?? directLocalHttpFetch;
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const onTimeout = () => controller.abort(new DOMException("The Aside sync exchange timed out", "TimeoutError"));
|
|
23
|
+
const cancelDeadline = (deps.scheduleExchangeDeadline ?? ((callback, delayMs) => {
|
|
24
|
+
const timer = setTimeout(callback, delayMs);
|
|
25
|
+
return () => clearTimeout(timer);
|
|
26
|
+
}))(onTimeout, deps.exchangeDeadlineMs ?? LOCAL_ASIDE_SYNC_CAPABILITY_TTL_MS);
|
|
27
|
+
const deadline = controller.signal;
|
|
28
|
+
try {
|
|
29
|
+
const live = await (deps.findLiveProxy ?? findLiveProxy)();
|
|
30
|
+
if (!live) throw new RuntimeApiError("Proxy is not running. Start it with: ocx start", 503, null);
|
|
31
|
+
if (live.source !== "runtime" || live.pid === null) {
|
|
32
|
+
throw new RuntimeApiError("Aside profile synchronization requires an attested running proxy", 503, null);
|
|
33
|
+
}
|
|
34
|
+
const runtime = readRuntimePort(live.pid);
|
|
35
|
+
if (!runtime?.attestationSecret || runtime.pid !== live.pid || runtime.port !== live.port) {
|
|
36
|
+
throw new RuntimeApiError("Aside profile synchronization could not verify the running proxy", 503, null);
|
|
37
|
+
}
|
|
38
|
+
const nonce = createLocalAttestationChallenge();
|
|
39
|
+
const baseUrl = `http://${probeHostname(live.hostname)}:${live.port}`;
|
|
40
|
+
const proofResponse = await localFetch(`${baseUrl}/healthz`, { headers: { [LOCAL_ATTESTATION_CHALLENGE_HEADER]: nonce }, signal: deadline });
|
|
41
|
+
const health = await proofResponse.json().catch(() => null);
|
|
42
|
+
if (!proofResponse.ok || !isOpencodexHealthz(health) || health?.pid !== live.pid || health?.port !== live.port
|
|
43
|
+
|| health?.asideSyncCapability !== LOCAL_ASIDE_SYNC_CAPABILITY_VERSION
|
|
44
|
+
|| !verifyLocalAttestationProof(runtime.attestationSecret, nonce, live.pid, live.port, proofResponse.headers.get(LOCAL_ATTESTATION_PROOF_HEADER))) {
|
|
45
|
+
throw new RuntimeApiError("Aside profile synchronization could not attest the running proxy", 503, null);
|
|
46
|
+
}
|
|
47
|
+
const expiresAt = Date.now() + LOCAL_ASIDE_SYNC_CAPABILITY_TTL_MS;
|
|
48
|
+
const capability = createLocalAsideSyncCapability(runtime.attestationSecret, nonce, LOCAL_ASIDE_SYNC_METHOD, LOCAL_ASIDE_SYNC_PATH, live.pid, live.port, expiresAt);
|
|
49
|
+
if (!capability) throw new RuntimeApiError("Aside profile synchronization capability was unavailable", 503, null);
|
|
50
|
+
const response = await localFetch(`${baseUrl}${LOCAL_ASIDE_SYNC_PATH}`, {
|
|
51
|
+
method: LOCAL_ASIDE_SYNC_METHOD,
|
|
52
|
+
signal: deadline,
|
|
53
|
+
headers: {
|
|
54
|
+
[LOCAL_ASIDE_SYNC_EXPECTED_PID_HEADER]: String(live.pid),
|
|
55
|
+
[LOCAL_ASIDE_SYNC_NONCE_HEADER]: nonce,
|
|
56
|
+
[LOCAL_ASIDE_SYNC_EXPIRES_AT_HEADER]: String(expiresAt),
|
|
57
|
+
[LOCAL_ASIDE_SYNC_CAPABILITY_HEADER]: capability,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
const body = await response.json().catch(() => null) as { results?: OwnedIntegrationRefreshOutcome[] } | null;
|
|
61
|
+
if (!response.ok) throw new RuntimeApiError("Aside profile synchronization was rejected", response.status, body);
|
|
62
|
+
if (!Array.isArray(body?.results)) throw new RuntimeApiError("The running proxy does not support Aside profile synchronization", 502, body);
|
|
63
|
+
return body.results;
|
|
64
|
+
} finally {
|
|
65
|
+
cancelDeadline();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
8
68
|
const result = await runtimeRequest<{ results?: OwnedIntegrationRefreshOutcome[] }>(
|
|
9
69
|
"/api/client-integrations/aside/sync",
|
|
10
70
|
{ method: "POST", body: "{}" },
|
|
@@ -398,7 +398,8 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
398
398
|
const applyInvocation = argv.length === 0 || command === "apply" || applyFlags.length > 0;
|
|
399
399
|
if (applyInvocation) {
|
|
400
400
|
const rest = argv.filter(arg => arg !== "apply");
|
|
401
|
-
const
|
|
401
|
+
const preApplyConfig = loadConfig();
|
|
402
|
+
const parsedTarget = parseDesktopApplyArgs(rest, preApplyConfig);
|
|
402
403
|
if ("error" in parsedTarget) { console.error(parsedTarget.error); return 2; }
|
|
403
404
|
const { target } = parsedTarget;
|
|
404
405
|
try {
|
|
@@ -424,7 +425,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
424
425
|
console.log(`Claude Desktop gateway 설정을 적용했습니다: ${result.path}`);
|
|
425
426
|
for (const line of gatewayModeExplanation({
|
|
426
427
|
requestedExplicitly: applyFlags.some(flag => flag !== "--first-party"),
|
|
427
|
-
config:
|
|
428
|
+
config: preApplyConfig,
|
|
428
429
|
})) {
|
|
429
430
|
console.log(line);
|
|
430
431
|
}
|
package/src/cli/claude.ts
CHANGED
|
@@ -572,7 +572,6 @@ export function claudeLaunchPreflight(
|
|
|
572
572
|
*/
|
|
573
573
|
const NATIVE_STRIPPED_LEVERS = [
|
|
574
574
|
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY",
|
|
575
|
-
"CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST",
|
|
576
575
|
"CLAUDE_CODE_MAX_CONTEXT_TOKENS",
|
|
577
576
|
"CLAUDE_CODE_AUTO_COMPACT_WINDOW",
|
|
578
577
|
"CLAUDE_CODE_ALWAYS_ENABLE_EFFORT",
|
|
@@ -632,6 +631,9 @@ export function buildNativeClaudeEnv(
|
|
|
632
631
|
}
|
|
633
632
|
|
|
634
633
|
for (const name of NATIVE_STRIPPED_LEVERS) delete env[name];
|
|
634
|
+
// An explicit caller-owned guard must follow a caller-owned gateway and credential;
|
|
635
|
+
// otherwise settings.env can replace the destination while retaining the credential.
|
|
636
|
+
if (hasOwnedAdmission) delete env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST;
|
|
635
637
|
const providerNames = Object.keys(config.providers);
|
|
636
638
|
for (const name of MODEL_ENV_SLOT_NAMES) {
|
|
637
639
|
const value = env[name];
|
|
@@ -31,7 +31,7 @@ export interface CodexCliUpdateCommandDeps {
|
|
|
31
31
|
readonly inspectIdentity?: (input: CodexCliInstallationIdentityInput) => Promise<CodexCliInstallationIdentityReport>;
|
|
32
32
|
readonly deriveInstallationInput?: (
|
|
33
33
|
snapshot: CodexCliInstallationSnapshot,
|
|
34
|
-
) => CodexCliInstallationTargetDerivation
|
|
34
|
+
) => Promise<CodexCliInstallationTargetDerivation>;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function identitySummary(report: CodexCliInstallationIdentityReport): string[] {
|
|
@@ -148,7 +148,7 @@ export async function handleCodexCliUpdateCommand(
|
|
|
148
148
|
const snapshot = trustedNodeLauncherContext()?.codexCliInspectionEnv;
|
|
149
149
|
const derive = deps.deriveInstallationInput
|
|
150
150
|
?? (await import("../codex/cli-installation-targets")).deriveCodexCliInstallationInput;
|
|
151
|
-
const derived = derive({
|
|
151
|
+
const derived = await derive({
|
|
152
152
|
codexCliPath: snapshot?.codexCliPath ?? null,
|
|
153
153
|
path: snapshot?.path ?? null,
|
|
154
154
|
pathExt: snapshot?.pathExt ?? null,
|
|
@@ -22,6 +22,9 @@ export function skipsCodexShimAutoRestore(command: string | undefined, args: str
|
|
|
22
22
|
// `resolve` is read-only inspection for embedding shells: a lookup made to populate
|
|
23
23
|
// a consent surface must not trigger a shim repair side effect first.
|
|
24
24
|
if (command === "resolve") return true;
|
|
25
|
+
// The desktop's approval-bound stop must validate its snapshot before any unrelated
|
|
26
|
+
// CLI repair changes the installation it was approved against.
|
|
27
|
+
if (command === "stop" && args.some(arg => arg.startsWith("--expect-"))) return true;
|
|
25
28
|
// The entire updater-inspection namespace is zero-effect, including malformed
|
|
26
29
|
// or future actions. A later `apply` implementation must own its preflight.
|
|
27
30
|
if (command === "system" && args[1] === "codex-cli-update") return true;
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { handleRestartScopeAfterWrite, readRestartScope, type RestartScope } fro
|
|
|
30
30
|
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
31
31
|
import { isJsonOption, takeFlag, terminalSafeError } from "./runtime-api";
|
|
32
32
|
import { printStopSummary, type StopOutcome } from "./stop-report";
|
|
33
|
+
import { parseStopApproval, type StopApproval } from "./stop-approval";
|
|
33
34
|
import type { ResolveArgs } from "./resolve";
|
|
34
35
|
import type { ClientConnectionState } from "../client/state";
|
|
35
36
|
import { OCX_NATIVE_REPLAY_RECOVERY_NOTE } from "../responses/compaction";
|
|
@@ -46,7 +47,7 @@ export interface CliDispatchDeps {
|
|
|
46
47
|
/** Spawn a detached proxy child (stdio ignore, unref'd, provenance env). */
|
|
47
48
|
spawnDetached: (argv: readonly string[]) => void;
|
|
48
49
|
handleStart: () => Promise<void>;
|
|
49
|
-
handleStop: () => Promise<StopOutcome>;
|
|
50
|
+
handleStop: (approval?: StopApproval) => Promise<StopOutcome>;
|
|
50
51
|
handleEnsure: (options?: { existingIsSuccess?: boolean }) => Promise<boolean>;
|
|
51
52
|
handleResolve: (args: ResolveArgs) => Promise<number>;
|
|
52
53
|
handleTrayProxyStart: (existingIsSuccess?: boolean) => Promise<boolean>;
|
|
@@ -99,10 +100,15 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
99
100
|
return Number(process.exitCode ?? 0);
|
|
100
101
|
},
|
|
101
102
|
stop: async deps => {
|
|
103
|
+
const parsed = parseStopApproval(deps.args.slice(1));
|
|
104
|
+
if (!parsed.ok) {
|
|
105
|
+
console.error("Usage: ocx stop [--json [--expect-pid <pid> --expect-port <port> --expect-hostname <host> --expect-config-home <home> --expect-cli-version <version> --expect-compatibility-token <hex>]]");
|
|
106
|
+
return 64;
|
|
107
|
+
}
|
|
102
108
|
// Downtime warning lives HERE, not in handleStop: `restart`/tray-restart callers
|
|
103
109
|
// re-start the proxy immediately, so warning there would contradict the next line.
|
|
104
110
|
const warning = "⚠️ Codex/Claude requests through the proxy will fail until it is restarted ('ocx start' or 'ocx service start').";
|
|
105
|
-
if (!
|
|
111
|
+
if (!parsed.json) {
|
|
106
112
|
// handleStop returns the structured outcome now; an object is always truthy, so
|
|
107
113
|
// the warning must key on .ok — otherwise a failed stop would still claim downtime.
|
|
108
114
|
if ((await deps.handleStop()).ok) console.log(warning);
|
|
@@ -117,7 +123,7 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
117
123
|
console.log = console.error;
|
|
118
124
|
let outcome: StopOutcome | undefined;
|
|
119
125
|
try {
|
|
120
|
-
outcome = await deps.handleStop();
|
|
126
|
+
outcome = await deps.handleStop(parsed.approval ?? undefined);
|
|
121
127
|
if (outcome.ok) console.log(warning);
|
|
122
128
|
} finally {
|
|
123
129
|
console.log = humanLog;
|
|
@@ -125,7 +131,9 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
125
131
|
// A throw above propagates after the finally restores the console, so reaching here
|
|
126
132
|
// with an undefined outcome cannot happen; the guard keeps the assignment provable.
|
|
127
133
|
if (outcome) printStopSummary(outcome.summary);
|
|
128
|
-
|
|
134
|
+
// A guarded refusal never reaches the code that records process.exitCode, so the
|
|
135
|
+
// approval-bound form answers with its summary's code; plain stop keeps its contract.
|
|
136
|
+
return parsed.approval ? (outcome?.summary.exitCode ?? 1) : Number(process.exitCode ?? 0);
|
|
129
137
|
},
|
|
130
138
|
resolve: async deps => {
|
|
131
139
|
// Same fail-closed shape as `ready`: parseCliHead pre-parsed the verb before any
|