@bitkyc08/opencodex 2.67.0-preview.20260926 → 2.68.0-preview.20260927
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/bin/ocx.mjs +3 -0
- package/gui/dist/assets/App-B5-cAEd2.js +51 -0
- package/gui/dist/assets/App-DCBismRi.css +1 -0
- package/gui/dist/assets/Tray-DFnMiyD0.js +1 -0
- package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
- package/gui/dist/assets/index-BcXblvem.js +86 -0
- package/gui/dist/assets/quota-summary-i84eOU3o.js +1 -0
- package/gui/dist/index.html +21 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-guard.ts +13 -1
- package/src/adapters/anthropic-image-normalize.ts +3 -2
- package/src/adapters/anthropic-output-schema.ts +36 -0
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/codebuddy/live-models.ts +180 -0
- package/src/adapters/coding-agent/protocol.ts +91 -12
- package/src/adapters/coding-agent/turn.ts +32 -27
- package/src/adapters/devin/cloud-direct/index.ts +1 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
- package/src/adapters/devin.ts +9 -6
- package/src/adapters/google-http.ts +4 -4
- package/src/adapters/google.ts +93 -3
- package/src/adapters/kiro/adapter.ts +6 -2
- package/src/adapters/kiro/stream.ts +12 -2
- package/src/adapters/kiro/usage.ts +3 -1
- package/src/adapters/kiro-errors.ts +11 -1
- package/src/adapters/kiro-events.ts +27 -1
- package/src/adapters/kiro-refusal.ts +30 -0
- package/src/adapters/kiro-retry.ts +71 -31
- package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
- package/src/adapters/openai-chat/messages.ts +31 -4
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
- package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
- package/src/adapters/openai-chat/tool-schema.ts +6 -2
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -3
- package/src/adapters/opencode-go-additional-tools.ts +19 -10
- package/src/adapters/physical-send.ts +10 -3
- package/src/adapters/registry.ts +2 -1
- package/src/adapters/responses-tool-schema.ts +31 -3
- package/src/adapters/run-turn-queue.ts +63 -23
- package/src/adapters/unique-tool-call-ids.ts +63 -0
- package/src/adapters/xai-web-search.ts +32 -2
- package/src/bridge/sse.ts +14 -2
- package/src/chat/inbound.ts +20 -6
- package/src/claude/agents-inject.ts +4 -0
- package/src/claude/desktop-3p.ts +53 -13
- package/src/claude/desktop-profile.ts +41 -4
- package/src/claude/inbound-model-options.ts +14 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +17 -1
- package/src/claude/intercept/local-ca.ts +7 -1
- package/src/claude/message-threads.ts +28 -0
- package/src/cli/account-api.ts +13 -0
- package/src/cli/account-auth.ts +49 -2
- package/src/cli/account-extended.ts +4 -2
- package/src/cli/account.ts +4 -2
- package/src/cli/capabilities.ts +17 -0
- package/src/cli/claude.ts +3 -1
- package/src/cli/dispatch.ts +62 -5
- package/src/cli/index.ts +84 -79
- package/src/cli/minimax.ts +4 -2
- package/src/cli/opencode.ts +6 -1
- package/src/cli/restart-handoff.ts +110 -0
- package/src/cli/status.ts +51 -0
- package/src/client/connect.ts +34 -15
- package/src/client/link-ingress.ts +102 -0
- package/src/client/link-join.ts +37 -16
- package/src/client/link-relay.ts +228 -45
- package/src/client/link-state.ts +54 -0
- package/src/client/link-status.ts +39 -0
- package/src/client/link-teardown.ts +2 -2
- package/src/client/link-tunnel.ts +615 -86
- package/src/client/machine-api.ts +10 -4
- package/src/client/machine-listener.ts +57 -10
- package/src/client/runtime.ts +210 -35
- package/src/clients/config-export/constants.ts +1 -12
- package/src/clients/config-export/contracts.ts +2 -0
- package/src/clients/config-export/model-metadata.ts +9 -3
- package/src/clients/config-export/omp.ts +1 -1
- package/src/clients/config-export/zcode-store.ts +2 -2
- package/src/clients/config-export.ts +36 -3
- package/src/codex/app-server-processes.ts +32 -0
- package/src/codex/app-server-restart-service.ts +20 -3
- package/src/codex/catalog/access-programs.ts +50 -0
- package/src/codex/catalog/build-entries.ts +13 -0
- package/src/codex/catalog/derive-entry.ts +1 -0
- package/src/codex/catalog/model-hints.ts +11 -0
- package/src/codex/catalog/parsing.ts +6 -2
- package/src/codex/catalog/provider-models.ts +81 -0
- package/src/codex/catalog/retained-sync.ts +3 -0
- package/src/codex/codex-write-lock.ts +2 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desired-state.ts +14 -4
- package/src/codex/home.ts +21 -3
- package/src/codex/inject/restore.ts +17 -0
- package/src/codex/inject/routing-classify.ts +3 -2
- package/src/codex/inject-coordination.ts +4 -1
- package/src/codex/inject.ts +8 -1
- package/src/codex/journal.ts +6 -2
- package/src/codex/management-convergence.ts +9 -0
- package/src/codex/model-entitlements.ts +28 -5
- package/src/codex/routing/idle-window.ts +58 -0
- package/src/codex/routing-drift.ts +134 -0
- package/src/codex/routing-healer.ts +419 -0
- package/src/codex/routing.ts +10 -0
- package/src/codex/runtime.ts +188 -57
- package/src/codex/sibling-handoff.ts +71 -0
- package/src/codex/sibling-start.ts +139 -0
- package/src/codex/sync.ts +5 -3
- package/src/combos/failover.ts +32 -5
- package/src/combos/request.ts +11 -3
- package/src/combos/reset-window.ts +10 -5
- package/src/combos/resolve.ts +36 -6
- package/src/config/diagnostics.ts +2 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/paths.ts +7 -1
- package/src/config/process-state.ts +10 -1
- package/src/config/schema/compaction-recovery.ts +15 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +9 -3
- package/src/generated/compatibility-version.json +360 -204
- package/src/images/loop.ts +47 -32
- package/src/integrations/catalog-refresh.ts +4 -0
- package/src/integrations/omp-yaml-source.ts +1 -0
- package/src/lib/claude-request-projection.ts +101 -0
- package/src/lib/local-account-switch-capability.ts +48 -0
- package/src/lib/local-upstream.ts +146 -0
- package/src/lib/package-tree-integrity.ts +25 -3
- package/src/lib/package-tree-retarget.ts +130 -0
- package/src/lib/process-control.ts +4 -1
- package/src/lib/system-restart-contract.ts +106 -0
- package/src/lib/upstream-retry.ts +57 -1
- package/src/link/ports.ts +9 -0
- package/src/link/ssh-argv.ts +16 -0
- package/src/link/ssh-runner.ts +70 -2
- package/src/link/tunnel-state.ts +58 -9
- package/src/oauth/account-quota-rank.ts +29 -18
- package/src/oauth/generic-account-failover.ts +179 -22
- package/src/oauth/index.ts +17 -4
- package/src/oauth/kiro-account-load.ts +107 -0
- package/src/oauth/kiro-device-login.ts +312 -0
- package/src/oauth/kiro-terminal-failover.ts +28 -0
- package/src/oauth/login-flow-state.ts +7 -1
- package/src/oauth/pool-settings-capability.ts +23 -14
- package/src/oauth/store.ts +56 -3
- package/src/oauth/types.ts +4 -0
- package/src/plugins/loader.ts +350 -0
- package/src/plugins/upstream-hooks.ts +119 -0
- package/src/protocols/encoders/adapter-events.ts +9 -2
- package/src/providers/account-quota-disk.ts +42 -4
- package/src/providers/codebuddy-models.ts +6 -2
- package/src/providers/command-code-efforts.ts +33 -10
- package/src/providers/kiro-account-state-disk.ts +44 -0
- package/src/providers/kiro-model-catalog.ts +162 -0
- package/src/providers/kiro-models.ts +5 -4
- package/src/providers/kiro-quota-metrics.ts +35 -0
- package/src/providers/kiro-usage.ts +118 -14
- package/src/providers/quota/account-cache.ts +43 -7
- package/src/providers/quota/antigravity.ts +8 -3
- package/src/providers/quota/kiro-account-probe.ts +12 -0
- package/src/providers/quota/vendor-probes-key.ts +31 -26
- package/src/providers/quota/vendor-probes-oauth.ts +16 -7
- package/src/providers/quota-types.ts +14 -0
- package/src/providers/quota.ts +21 -21
- package/src/providers/registry/entries-extended.ts +5 -3
- package/src/providers/request-pacing.ts +410 -12
- package/src/remote-control/workspace-codex-runtime.ts +4 -3
- package/src/responses/citation-markers.ts +132 -65
- package/src/responses/hosted-tool-policy.ts +14 -3
- package/src/responses/parser-content.ts +3 -2
- package/src/responses/parser.ts +4 -1
- package/src/responses/schema.ts +4 -0
- package/src/responses/visualization-directives.ts +182 -0
- package/src/server/chat-native-sse.ts +24 -12
- package/src/server/chat-native.ts +137 -68
- package/src/server/claude-messages.ts +147 -12
- package/src/server/index/link-listener.ts +7 -0
- package/src/server/index/optional-listeners.ts +4 -1
- package/src/server/index/package-tree-guard.ts +35 -16
- package/src/server/index/serve-options.ts +4 -2
- package/src/server/index/startup-warnings.ts +6 -0
- package/src/server/index.ts +7 -7
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/inference/context.ts +30 -2
- package/src/server/local-account-switch-auth.ts +79 -0
- package/src/server/management/agent-settings-routes.ts +11 -3
- package/src/server/management/config-routes.ts +28 -11
- package/src/server/management/context.ts +2 -0
- package/src/server/management/link-routes.ts +123 -39
- package/src/server/management/logs-usage-routes.ts +9 -42
- package/src/server/management/model-rows.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +67 -12
- package/src/server/management/route-registry.ts +5 -5
- package/src/server/management/sibling-guard.ts +60 -0
- package/src/server/management/storage-log-guard-routes.ts +72 -6
- package/src/server/management/system-restart.ts +92 -135
- package/src/server/management/system-routes.ts +1 -1
- package/src/server/management-api.ts +31 -3
- package/src/server/management-auth.ts +3 -0
- package/src/server/port-reclaim.ts +111 -1
- package/src/server/proxy-liveness.ts +16 -0
- package/src/server/relay.ts +16 -15
- package/src/server/request-log-filter.ts +28 -27
- package/src/server/request-log.ts +7 -3
- package/src/server/request-metrics.ts +28 -0
- package/src/server/responses/adapter-continuation.ts +151 -21
- package/src/server/responses/adapter-delivery.ts +21 -7
- package/src/server/responses/adapter-dispatch.ts +239 -51
- package/src/server/responses/codex-ws-pool.ts +5 -4
- package/src/server/responses/codex-ws-request.ts +4 -1
- package/src/server/responses/compact.ts +8 -4
- package/src/server/responses/compaction-recovery-policy.ts +116 -0
- package/src/server/responses/compaction-recovery.ts +330 -0
- package/src/server/responses/core-combo-failure.ts +1 -0
- package/src/server/responses/core-combo.ts +5 -1
- package/src/server/responses/core-lifetime.ts +22 -0
- package/src/server/responses/core-options.ts +16 -1
- package/src/server/responses/core.ts +14 -4
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +82 -31
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +12 -2
- package/src/server/responses/passthrough-execution.ts +2 -2
- package/src/server/responses/request-send-budget.ts +21 -5
- package/src/server/responses/request-transport.ts +116 -18
- package/src/server/responses/reset-replay.ts +32 -0
- package/src/server/responses/run-turn-execution.ts +136 -19
- package/src/server/responses/sidecar-execution.ts +41 -8
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses/ws-upstream.ts +44 -4
- package/src/server/restart-replacement.ts +382 -0
- package/src/server/startup-health-cache.ts +23 -4
- package/src/server/stop-teardown.ts +35 -1
- package/src/server/system-env.ts +3 -0
- package/src/service/windows-taskxml.ts +11 -7
- package/src/service/windows-wrapper-exit.ts +8 -0
- package/src/stall-timeout.ts +35 -10
- package/src/storage/policy-job.ts +4 -0
- package/src/storage/scanner.ts +177 -29
- package/src/storage/storage-mutation-coordinator.ts +17 -0
- package/src/tray/windows-tray.ps1 +50 -23
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +5 -1
- package/src/types/request.ts +13 -2
- package/src/update/index.ts +3 -1
- package/src/update/job.ts +6 -2
- package/src/update/mise-launcher-target.ts +100 -0
- package/src/update/worker-launch.ts +46 -0
- package/src/usage/log.ts +2 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-B3VRRYSj.js +0 -51
- package/gui/dist/assets/App-BJsT8Icc.css +0 -1
- package/gui/dist/assets/Tray-TYAA0EQY.js +0 -1
- package/gui/dist/assets/index-9Ju7nHsu.js +0 -86
- package/gui/dist/assets/tray-data-xKfS2gSG.js +0 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { siblingOfLivePort } from "../../codex/sibling-start";
|
|
2
|
+
import { CODEX_RESTART_PATH } from "../../lib/codex-restart-contract";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Management mutations a sibling instance refuses (`src/codex/sibling-start.ts`).
|
|
6
|
+
*
|
|
7
|
+
* The owner-level gates stop the writers a sibling reaches on its own: startup sync, catalog
|
|
8
|
+
* convergence, owned-catalog refresh, the Claude roster, system env and the stop teardown. These
|
|
9
|
+
* routes reach shared client state through paths those gates do not cover, so the sibling's own
|
|
10
|
+
* management API refuses them outright: toggling or stripping the live owner's Codex, Grok or Claude
|
|
11
|
+
* integrations, rewriting client files at this port, the Codex prompt layers and `[features]`
|
|
12
|
+
* written into the shared `config.toml`, killing the user's Codex app-servers, joining a link (which
|
|
13
|
+
* writes the shared catalog and restarts as a client), staging or switching the physical native-main
|
|
14
|
+
* login, baking the machine service, PATH shim or tray from this home, replacing the global package
|
|
15
|
+
* (the live proxy drains on its package-tree refresh), Codex's own log database, and the
|
|
16
|
+
* archived-session cleanup, cleanup-policy run and trash restore that rewrite the shared CODEX_HOME.
|
|
17
|
+
*
|
|
18
|
+
* Everything else stays open: every read, `POST /api/stop`, `POST /api/system/restart`, and the
|
|
19
|
+
* own-home mutations (providers, keys, settings). `PUT /api/settings` is allowed because it carries
|
|
20
|
+
* own-home settings; its shared fan-out is gated in `syncEnabledClientIntegrations`. So is
|
|
21
|
+
* `PUT /api/storage/cleanup-policy`; the scheduled runs it enables stand down in
|
|
22
|
+
* `maybeRequestStorageCleanupPolicyRun` (`src/storage/policy-job.ts`).
|
|
23
|
+
*
|
|
24
|
+
* Paths are compared through this table and a variable, never as a quoted pathname equality:
|
|
25
|
+
* `tests/helpers/management-route-scan.ts` reads that form as a route declaration, and this file
|
|
26
|
+
* declares no route of its own.
|
|
27
|
+
*/
|
|
28
|
+
export const SIBLING_REFUSED_MANAGEMENT_PATHS: readonly { readonly path: string; readonly children: boolean }[] = [
|
|
29
|
+
{ path: "/api/sync", children: false },
|
|
30
|
+
{ path: "/api/client-integrations", children: true },
|
|
31
|
+
{ path: "/api/native-integrations", children: true },
|
|
32
|
+
{ path: "/api/claude-desktop", children: true },
|
|
33
|
+
{ path: "/api/claude-code", children: false },
|
|
34
|
+
{ path: "/api/grok/apply", children: false },
|
|
35
|
+
{ path: "/api/grok/selection", children: false },
|
|
36
|
+
{ path: "/api/codex-prompt", children: true },
|
|
37
|
+
{ path: CODEX_RESTART_PATH, children: false },
|
|
38
|
+
{ path: "/api/link/join", children: false },
|
|
39
|
+
{ path: "/api/native-main-profiles", children: true },
|
|
40
|
+
{ path: "/api/codex-auth/main", children: true },
|
|
41
|
+
{ path: "/api/startup-action", children: false },
|
|
42
|
+
{ path: "/api/windows-tray", children: false },
|
|
43
|
+
{ path: "/api/update/run", children: false },
|
|
44
|
+
{ path: "/api/v2", children: false },
|
|
45
|
+
{ path: "/api/codex-auth/features", children: true },
|
|
46
|
+
{ path: "/api/storage/codex-logs", children: true },
|
|
47
|
+
// Archived-session cleanup, its policy run and trash restore rewrite the shared CODEX_HOME.
|
|
48
|
+
{ path: "/api/storage/cleanup", children: false },
|
|
49
|
+
{ path: "/api/storage/cleanup-policy/run", children: false },
|
|
50
|
+
{ path: "/api/storage/trash/restore", children: false },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const READ_METHODS: ReadonlySet<string> = new Set(["GET", "HEAD"]);
|
|
54
|
+
|
|
55
|
+
/** True when this process is a sibling and the request would mutate shared client state. */
|
|
56
|
+
export function siblingRefusesManagementRequest(method: string, pathname: string): boolean {
|
|
57
|
+
if (siblingOfLivePort() === null || READ_METHODS.has(method.toUpperCase())) return false;
|
|
58
|
+
return SIBLING_REFUSED_MANAGEMENT_PATHS.some(entry =>
|
|
59
|
+
pathname === entry.path || (entry.children && pathname.startsWith(`${entry.path}/`)));
|
|
60
|
+
}
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
type CodexLogGuardMutationResult,
|
|
12
12
|
type CodexLogGuardStatus,
|
|
13
13
|
} from "../../codex/log-guard/protection";
|
|
14
|
-
import {
|
|
14
|
+
import { scanStorageAsync, type StorageReport } from "../../storage/scanner";
|
|
15
|
+
import { noteStorageMutationCompleted, storageMutationEpoch } from "../../storage/storage-mutation-coordinator";
|
|
15
16
|
import { jsonResponse } from "../auth-cors";
|
|
16
17
|
import {
|
|
17
18
|
managementBodyTooLargeResponse,
|
|
@@ -104,6 +105,67 @@ async function readProtectMode(ctx: ManagementContext): Promise<"compat" | "quie
|
|
|
104
105
|
return mode;
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Concurrent GET /api/storage requests for the same CODEX_HOME share one walk; nothing is
|
|
110
|
+
* cached past it. A walk is joined only while no storage mutation has completed since it
|
|
111
|
+
* started, so a read issued after a cleanup, restore, policy run or Log Guard change always
|
|
112
|
+
* rescans instead of receiving sizes observed before it.
|
|
113
|
+
*/
|
|
114
|
+
interface StorageScanFlight {
|
|
115
|
+
epoch: number;
|
|
116
|
+
promise: Promise<StorageReport>;
|
|
117
|
+
}
|
|
118
|
+
const storageScanFlights = new Map<string, StorageScanFlight>();
|
|
119
|
+
|
|
120
|
+
export function sharedStorageScan(
|
|
121
|
+
codexHome: string,
|
|
122
|
+
scan: (codexHome: string) => Promise<StorageReport> = scanStorageAsync,
|
|
123
|
+
): Promise<StorageReport> {
|
|
124
|
+
const epoch = storageMutationEpoch();
|
|
125
|
+
const pending = storageScanFlights.get(codexHome);
|
|
126
|
+
if (pending && pending.epoch === epoch) return pending.promise;
|
|
127
|
+
const flight: StorageScanFlight = {
|
|
128
|
+
epoch,
|
|
129
|
+
promise: scan(codexHome).finally(() => {
|
|
130
|
+
// A newer flight may already own this key; only retire our own entry.
|
|
131
|
+
if (storageScanFlights.get(codexHome) === flight) storageScanFlights.delete(codexHome);
|
|
132
|
+
}),
|
|
133
|
+
};
|
|
134
|
+
storageScanFlights.set(codexHome, flight);
|
|
135
|
+
return flight.promise;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Log Guard results that are refused before anything under CODEX_HOME is touched. Anything
|
|
140
|
+
* else (success, a database or config write failure, `busy` which compaction can report
|
|
141
|
+
* after partial progress, a post-compaction integrity failure) may have changed storage.
|
|
142
|
+
*/
|
|
143
|
+
const REFUSED_BEFORE_CHANGE: ReadonlySet<string> = new Set([
|
|
144
|
+
"unsupported_schema",
|
|
145
|
+
"codex_running",
|
|
146
|
+
"process_enumeration_failed",
|
|
147
|
+
"unsafe_path",
|
|
148
|
+
"trigger_collision",
|
|
149
|
+
"auto_vacuum_not_incremental",
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
export function logGuardResultMayHaveChangedStorage(
|
|
153
|
+
result: CodexLogGuardMutationResult | CodexLogGuardCompactionResult,
|
|
154
|
+
): boolean {
|
|
155
|
+
if (result.ok) return true;
|
|
156
|
+
if (result.error === "integrity_check_failed") return result.phase === "after";
|
|
157
|
+
return !REFUSED_BEFORE_CHANGE.has(result.error);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Invalidate in-flight storage scans only when the Log Guard operation may have changed storage. */
|
|
161
|
+
function afterLogGuardMutation(
|
|
162
|
+
result: CodexLogGuardMutationResult | CodexLogGuardCompactionResult,
|
|
163
|
+
response: Response,
|
|
164
|
+
): Response {
|
|
165
|
+
if (logGuardResultMayHaveChangedStorage(result)) noteStorageMutationCompleted();
|
|
166
|
+
return response;
|
|
167
|
+
}
|
|
168
|
+
|
|
107
169
|
/** Codex Log Guard diagnostics plus explicit protection and maintenance mutations. */
|
|
108
170
|
export async function handleStorageLogGuardRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
109
171
|
const { req, url, config, deps } = ctx;
|
|
@@ -129,22 +191,26 @@ export async function handleStorageLogGuardRoutes(ctx: ManagementContext): Promi
|
|
|
129
191
|
if (req.method !== "POST") return null;
|
|
130
192
|
const mode = await readProtectMode(ctx);
|
|
131
193
|
if (mode instanceof Response) return mode;
|
|
132
|
-
|
|
194
|
+
const result = protectCodexLogs(mode, protectionDeps);
|
|
195
|
+
return afterLogGuardMutation(result, mutationResponse(result, ctx));
|
|
133
196
|
}
|
|
134
197
|
|
|
135
198
|
if (url.pathname === "/api/storage/codex-logs/unprotect") {
|
|
136
199
|
if (req.method !== "POST") return null;
|
|
137
|
-
|
|
200
|
+
const result = unprotectCodexLogs(protectionDeps);
|
|
201
|
+
return afterLogGuardMutation(result, mutationResponse(result, ctx));
|
|
138
202
|
}
|
|
139
203
|
|
|
140
204
|
if (url.pathname === "/api/storage/codex-logs/repair") {
|
|
141
205
|
if (req.method !== "POST") return null;
|
|
142
|
-
|
|
206
|
+
const result = repairCodexLogGuardProtection(protectionDeps);
|
|
207
|
+
return afterLogGuardMutation(result, mutationResponse(result, ctx));
|
|
143
208
|
}
|
|
144
209
|
|
|
145
210
|
if (url.pathname === "/api/storage/codex-logs/compact") {
|
|
146
211
|
if (req.method !== "POST") return null;
|
|
147
|
-
|
|
212
|
+
const result = compactCodexLogs(deps.codexLogGuardMaintenanceDeps);
|
|
213
|
+
return afterLogGuardMutation(result, compactResponse(result, ctx));
|
|
148
214
|
}
|
|
149
215
|
|
|
150
216
|
if (url.pathname !== "/api/storage" || req.method !== "GET") return null;
|
|
@@ -154,7 +220,7 @@ export async function handleStorageLogGuardRoutes(ctx: ManagementContext): Promi
|
|
|
154
220
|
// silently folded into CODEX_HOME totals.
|
|
155
221
|
let storage;
|
|
156
222
|
try {
|
|
157
|
-
storage =
|
|
223
|
+
storage = await sharedStorageScan(resolveCodexHomeDir());
|
|
158
224
|
} catch {
|
|
159
225
|
const fallback = {
|
|
160
226
|
codexHome: resolveCodexHomeDir(),
|
|
@@ -14,14 +14,28 @@
|
|
|
14
14
|
* Installed-but-stale/missing service assets are NOT treated as supervised —
|
|
15
15
|
* exit(1) would leave the proxy dead with `Service: installed, stale or missing
|
|
16
16
|
* service assets` and a /healthz timeout.
|
|
17
|
-
* -
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* - The replacement is spawned by `src/server/restart-replacement.ts`: it carries
|
|
18
|
+
* `OCX_RESTART_PARENT_PID` and its output goes to the bounded `restart-handoff.log`.
|
|
19
|
+
* Only after a completed drain (the handoff waits for health) is a replacement that
|
|
20
|
+
* exits before it answers retried, twice, inside the readiness budget. A deadline,
|
|
21
|
+
* failed-drain or listener-stop-fallback handoff resolves on spawn and exits: the
|
|
22
|
+
* replacement's own parent wait and port reclaim cover it, and any other early exit
|
|
23
|
+
* there is not retried.
|
|
24
|
+
* - If detached spawn fails (sync throw, pre-start `error`, or every attempt exited
|
|
25
|
+
* early): exit(1) without markRecycling — after drain the listen socket is already
|
|
26
|
+
* closed, so a latch reset cannot recover serving. Clear inherited `OCX_SERVICE`
|
|
27
|
+
* so exit cleanup can restore Codex/Grok fences when a stale service marker has no
|
|
28
|
+
* viable supervisor. Log only a stable errno code — never the raw message
|
|
22
29
|
* (paths in ENOENT often include the OS username).
|
|
30
|
+
* - Except after a committed client connection (a join into a Child): Codex already
|
|
31
|
+
* routes to the client runtime the next start serves on this port, so the failed
|
|
32
|
+
* handoff still marks recycling and exit cleanup keeps that routing instead of
|
|
33
|
+
* silently falling back to native Codex.
|
|
34
|
+
* - Desktop-supervised child (the desktop app spawned it with `OCX_DESKTOP_SUPERVISED=1`
|
|
35
|
+
* and is still its parent): no spawn. Mark recycle and exit 75; the app sees the exit
|
|
36
|
+
* and starts the replacement itself, so it keeps owning, stopping and quitting it.
|
|
37
|
+
* Checked before the service rule: that app, not a service manager, is the parent.
|
|
23
38
|
*/
|
|
24
|
-
import { spawn } from "node:child_process";
|
|
25
39
|
import {
|
|
26
40
|
acquireTemporaryDrain,
|
|
27
41
|
beginShutdownDrain,
|
|
@@ -34,25 +48,22 @@ import {
|
|
|
34
48
|
stopServerListener,
|
|
35
49
|
} from "../lifecycle";
|
|
36
50
|
import { isServiceViable } from "../../service";
|
|
51
|
+
import { readClientConnectionState } from "../../client/state";
|
|
52
|
+
import { withSiblingMarker } from "../../codex/sibling-start";
|
|
53
|
+
import { issueSiblingHandoff } from "../../codex/sibling-handoff";
|
|
37
54
|
import { readRuntimePort } from "../../config/process-state";
|
|
38
|
-
import { withProcessRuntimeProvenance } from "../../lib/bun-runtime";
|
|
39
|
-
import { selfLaunchArgv } from "../../lib/self-launch-argv";
|
|
40
55
|
import { spendLedgerRestartEnvironment } from "../../lib/spend-ledger-owner";
|
|
41
56
|
import {
|
|
57
|
+
DESKTOP_RESTART_EXIT_CODE,
|
|
58
|
+
DESKTOP_SUPERVISED_ENV,
|
|
42
59
|
MEMORY_DRAIN_RESTART_MS,
|
|
43
|
-
|
|
60
|
+
isDesktopSupervised,
|
|
44
61
|
} from "../../lib/system-restart-contract";
|
|
45
|
-
import {
|
|
62
|
+
import { spawnReplacementStart } from "../restart-replacement";
|
|
46
63
|
|
|
47
64
|
export { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../../lib/system-restart-contract";
|
|
65
|
+
export { waitForReplacementReady, type ReplacementReadinessIo } from "../restart-replacement";
|
|
48
66
|
export const DEADLINE_LISTENER_STOP_TIMEOUT_MS = 5_000;
|
|
49
|
-
const REPLACEMENT_READY_POLL_MS = 150;
|
|
50
|
-
|
|
51
|
-
export interface ReplacementReadinessIo {
|
|
52
|
-
findLive?: typeof findLiveProxy;
|
|
53
|
-
now?: () => number;
|
|
54
|
-
sleep?: (ms: number) => Promise<void>;
|
|
55
|
-
}
|
|
56
67
|
|
|
57
68
|
export interface SystemRestartIo {
|
|
58
69
|
acquireTemporaryDrain?: () => { release(): void } | null;
|
|
@@ -60,11 +71,15 @@ export interface SystemRestartIo {
|
|
|
60
71
|
/** True when a background service can actually respawn this process after exit(1). */
|
|
61
72
|
isServiceViable?: () => boolean;
|
|
62
73
|
isSupervisedServiceChild?: () => boolean;
|
|
74
|
+
/** True when the desktop app that spawned this process starts its replacement after exit 75. */
|
|
75
|
+
isDesktopSupervised?: () => boolean;
|
|
63
76
|
/** Ordinary start; deadline handoff may defer health until parent exit releases OS locks. */
|
|
64
77
|
spawnStart?: (port?: number, waitForHealthBeforeParentExit?: boolean) => void | Promise<void>;
|
|
65
78
|
/** Idempotent listener close; must settle before an ordinary start is spawned. */
|
|
66
79
|
stopListener?: () => void | Promise<void>;
|
|
67
80
|
markRecycling?: () => void;
|
|
81
|
+
/** True once a client connection has committed (a join into a Child); read only after a failed handoff. */
|
|
82
|
+
isClientConnected?: () => boolean;
|
|
68
83
|
exitProcess?: (code: number) => void;
|
|
69
84
|
schedule?: (fn: () => void | Promise<void>, ms: number) => void;
|
|
70
85
|
scheduleDeadline?: (fn: () => void, ms: number) => () => void;
|
|
@@ -161,7 +176,8 @@ export function setSystemRestartIoForTests(io: SystemRestartIo = {}): void {
|
|
|
161
176
|
explicitShutdownRequested = false;
|
|
162
177
|
}
|
|
163
178
|
|
|
164
|
-
|
|
179
|
+
/** The port this process is listening on, or undefined when that cannot be established. */
|
|
180
|
+
export function resolveListenPort(): number | undefined {
|
|
165
181
|
const live = getServerListenPort();
|
|
166
182
|
if (live) return live;
|
|
167
183
|
const runtime = readRuntimePort(process.pid);
|
|
@@ -185,131 +201,67 @@ function spawnFailureCode(err: unknown): string {
|
|
|
185
201
|
return "spawn_failed";
|
|
186
202
|
}
|
|
187
203
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
204
|
+
/**
|
|
205
|
+
* After a failed handoff, keep Codex routing when a client connection has committed.
|
|
206
|
+
*
|
|
207
|
+
* `connectClient` already pointed Codex at the client runtime that the next start (a service
|
|
208
|
+
* relaunch, the desktop app, or `ocx start`) serves on this same port. Restoring native Codex here
|
|
209
|
+
* would be a silent local fallback while client state says connected. An unreadable state is not
|
|
210
|
+
* proof of a join, so it keeps today's restore.
|
|
211
|
+
*/
|
|
212
|
+
function keepRoutingForCommittedClient(io: SystemRestartIo): void {
|
|
213
|
+
let connected = false;
|
|
214
|
+
try {
|
|
215
|
+
connected = (io.isClientConnected ?? (() => readClientConnectionState().kind === "connected"))();
|
|
216
|
+
} catch {
|
|
217
|
+
connected = false;
|
|
218
|
+
}
|
|
219
|
+
if (connected) (io.markRecycling ?? markRecyclingForExit)();
|
|
192
220
|
}
|
|
193
221
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
):
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
// A not-yet-bound replacement is indistinguishable from a transient
|
|
225
|
-
// liveness failure here; keep polling inside the one bounded window.
|
|
226
|
-
}
|
|
227
|
-
const remainingMs = deadline - now();
|
|
228
|
-
if (remainingMs <= 0) break;
|
|
229
|
-
await sleep(Math.min(REPLACEMENT_READY_POLL_MS, remainingMs));
|
|
230
|
-
}
|
|
231
|
-
return false;
|
|
222
|
+
/**
|
|
223
|
+
* Hand the restart to the desktop app that spawned this process: keep Codex routing for the
|
|
224
|
+
* replacement it starts on this port, and exit with the code it restarts on. False when nothing
|
|
225
|
+
* supervises this process that way, and then nothing happened.
|
|
226
|
+
*/
|
|
227
|
+
function handOffToDesktop(io: SystemRestartIo, exitProcess: (code: number) => void): boolean {
|
|
228
|
+
if (!(io.isDesktopSupervised ?? isDesktopSupervised)()) return false;
|
|
229
|
+
(io.markRecycling ?? markRecyclingForExit)();
|
|
230
|
+
exitProcess(DESKTOP_RESTART_EXIT_CODE);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The replacement's environment before `spawnReplacementStart` adds the restart-parent marker and
|
|
236
|
+
* the runtime provenance: never under a service marker, a sibling's replacement stays a sibling,
|
|
237
|
+
* and only a parent-exit handoff marks the bounded spend-ledger lease wait.
|
|
238
|
+
*/
|
|
239
|
+
export function replacementStartEnvironment(
|
|
240
|
+
waitForHealthBeforeParentExit: boolean,
|
|
241
|
+
parentPid: number = process.pid,
|
|
242
|
+
): NodeJS.ProcessEnv {
|
|
243
|
+
// A sibling's replacement stays a sibling even if the owner is down while it probes.
|
|
244
|
+
const sourceEnv: NodeJS.ProcessEnv = withSiblingMarker(process.env, issueSiblingHandoff);
|
|
245
|
+
delete sourceEnv.OCX_SERVICE;
|
|
246
|
+
// A detached replacement is not the desktop app's child; it must never exit to an app that is not waiting on it.
|
|
247
|
+
delete sourceEnv[DESKTOP_SUPERVISED_ENV];
|
|
248
|
+
return spendLedgerRestartEnvironment(
|
|
249
|
+
sourceEnv,
|
|
250
|
+
waitForHealthBeforeParentExit ? undefined : parentPid,
|
|
251
|
+
);
|
|
232
252
|
}
|
|
233
253
|
|
|
234
254
|
function spawnDetachedStart(
|
|
235
255
|
port?: number,
|
|
236
256
|
waitForHealthBeforeParentExit = true,
|
|
237
257
|
): Promise<void> {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
args.push("--port", String(expectedPort));
|
|
258
|
+
let env: NodeJS.ProcessEnv;
|
|
259
|
+
try {
|
|
260
|
+
env = replacementStartEnvironment(waitForHealthBeforeParentExit);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
return Promise.reject(err);
|
|
244
263
|
}
|
|
245
|
-
|
|
246
|
-
return new Promise<void>((resolve, reject) => {
|
|
247
|
-
let child: ReturnType<typeof spawn>;
|
|
248
|
-
try {
|
|
249
|
-
const sourceEnv: NodeJS.ProcessEnv = { ...process.env };
|
|
250
|
-
delete sourceEnv.OCX_SERVICE;
|
|
251
|
-
const env = spendLedgerRestartEnvironment(
|
|
252
|
-
sourceEnv,
|
|
253
|
-
waitForHealthBeforeParentExit ? undefined : process.pid,
|
|
254
|
-
);
|
|
255
|
-
child = spawn(process.execPath, launchArgs, {
|
|
256
|
-
detached: true,
|
|
257
|
-
stdio: "ignore",
|
|
258
|
-
windowsHide: true,
|
|
259
|
-
env: withProcessRuntimeProvenance(env),
|
|
260
|
-
});
|
|
261
|
-
} catch (err) {
|
|
262
|
-
reject(err);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
let settled = false;
|
|
266
|
-
const cleanup = () => {
|
|
267
|
-
child.off("error", onError);
|
|
268
|
-
child.off("exit", onExit);
|
|
269
|
-
child.off("spawn", onSpawn);
|
|
270
|
-
};
|
|
271
|
-
const finish = (error?: unknown) => {
|
|
272
|
-
if (settled) return;
|
|
273
|
-
settled = true;
|
|
274
|
-
cleanup();
|
|
275
|
-
if (error !== undefined) {
|
|
276
|
-
if (child.exitCode === null && child.signalCode === null) {
|
|
277
|
-
try { child.kill(); } catch { /* best-effort failed-start cleanup */ }
|
|
278
|
-
}
|
|
279
|
-
try { child.unref(); } catch { /* best-effort */ }
|
|
280
|
-
reject(error);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
child.unref();
|
|
284
|
-
resolve();
|
|
285
|
-
};
|
|
286
|
-
const onError = (err: Error) => { finish(err); };
|
|
287
|
-
const onExit = () => { finish(handoffError("child_exit")); };
|
|
288
|
-
const onSpawn = () => {
|
|
289
|
-
if (!waitForHealthBeforeParentExit) {
|
|
290
|
-
// A deadline may have been caused by native-main ownership cleanup.
|
|
291
|
-
// Let the ordinary child survive parent exit, which releases those OS locks.
|
|
292
|
-
finish();
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
void waitForReplacementReady(child.pid, process.pid, expectedPort).then(
|
|
296
|
-
ready => {
|
|
297
|
-
if (!ready) {
|
|
298
|
-
console.warn(
|
|
299
|
-
"Drain-and-restart replacement is still alive after the readiness window; allowing it to continue after parent exit",
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
// Never kill a live ordinary start at its valid reclaim boundary. Parent
|
|
303
|
-
// exit is the final resource release the child may still be waiting for.
|
|
304
|
-
finish();
|
|
305
|
-
},
|
|
306
|
-
err => finish(err),
|
|
307
|
-
);
|
|
308
|
-
};
|
|
309
|
-
child.once("error", onError);
|
|
310
|
-
child.once("exit", onExit);
|
|
311
|
-
child.once("spawn", onSpawn);
|
|
312
|
-
});
|
|
264
|
+
return spawnReplacementStart({ port, waitForHealth: waitForHealthBeforeParentExit, env });
|
|
313
265
|
}
|
|
314
266
|
|
|
315
267
|
async function completeDeferredParentExitHandoff(
|
|
@@ -327,6 +279,7 @@ async function completeDeferredParentExitHandoff(
|
|
|
327
279
|
`Drain-and-restart ${phase} spawn failed (${spawnFailureCode(err)}); exiting without replacement`,
|
|
328
280
|
);
|
|
329
281
|
delete process.env.OCX_SERVICE;
|
|
282
|
+
keepRoutingForCommittedClient(io);
|
|
330
283
|
exitProcess(1);
|
|
331
284
|
return;
|
|
332
285
|
}
|
|
@@ -342,6 +295,7 @@ async function completeDeadlineRestartHandoff(
|
|
|
342
295
|
canHandoff: () => boolean = () => true,
|
|
343
296
|
): Promise<void> {
|
|
344
297
|
if (!canHandoff()) return;
|
|
298
|
+
if (handOffToDesktop(io, exitProcess)) return;
|
|
345
299
|
const supervised = (io.isSupervisedServiceChild ?? (() => isSupervisedServiceChild(io)))();
|
|
346
300
|
if (supervised) {
|
|
347
301
|
// Failure-only supervisors ignore exit(0); intentional non-zero triggers respawn.
|
|
@@ -455,6 +409,7 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission:
|
|
|
455
409
|
// rejects, an accepted restart must still reach replacement or terminal exit.
|
|
456
410
|
console.warn("Drain-and-restart cleanup failed; continuing terminal restart handoff");
|
|
457
411
|
}
|
|
412
|
+
if (handOffToDesktop(io, exitProcess)) return;
|
|
458
413
|
const supervised = (io.isSupervisedServiceChild ?? (() => isSupervisedServiceChild(io)))();
|
|
459
414
|
if (supervised) {
|
|
460
415
|
// Failure-only supervisors ignore exit(0); intentional non-zero triggers respawn.
|
|
@@ -487,8 +442,10 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission:
|
|
|
487
442
|
);
|
|
488
443
|
// Listen socket is already stopped; do not markRecycling — no child to inherit fences.
|
|
489
444
|
// No replacement inherited the routing. Clear a stale service marker so
|
|
490
|
-
// this unsupervised parent restores clients after the failed handoff
|
|
445
|
+
// this unsupervised parent restores clients after the failed handoff — unless a
|
|
446
|
+
// committed client connection already owns that routing.
|
|
491
447
|
delete process.env.OCX_SERVICE;
|
|
448
|
+
keepRoutingForCommittedClient(io);
|
|
492
449
|
exitProcess(1);
|
|
493
450
|
return;
|
|
494
451
|
}
|
|
@@ -217,7 +217,7 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
217
217
|
};
|
|
218
218
|
}
|
|
219
219
|
if (req.method === "GET") {
|
|
220
|
-
return jsonResponse(service.readState(), 200, req, config);
|
|
220
|
+
return jsonResponse(await service.readState(), 200, req, config);
|
|
221
221
|
}
|
|
222
222
|
return jsonResponse(await service.performRestart(), 200, req, config);
|
|
223
223
|
}
|
|
@@ -88,7 +88,11 @@ import type { CatalogDisposition, ConvergeCodex } from "../codex/convergence-typ
|
|
|
88
88
|
import { normalizeCatalogDisposition } from "../codex/catalog-refresh-status";
|
|
89
89
|
import { managementBodyTooLargeResponse } from "./management/body";
|
|
90
90
|
import { handleSessionRoutes } from "./management/session-routes";
|
|
91
|
+
import { siblingRefusesManagementRequest } from "./management/sibling-guard";
|
|
92
|
+
import { siblingOfLivePort, siblingSkipMessage } from "../codex/sibling-start";
|
|
91
93
|
import { packageVersion } from "../lib/package-version";
|
|
94
|
+
import { isLocalAccountSwitchPath } from "../lib/local-account-switch-capability";
|
|
95
|
+
import { readVerifiedAccountSwitchBody } from "./local-account-switch-auth";
|
|
92
96
|
|
|
93
97
|
// installed npm version instead of a stale hardcode.
|
|
94
98
|
const MANAGEMENT_VERSION_FALLBACK = "0.0.0";
|
|
@@ -213,6 +217,19 @@ export async function handleManagementAPI(
|
|
|
213
217
|
if (!isAllowedManagementOrigin(req, config)) {
|
|
214
218
|
return jsonResponse({ error: "cross-origin request blocked" }, 403, req, config);
|
|
215
219
|
}
|
|
220
|
+
if (principal === "local-account-switch-capability") {
|
|
221
|
+
if (req.method !== "PUT" || !isLocalAccountSwitchPath(url.pathname) || url.search !== "") {
|
|
222
|
+
return jsonResponse({ error: "account switch capability scope mismatch" }, 403, req, config);
|
|
223
|
+
}
|
|
224
|
+
if (req.headers.has("content-encoding")) {
|
|
225
|
+
return jsonResponse({ error: "content encoding is not supported" }, 415, req, config);
|
|
226
|
+
}
|
|
227
|
+
const verified = await readVerifiedAccountSwitchBody(req);
|
|
228
|
+
if (verified.status !== 200) {
|
|
229
|
+
return jsonResponse({ error: "account switch body rejected" }, verified.status, req, config);
|
|
230
|
+
}
|
|
231
|
+
req = new Request(req.url, { method: req.method, headers: req.headers, body: Buffer.from(verified.body) });
|
|
232
|
+
}
|
|
216
233
|
// Management bodies are small JSON (provider names, key ids, settings). Reject oversized
|
|
217
234
|
// payloads before any handler buffers them — the data plane has its own decompression cap.
|
|
218
235
|
if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
|
|
@@ -297,6 +314,10 @@ export async function handleManagementAPI(
|
|
|
297
314
|
guiSessionIssuance: requestIngress.guiSessionIssuance ?? null,
|
|
298
315
|
convergeCodexCatalog, syncClaudeAgentDefsBestEffort,
|
|
299
316
|
};
|
|
317
|
+
// Before any route module, including the link, native-main and codex-auth dispatch below.
|
|
318
|
+
if (siblingRefusesManagementRequest(req.method, url.pathname)) {
|
|
319
|
+
return jsonResponse({ error: siblingSkipMessage(), code: "sibling_instance" }, 409, req, config);
|
|
320
|
+
}
|
|
300
321
|
let routed: Response | null | undefined;
|
|
301
322
|
try {
|
|
302
323
|
routed = handleSessionRoutes(ctx)
|
|
@@ -364,9 +385,16 @@ export async function handleManagementAPI(
|
|
|
364
385
|
// outcome. This process cannot verify its own post-exit respawn window; only the
|
|
365
386
|
// receipt-backed parent `ocx stop` can, which is what the deferral exists for.
|
|
366
387
|
const { deferralMatchesReceipt } = await import("../config/pending-teardown");
|
|
367
|
-
const { deferralHonored, performStopTeardown } = await import("./stop-teardown");
|
|
388
|
+
const { deferralHonored, desktopSupervisedStopRefusal, performStopTeardown } = await import("./stop-teardown");
|
|
389
|
+
// The desktop app would start this proxy again within seconds; refuse before anything is
|
|
390
|
+
// touched and point at its tray, whose Stop it honours (#3008 refuses an undone stop the same way).
|
|
391
|
+
const desktopRefusal = desktopSupervisedStopRefusal(principal);
|
|
392
|
+
if (desktopRefusal) return jsonResponse(desktopRefusal, 409, req, config);
|
|
368
393
|
const holdsReceipt = deferralHonored(url, deferralMatchesReceipt);
|
|
369
|
-
|
|
394
|
+
// A sibling never runs under a service manager, and the installed service is the live
|
|
395
|
+
// owner's: asking the manager to stop from here would refuse, or boot the owner's job out.
|
|
396
|
+
const sibling = siblingOfLivePort() !== null;
|
|
397
|
+
const respawnRisk = holdsReceipt || sibling ? "none" : installedServiceRespawnRisk();
|
|
370
398
|
if (respawnRisk === "respawnable") {
|
|
371
399
|
return jsonResponse({
|
|
372
400
|
success: false,
|
|
@@ -400,7 +428,7 @@ export async function handleManagementAPI(
|
|
|
400
428
|
}
|
|
401
429
|
let serviceStop: import("../service").ServiceStopOutcome;
|
|
402
430
|
try {
|
|
403
|
-
serviceStop = stopServiceIfInstalledDetailed();
|
|
431
|
+
serviceStop = sibling ? "absent" : stopServiceIfInstalledDetailed();
|
|
404
432
|
} catch (err) {
|
|
405
433
|
if (isServiceOwnershipError(err)) {
|
|
406
434
|
// The installed service belongs to another CODEX_HOME/OPENCODEX_HOME: it would respawn
|
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
type GuiSessionRequestContext,
|
|
73
73
|
} from "./gui-session";
|
|
74
74
|
import { hasLocalDesktopSnapshotCapability } from "./local-desktop-snapshot-auth";
|
|
75
|
+
import { hasLocalAccountSwitchCapability } from "./local-account-switch-auth";
|
|
75
76
|
export type { GuiSessionBootstrap, GuiSessionRequestContext } from "./gui-session";
|
|
76
77
|
|
|
77
78
|
const LOCAL_READ_REPLAY_LIMIT = 256;
|
|
@@ -337,6 +338,7 @@ export type ManagementPrincipal =
|
|
|
337
338
|
| "gui-pair-capability"
|
|
338
339
|
| "local-read-capability"
|
|
339
340
|
| "local-desktop-snapshot-capability"
|
|
341
|
+
| "local-account-switch-capability"
|
|
340
342
|
| "local-provider-reload-capability"
|
|
341
343
|
| "local-aside-sync-capability"
|
|
342
344
|
| "system-restart-capability";
|
|
@@ -564,6 +566,7 @@ function resolveManagementAdmission(
|
|
|
564
566
|
else if (hasLocalAsideSyncCapability(req, local)) principal = "local-aside-sync-capability";
|
|
565
567
|
else if (hasLocalProviderReloadCapability(req, local)) principal = "local-provider-reload-capability";
|
|
566
568
|
else if (hasLocalDesktopSnapshotCapability(req, local)) principal = "local-desktop-snapshot-capability";
|
|
569
|
+
else if (hasLocalAccountSwitchCapability(req, local)) principal = "local-account-switch-capability";
|
|
567
570
|
else if (hasLocalReadCapability(req, local)) principal = "local-read-capability";
|
|
568
571
|
else if (hasGuiPairCapability(req, local)) principal = "gui-pair-capability";
|
|
569
572
|
else if (state.available) {
|