@bitkyc08/opencodex 2.80.0 → 2.82.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/README.md +3 -3
- package/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
- package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/index-D3FBCNiP.js +94 -0
- package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +8 -3
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +19 -17
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +78 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +18 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +15 -1
- package/src/cli/doctor.ts +16 -3
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +25 -14
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +48 -6
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/cli/resolve.ts +25 -2
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +11 -49
- package/src/codex/auth-context.ts +20 -1
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +33 -7
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +37 -6
- package/src/codex/catalog/gather-capture.ts +10 -3
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +58 -44
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/control-plane-models.ts +6 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -219
- package/src/combos/types.ts +1 -1
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/diagnostics.ts +27 -16
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +42 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +11 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +566 -298
- package/src/generated/model-metadata.ts +7 -6
- package/src/grok/inject.ts +50 -4
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +30 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +19 -3
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +17 -5
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +244 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +90 -18
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +96 -175
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +68 -10
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +23 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +23 -33
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +20 -3
- package/src/responses/state/metrics.ts +95 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +13 -5
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +68 -76
- package/src/responses/task-input.ts +17 -0
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native.ts +64 -28
- package/src/server/claude-messages.ts +59 -8
- package/src/server/gui-static.ts +38 -4
- package/src/server/inbound-body-admission.ts +1 -0
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/serve-options.ts +10 -2
- package/src/server/index/spend-ledger-lifecycle.ts +12 -5
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +5 -2
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +123 -38
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +85 -14
- package/src/server/management/context.ts +2 -2
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/shared.ts +3 -3
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +91 -45
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +133 -38
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log.ts +36 -2
- package/src/server/responses/adapter-continuation.ts +31 -19
- package/src/server/responses/adapter-dispatch.ts +75 -24
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +24 -4
- package/src/server/responses/core-combo.ts +38 -25
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +8 -1
- package/src/server/responses/core-replay.ts +78 -0
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/passthrough-delivery.ts +30 -4
- package/src/server/responses/passthrough-dispatch.ts +39 -9
- package/src/server/responses/request-prepare.ts +41 -5
- package/src/server/responses/request-send-budget.ts +150 -50
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +13 -3
- package/src/server/responses/sidecar-execution.ts +113 -22
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +33 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +33 -28
- package/src/types/provider.ts +5 -0
- package/src/types/request.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types/wire.ts +1 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -7
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-MbDVfUeC.js +0 -54
- package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
- package/gui/dist/assets/index-UJt6npXk.js +0 -94
- package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
|
@@ -8,7 +8,7 @@ import { readIntegrationState, type IntegrationState, type IntegrationStatus } f
|
|
|
8
8
|
import {
|
|
9
9
|
applyIntegrationCoordinated, disableIntegrationCoordinated,
|
|
10
10
|
overwriteIntegrationCoordinated, refreshIntegrationCoordinated,
|
|
11
|
-
type IntegrationWriteInput,
|
|
11
|
+
type CoordinatedIntegrationOptions, type IntegrationWriteInput,
|
|
12
12
|
} from "./writer";
|
|
13
13
|
import {
|
|
14
14
|
asideProfileEnabled, asideProfileFailure, asideProfileScope, asideWriteInput,
|
|
@@ -260,7 +260,10 @@ export function mutateAsideProfiles(
|
|
|
260
260
|
});
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
export function refreshAsideProfiles(
|
|
263
|
+
export function refreshAsideProfiles(
|
|
264
|
+
input: AsideProfilesInput,
|
|
265
|
+
options?: { refreshOnly?: boolean; guard?: CoordinatedIntegrationOptions["guard"] },
|
|
266
|
+
): Promise<Array<OwnedIntegrationRefreshOutcome & { profileId: number }>> {
|
|
264
267
|
const policy = input.config.asideProfileSync;
|
|
265
268
|
const selected = Object.values(policy?.profiles ?? {}).some(enabled => enabled === true);
|
|
266
269
|
if (!selected && (policy?.allProfiles === false
|
|
@@ -272,11 +275,15 @@ export function refreshAsideProfiles(input: AsideProfilesInput): Promise<Array<O
|
|
|
272
275
|
try {
|
|
273
276
|
const scope = asideProfileScope(ctx, profile);
|
|
274
277
|
const owned = scope.store.readRecords().aside !== undefined;
|
|
278
|
+
if (options?.refreshOnly && !owned) continue;
|
|
275
279
|
const bound = await asideWriteInput(ctx, scope);
|
|
276
280
|
// A surviving ownership record means a removed block stays removed.
|
|
277
281
|
// A newly discovered, enabled profile may receive its first safe apply.
|
|
278
282
|
const operation = owned ? refreshIntegrationCoordinated : applyIntegrationCoordinated;
|
|
279
|
-
const result = await operation(bound, {
|
|
283
|
+
const result = await operation(bound, {
|
|
284
|
+
lockSeams: input.lockSeams,
|
|
285
|
+
...(options?.guard ? { guard: options.guard } : {}),
|
|
286
|
+
});
|
|
280
287
|
outcomes.push({
|
|
281
288
|
client: "aside", profileId: profile.id, ok: result.ok,
|
|
282
289
|
...(result.ok ? { changed: result.changed } : {}),
|
|
@@ -2,6 +2,7 @@ import { siblingOfLivePort } from "../codex/sibling-start";
|
|
|
2
2
|
import { redactSecretString } from "../lib/redact";
|
|
3
3
|
import type { ExportModel } from "../clients/config-export";
|
|
4
4
|
import type { IntegrationClientId } from "./registry";
|
|
5
|
+
import type { CoordinatedIntegrationOptions, WriteOutcome } from "./writer";
|
|
5
6
|
import {
|
|
6
7
|
refreshOwnedIntegration,
|
|
7
8
|
type OwnedIntegrationRefreshInput,
|
|
@@ -11,7 +12,8 @@ import {
|
|
|
11
12
|
/** Refresh only previously connected clients; a refused file never blocks its peers. */
|
|
12
13
|
export async function refreshOwnedCatalogIntegrations(
|
|
13
14
|
input: Omit<OwnedIntegrationRefreshInput, "clientId">,
|
|
14
|
-
clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "droid", "opencode", "kilo"],
|
|
15
|
+
clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo"],
|
|
16
|
+
options: { refreshOnly?: boolean; admit?: () => boolean } = {},
|
|
15
17
|
): Promise<OwnedIntegrationRefreshOutcome[]> {
|
|
16
18
|
// Client files are shared with the live proxy a sibling instance runs beside; their entries
|
|
17
19
|
// point at the owner's port, and refreshing them here would re-point them at this one.
|
|
@@ -20,14 +22,28 @@ export async function refreshOwnedCatalogIntegrations(
|
|
|
20
22
|
const loadModels = () => models ??= Promise.resolve().then(() =>
|
|
21
23
|
typeof input.models === "function" ? input.models() : input.models);
|
|
22
24
|
const outcomes: OwnedIntegrationRefreshOutcome[] = [];
|
|
25
|
+
const admit = options.admit;
|
|
26
|
+
const writeOptions: CoordinatedIntegrationOptions | undefined = admit ? {
|
|
27
|
+
guard: (frozen): WriteOutcome | null => admit() ? null : {
|
|
28
|
+
ok: false, reason: "superseded_store", state: "current", clientId: frozen.clientId,
|
|
29
|
+
message: "Background refresh superseded",
|
|
30
|
+
},
|
|
31
|
+
} : undefined;
|
|
23
32
|
for (const clientId of clientIds) {
|
|
33
|
+
if (admit?.() === false) break;
|
|
24
34
|
try {
|
|
25
35
|
if (clientId === "aside") {
|
|
26
36
|
const { refreshAsideProfiles } = await import("./aside-profiles");
|
|
27
|
-
|
|
37
|
+
const asideOptions = options.refreshOnly === undefined ? writeOptions
|
|
38
|
+
: { ...writeOptions, refreshOnly: options.refreshOnly };
|
|
39
|
+
const bound = { ...input, models: loadModels };
|
|
40
|
+
outcomes.push(...await (asideOptions === undefined
|
|
41
|
+
? refreshAsideProfiles(bound) : refreshAsideProfiles(bound, asideOptions)));
|
|
28
42
|
continue;
|
|
29
43
|
}
|
|
30
|
-
const
|
|
44
|
+
const bound = { ...input, clientId, models: loadModels };
|
|
45
|
+
const result = await (writeOptions === undefined
|
|
46
|
+
? refreshOwnedIntegration(bound) : refreshOwnedIntegration(bound, writeOptions));
|
|
31
47
|
if (result) outcomes.push(result);
|
|
32
48
|
} catch (error) {
|
|
33
49
|
const busy = error !== null && typeof error === "object"
|
|
@@ -149,6 +149,13 @@ const CLIENT_MANAGED_PATHS = {
|
|
|
149
149
|
[...ZCODE_STORE_PROVIDER_RULES_PATH, `[providerId=${OPENCODE_PROVIDER_ID}]`],
|
|
150
150
|
[...ZCODE_STORE_MODEL_RULES_PATH, DYNAMIC_SEGMENT],
|
|
151
151
|
],
|
|
152
|
+
/*
|
|
153
|
+
* Both roots, because the client picks between them rather than reading both:
|
|
154
|
+
* `document.provider ?? document.providers` in `command-code@1.66.0`. The
|
|
155
|
+
* builder chooses the one the target already uses, and a block we wrote under
|
|
156
|
+
* either root has to be removable from either root afterwards.
|
|
157
|
+
*/
|
|
158
|
+
commandcode: [["provider", OPENCODE_PROVIDER_ID], ["providers", OPENCODE_PROVIDER_ID]],
|
|
152
159
|
prime: [["providers", OPENCODE_PROVIDER_ID]],
|
|
153
160
|
aside: [["providers", OPENCODE_PROVIDER_ID]],
|
|
154
161
|
raycast: [["providers", `[id=${OPENCODE_PROVIDER_ID}]`]],
|
|
@@ -58,6 +58,8 @@ import {
|
|
|
58
58
|
DSH_PROFILE_PROVIDER_PATH,
|
|
59
59
|
type BuildContribution,
|
|
60
60
|
type ConfigFormat,
|
|
61
|
+
commandCodeConfigPath,
|
|
62
|
+
commandCodeHomeDir,
|
|
61
63
|
kiloConfigPath,
|
|
62
64
|
kiloHomeDir,
|
|
63
65
|
kiloCandidatePath,
|
|
@@ -369,6 +371,12 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
|
|
|
369
371
|
buildContribution: buildZcodeStoreContribution,
|
|
370
372
|
},
|
|
371
373
|
},
|
|
374
|
+
commandcode: {
|
|
375
|
+
id: "commandcode",
|
|
376
|
+
configPath: (env = process.env, home = homedir()) => commandCodeConfigPath(env, home),
|
|
377
|
+
detectDir: (env = process.env, home = homedir()) => commandCodeHomeDir(env, home),
|
|
378
|
+
writerLock: { suffix: ".lock" },
|
|
379
|
+
},
|
|
372
380
|
prime: {
|
|
373
381
|
id: "prime",
|
|
374
382
|
configPath: (env = process.env, home = homedir()) => primeConfigPath(env, home),
|
|
@@ -490,7 +490,8 @@ export function exportContextOf(input: {
|
|
|
490
490
|
config: OcxConfig;
|
|
491
491
|
port: number;
|
|
492
492
|
droidReasoningDefaults?: Record<string, string>;
|
|
493
|
-
|
|
493
|
+
document?: unknown;
|
|
494
|
+
}): { baseUrl: string; models: readonly ExportModel[]; config: OcxConfig; droidReasoningDefaults?: Record<string, string>; document?: unknown } {
|
|
494
495
|
return {
|
|
495
496
|
/*
|
|
496
497
|
* Composed through the SAME helper `ocx export` uses. Interpolating the
|
|
@@ -505,6 +506,9 @@ export function exportContextOf(input: {
|
|
|
505
506
|
models: input.models,
|
|
506
507
|
config: input.config,
|
|
507
508
|
...(input.droidReasoningDefaults === undefined ? {} : { droidReasoningDefaults: input.droidReasoningDefaults }),
|
|
509
|
+
// Forwarded only when the caller parsed a target: a client that resolves
|
|
510
|
+
// between two document roots (Command Code) needs the bytes on disk.
|
|
511
|
+
...(input.document === undefined ? {} : { document: input.document }),
|
|
508
512
|
};
|
|
509
513
|
}
|
|
510
514
|
|
|
@@ -514,7 +518,10 @@ export function buildIntegrationContribution(
|
|
|
514
518
|
parsed: unknown,
|
|
515
519
|
record: OwnershipRecord | null,
|
|
516
520
|
): ManagedContribution {
|
|
517
|
-
|
|
521
|
+
// The parsed target travels with the context: a client whose reader picks
|
|
522
|
+
// between two document roots (Command Code) must see the bytes on disk, and
|
|
523
|
+
// routing it through this one builder keeps classify, plan and write agreeing.
|
|
524
|
+
const context = exportContextOf({ ...input, document: parsed });
|
|
518
525
|
if (input.clientId === "droid" && input.droidReasoningDefaults === undefined && record?.configPath === effective.configPath) {
|
|
519
526
|
const inherited = recordedDroidContributionMatches(parsed, record)
|
|
520
527
|
? droidDefaultsFromOwnedRows(context, parsed, record.fragmentPaths)
|
|
@@ -18,7 +18,7 @@ import { shouldInjectApiAuthHeader } from "../codex/inject";
|
|
|
18
18
|
import { detachedConfigSnapshot } from "../config/admitted-identity";
|
|
19
19
|
import { copyPlainData } from "../lib/plain-data";
|
|
20
20
|
import type { OcxConfig } from "../types";
|
|
21
|
-
import { defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
21
|
+
import { PARSE_FAILED, defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
22
22
|
import { inspectKiloCandidates } from "./kilo-candidates";
|
|
23
23
|
import {
|
|
24
24
|
fingerprint,
|
|
@@ -727,9 +727,15 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
727
727
|
// exact bytes when the snapshot was taken. Re-deriving it from the file would
|
|
728
728
|
// mean guessing which entries are ours, and a wrong guess deletes a user's.
|
|
729
729
|
const restoredRecord = entry.priorRecord;
|
|
730
|
+
let restoredDocument = clientId === "droid" && restoredText !== null ? parseConfig(restoredText, "json") : undefined;
|
|
731
|
+
if (clientId === "commandcode" && restoredText !== null) {
|
|
732
|
+
const parsed = parseConfig(restoredText, "json");
|
|
733
|
+
// Restoring raw snapshot bytes must remain possible even when they are not parseable.
|
|
734
|
+
restoredDocument = parsed === PARSE_FAILED ? undefined : parsed;
|
|
735
|
+
}
|
|
730
736
|
const fresh = buildIntegrationContribution(
|
|
731
737
|
{ ...input, droidReasoningDefaults: undefined }, rowTarget,
|
|
732
|
-
|
|
738
|
+
restoredDocument,
|
|
733
739
|
restoredRecord,
|
|
734
740
|
);
|
|
735
741
|
/*
|
|
@@ -795,6 +801,11 @@ export interface CoordinatedIntegrationOptions {
|
|
|
795
801
|
* non-null result refuses without writing anything.
|
|
796
802
|
*/
|
|
797
803
|
revalidate?: (frozen: IntegrationWriteInput) => Promise<WriteOutcome | null>;
|
|
804
|
+
/**
|
|
805
|
+
* Synchronous last word, evaluated with no await between it and the transaction (after
|
|
806
|
+
* `revalidate` and any lock acquisition). A non-null result refuses without writing.
|
|
807
|
+
*/
|
|
808
|
+
guard?: (frozen: IntegrationWriteInput) => WriteOutcome | null;
|
|
798
809
|
}
|
|
799
810
|
|
|
800
811
|
/** Freeze all mutable resolution seams before the first lock await. */
|
|
@@ -936,21 +947,22 @@ async function coordinatedWrite(
|
|
|
936
947
|
if (!prepared.ok) return prepared.refusal;
|
|
937
948
|
const frozen = prepared.value;
|
|
938
949
|
const spec = INTEGRATION_CLIENTS[frozen.clientId];
|
|
950
|
+
const guarded = (bound: IntegrationWriteInput) => options?.guard?.(bound) ?? operation(bound);
|
|
939
951
|
if (!spec.writerLock) {
|
|
940
952
|
const refused = await options?.revalidate?.(frozen);
|
|
941
|
-
return refused ??
|
|
953
|
+
return refused ?? guarded(frozen);
|
|
942
954
|
}
|
|
943
955
|
|
|
944
956
|
// An absent client home is not created merely to acquire a sibling lock.
|
|
945
957
|
if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") {
|
|
946
958
|
const refused = await options?.revalidate?.(frozen);
|
|
947
959
|
if (refused) return refused;
|
|
948
|
-
if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") return
|
|
960
|
+
if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") return guarded(frozen);
|
|
949
961
|
}
|
|
950
962
|
return withClientLocks(
|
|
951
963
|
frozen,
|
|
952
964
|
spec.writerLock.suffix,
|
|
953
|
-
() =>
|
|
965
|
+
() => guarded(frozen),
|
|
954
966
|
options,
|
|
955
967
|
);
|
|
956
968
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface DesktopCliIo {
|
|
2
|
+
stat?: (path: string) => { isFile(): boolean };
|
|
3
|
+
access?: (path: string, mode: number) => void;
|
|
4
|
+
}
|
|
5
|
+
export interface PathBunIo extends DesktopCliIo {
|
|
6
|
+
now?: () => number;
|
|
7
|
+
realpath?: (path: string) => string;
|
|
8
|
+
stat?: (path: string) => { isFile(): boolean; mode: number };
|
|
9
|
+
isRealBunBinary?: (path: string) => boolean;
|
|
10
|
+
spawnSync?: (path: string, args: string[], options: {
|
|
11
|
+
env: NodeJS.ProcessEnv;
|
|
12
|
+
encoding: "utf8";
|
|
13
|
+
timeout: number;
|
|
14
|
+
maxBuffer: number;
|
|
15
|
+
killSignal: "SIGKILL";
|
|
16
|
+
shell: false;
|
|
17
|
+
windowsHide: true;
|
|
18
|
+
stdio: ["ignore", "pipe", "pipe"];
|
|
19
|
+
}) => { status: number | null; stdout?: string; signal?: string | null; error?: unknown };
|
|
20
|
+
}
|
|
21
|
+
export declare function findPathBun(options: {
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
platform?: NodeJS.Platform;
|
|
24
|
+
pinnedVersion: string;
|
|
25
|
+
deadlineMs?: number;
|
|
26
|
+
io?: PathBunIo;
|
|
27
|
+
}): { path: string; version: string } | null;
|
|
28
|
+
export declare function findDesktopCli(options?: {
|
|
29
|
+
platform?: NodeJS.Platform;
|
|
30
|
+
home?: string;
|
|
31
|
+
io?: DesktopCliIo;
|
|
32
|
+
}): string | null;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { posix, win32 } from "node:path";
|
|
5
|
+
import { performance } from "node:perf_hooks";
|
|
6
|
+
import { isRealBunBinary } from "./bun-binary-validator.mjs";
|
|
7
|
+
|
|
8
|
+
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
9
|
+
const MAX_VERSION_BYTES = 4096;
|
|
10
|
+
const IDENTITY_PROBE = "process.stdout.write(String(typeof Bun==='object'&&Bun.version))";
|
|
11
|
+
|
|
12
|
+
function stableVersion(value) {
|
|
13
|
+
if (typeof value !== "string" || value.length > 64) return null;
|
|
14
|
+
const match = STABLE.exec(value);
|
|
15
|
+
if (!match) return null;
|
|
16
|
+
const numbers = match.slice(1, 4).map(Number);
|
|
17
|
+
return numbers.every(Number.isSafeInteger) ? numbers : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Discover the first absolute PATH Bun passing version-policy and identity checks. */
|
|
21
|
+
export function findPathBun({
|
|
22
|
+
env = process.env, platform = process.platform, pinnedVersion,
|
|
23
|
+
deadlineMs = 750, io = {},
|
|
24
|
+
}) {
|
|
25
|
+
const pin = stableVersion(pinnedVersion);
|
|
26
|
+
if (!pin || !Number.isFinite(deadlineMs) || deadlineMs <= 0) return null;
|
|
27
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
28
|
+
const absolute = value => paths.isAbsolute(value)
|
|
29
|
+
&& (platform !== "win32" || /^(?:[A-Za-z]:[\\/]|\\\\[^\\/]+[\\/][^\\/]+)/.test(value));
|
|
30
|
+
const now = io.now ?? (() => performance.now());
|
|
31
|
+
const end = now() + deadlineMs;
|
|
32
|
+
const realpath = io.realpath ?? realpathSync.native;
|
|
33
|
+
const stat = io.stat ?? statSync;
|
|
34
|
+
const access = io.access ?? accessSync;
|
|
35
|
+
const validate = io.isRealBunBinary ?? isRealBunBinary;
|
|
36
|
+
const spawn = io.spawnSync ?? spawnSync;
|
|
37
|
+
const key = platform === "win32"
|
|
38
|
+
? Object.keys(env).find(name => name.toLowerCase() === "path") : "PATH";
|
|
39
|
+
const directories = (env[key ?? "PATH"] ?? "").split(platform === "win32" ? ";" : ":");
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const probe = (path, args) => {
|
|
42
|
+
const remaining = Math.floor(end - now());
|
|
43
|
+
if (remaining <= 0) return null;
|
|
44
|
+
const result = spawn(path, args, {
|
|
45
|
+
env, encoding: "utf8", timeout: remaining, maxBuffer: MAX_VERSION_BYTES,
|
|
46
|
+
killSignal: "SIGKILL", shell: false, windowsHide: true,
|
|
47
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
48
|
+
});
|
|
49
|
+
if (now() >= end || result.error || result.signal || result.status !== 0
|
|
50
|
+
|| typeof result.stdout !== "string" || result.stdout.length > MAX_VERSION_BYTES) return null;
|
|
51
|
+
return result.stdout.trim();
|
|
52
|
+
};
|
|
53
|
+
for (const directory of directories) {
|
|
54
|
+
if (now() >= end) break;
|
|
55
|
+
if (!directory || !absolute(directory)) continue;
|
|
56
|
+
try {
|
|
57
|
+
const path = realpath(paths.join(directory, platform === "win32" ? "bun.exe" : "bun"));
|
|
58
|
+
if (!absolute(path)) continue;
|
|
59
|
+
const identity = platform === "win32" ? path.toLowerCase() : path;
|
|
60
|
+
if (seen.has(identity)) continue;
|
|
61
|
+
seen.add(identity);
|
|
62
|
+
const file = stat(path);
|
|
63
|
+
if (!file.isFile()) continue;
|
|
64
|
+
if (platform !== "win32" && ((file.mode & 0o022) || (stat(paths.dirname(path)).mode & 0o022))) continue;
|
|
65
|
+
access(path, constants.X_OK);
|
|
66
|
+
if (!validate(path)) continue;
|
|
67
|
+
const version = probe(path, ["--version"]);
|
|
68
|
+
const candidate = stableVersion(version);
|
|
69
|
+
if (!candidate || candidate[0] !== pin[0] || candidate[1] < pin[1]) continue;
|
|
70
|
+
if (probe(path, ["-e", IDENTITY_PROBE]) !== version) continue;
|
|
71
|
+
return { path, version };
|
|
72
|
+
} catch {
|
|
73
|
+
// Unreadable, unexecutable, vanished or invalid: try the next absolute candidate.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Failure-text pointer only. Never execute or delegate to this binary. */
|
|
80
|
+
export function findDesktopCli({ platform = process.platform, home = homedir(), io = {} } = {}) {
|
|
81
|
+
if (platform !== "darwin") return null;
|
|
82
|
+
const stat = io.stat ?? statSync;
|
|
83
|
+
const access = io.access ?? accessSync;
|
|
84
|
+
for (const path of [
|
|
85
|
+
"/Applications/OpenCodex.app/Contents/MacOS/ocx",
|
|
86
|
+
posix.join(home, "Applications/OpenCodex.app/Contents/MacOS/ocx"),
|
|
87
|
+
]) {
|
|
88
|
+
try {
|
|
89
|
+
if (!stat(path).isFile()) continue;
|
|
90
|
+
access(path, constants.X_OK);
|
|
91
|
+
return path;
|
|
92
|
+
} catch { /* Missing or not executable: no pointer for this candidate. */ }
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { lstatSync, mkdirSync, readdirSync, rmdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { DurableBunRuntime } from "./bun-runtime";
|
|
6
|
+
|
|
7
|
+
export type RuntimePreflightReason = "spawn" | "timeout" | "create" | "remove" | "protocol";
|
|
8
|
+
export const RUNTIME_PREFLIGHT_TIMEOUT_MS = 5_000;
|
|
9
|
+
export const RUNTIME_PREFLIGHT_GUIDANCE = "The selected Bun runtime could not create and remove a directory in the OpenCodex config directory. "
|
|
10
|
+
+ "Windows application policy may deny writes from this executable location. "
|
|
11
|
+
+ "Set OPENCODEX_BUN_PATH to a trusted Bun executable allowed by your policy before launching ocx, "
|
|
12
|
+
+ "or reinstall opencodex with npm install -g @bitkyc08/opencodex, then retry. No alternate runtime was selected.";
|
|
13
|
+
|
|
14
|
+
export class RuntimePreflightError extends Error {
|
|
15
|
+
readonly code = "OCX_RUNTIME_PREFLIGHT_FAILED";
|
|
16
|
+
constructor(readonly reason: RuntimePreflightReason) {
|
|
17
|
+
super(`${RUNTIME_PREFLIGHT_GUIDANCE} (preflight: ${reason})`);
|
|
18
|
+
this.name = "RuntimePreflightError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type DirectoryIdentity = { dev: number; ino: number; isDirectory(): boolean; isSymbolicLink(): boolean };
|
|
23
|
+
export interface RuntimePreflightFs {
|
|
24
|
+
lstatSync: (path: string) => DirectoryIdentity;
|
|
25
|
+
mkdirSync: (path: string, mode: number) => unknown;
|
|
26
|
+
rmdirSync: (path: string) => void;
|
|
27
|
+
readdirSync: (path: string) => readonly string[];
|
|
28
|
+
}
|
|
29
|
+
export interface RuntimePreflightOptions {
|
|
30
|
+
platform?: NodeJS.Platform;
|
|
31
|
+
rootWasAbsent?: boolean;
|
|
32
|
+
spawnSync?: typeof spawnSync;
|
|
33
|
+
fs?: RuntimePreflightFs;
|
|
34
|
+
nonce?: () => string;
|
|
35
|
+
}
|
|
36
|
+
/** Caller seams keep one selection through admission, rendering and state publication. */
|
|
37
|
+
export interface RuntimePreflightDeps {
|
|
38
|
+
selectRuntime?: () => DurableBunRuntime;
|
|
39
|
+
assertRuntimeWritable?: typeof assertSelectedRuntimeWritable;
|
|
40
|
+
configDir?: () => string;
|
|
41
|
+
platform?: NodeJS.Platform;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const probeFs: RuntimePreflightFs = { lstatSync, mkdirSync, rmdirSync, readdirSync };
|
|
45
|
+
|
|
46
|
+
function sameDirectory(fs: RuntimePreflightFs, path: string, owned: DirectoryIdentity): boolean {
|
|
47
|
+
const current = fs.lstatSync(path);
|
|
48
|
+
return current.isDirectory() && !current.isSymbolicLink()
|
|
49
|
+
&& current.dev === owned.dev && current.ino === owned.ino;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function removeOwnedEmptyDirectory(fs: RuntimePreflightFs, path: string, owned: DirectoryIdentity): void {
|
|
53
|
+
try {
|
|
54
|
+
if (sameDirectory(fs, path, owned) && fs.readdirSync(path).length === 0
|
|
55
|
+
&& sameDirectory(fs, path, owned)) fs.rmdirSync(path);
|
|
56
|
+
} catch { /* Never delete contents or a replacement; cleanup does not mask refusal. */ }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function prepareRoot(fs: RuntimePreflightFs, root: string, rootWasAbsent?: boolean): void {
|
|
60
|
+
try {
|
|
61
|
+
if (rootWasAbsent === undefined) {
|
|
62
|
+
try { fs.lstatSync(root); rootWasAbsent = false; }
|
|
63
|
+
catch (error) {
|
|
64
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
65
|
+
rootWasAbsent = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (rootWasAbsent) {
|
|
69
|
+
try {
|
|
70
|
+
fs.mkdirSync(root, 0o700); // Exclusive, non-recursive: a missing parent refuses.
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
73
|
+
// Use a concurrently created root, subject to the directory check below.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const current = fs.lstatSync(root);
|
|
77
|
+
if (!current.isDirectory() || current.isSymbolicLink()) throw new RuntimePreflightError("create");
|
|
78
|
+
} catch {
|
|
79
|
+
throw new RuntimePreflightError("create");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function probeInProcess(fs: RuntimePreflightFs, path: string): void {
|
|
84
|
+
let owned: DirectoryIdentity | undefined;
|
|
85
|
+
try {
|
|
86
|
+
try { fs.mkdirSync(path, 0o700); owned = fs.lstatSync(path); }
|
|
87
|
+
catch { throw new RuntimePreflightError("create"); }
|
|
88
|
+
try { fs.rmdirSync(path); }
|
|
89
|
+
catch { throw new RuntimePreflightError("remove"); }
|
|
90
|
+
} finally {
|
|
91
|
+
if (owned) removeOwnedEmptyDirectory(fs, path, owned);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function probeChild(runtime: DurableBunRuntime, path: string, nonce: string, spawn: typeof spawnSync): void {
|
|
96
|
+
// The child emits only this invocation's nonce and a fixed result, never paths/errors.
|
|
97
|
+
const script = `const fs = require("node:fs"); const path = ${JSON.stringify(path)};
|
|
98
|
+
let result = "create", owned;
|
|
99
|
+
try { fs.mkdirSync(path, 0o700); owned = fs.lstatSync(path); result = "remove";
|
|
100
|
+
fs.rmdirSync(path); result = "ok";
|
|
101
|
+
} catch { /* result already names the failed step; details never leave the child */ } finally {
|
|
102
|
+
if (owned) { try { const now = fs.lstatSync(path);
|
|
103
|
+
if (now.isDirectory() && !now.isSymbolicLink() && now.dev === owned.dev && now.ino === owned.ino
|
|
104
|
+
&& fs.readdirSync(path).length === 0) fs.rmdirSync(path);
|
|
105
|
+
} catch { /* leave any probe entry that cannot be proven ours */ } }
|
|
106
|
+
}
|
|
107
|
+
process.stdout.write(${JSON.stringify(nonce)} + ":" + result);
|
|
108
|
+
process.exitCode = result === "ok" ? 0 : 1;`;
|
|
109
|
+
let child: ReturnType<typeof spawnSync>;
|
|
110
|
+
try {
|
|
111
|
+
child = spawn(runtime.path, ["-e", script], {
|
|
112
|
+
shell: false, windowsHide: true, timeout: RUNTIME_PREFLIGHT_TIMEOUT_MS,
|
|
113
|
+
encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 1024,
|
|
114
|
+
});
|
|
115
|
+
} catch { throw new RuntimePreflightError("spawn"); }
|
|
116
|
+
if (child.error) {
|
|
117
|
+
throw new RuntimePreflightError((child.error as NodeJS.ErrnoException).code === "ETIMEDOUT" ? "timeout" : "spawn");
|
|
118
|
+
}
|
|
119
|
+
const ack = String(child.stdout ?? "");
|
|
120
|
+
if (ack === `${nonce}:create` && child.status === 1) throw new RuntimePreflightError("create");
|
|
121
|
+
if (ack === `${nonce}:remove` && child.status === 1) throw new RuntimePreflightError("remove");
|
|
122
|
+
if (ack !== `${nonce}:ok` || child.status !== 0) throw new RuntimePreflightError("protocol");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Windows admission of ONLY the already selected runtime. No discovery or memoization. */
|
|
126
|
+
export function assertSelectedRuntimeWritable(
|
|
127
|
+
runtime: DurableBunRuntime,
|
|
128
|
+
configDir: string,
|
|
129
|
+
deps: RuntimePreflightOptions = {},
|
|
130
|
+
): void {
|
|
131
|
+
if ((deps.platform ?? process.platform) !== "win32") return;
|
|
132
|
+
if (runtime.source === "standalone" && runtime.path !== process.execPath) throw new RuntimePreflightError("protocol");
|
|
133
|
+
const fs = deps.fs ?? probeFs;
|
|
134
|
+
// The config root is durable: never delete it by pathname, even on refusal.
|
|
135
|
+
prepareRoot(fs, configDir, deps.rootWasAbsent);
|
|
136
|
+
const nonce = (deps.nonce ?? randomUUID)();
|
|
137
|
+
const path = join(configDir, `.ocx-runtime-probe-${nonce}`);
|
|
138
|
+
if (runtime.source === "standalone") probeInProcess(fs, path);
|
|
139
|
+
else probeChild(runtime, path, nonce, deps.spawnSync ?? spawnSync);
|
|
140
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { spawn } from "node:child_process";
|
|
2
|
+
import type { accessSync, lstatSync, statSync } from "node:fs";
|
|
3
|
+
import type { DesktopCliRecordRead, DesktopCliRecordIssue } from "./desktop-cli-record.mjs";
|
|
4
|
+
export type DesktopHandoffIssue = DesktopCliRecordIssue | "target-invalid" | "target-self" | "target-unusable" | "spawn-failed";
|
|
5
|
+
export type DesktopCliHandoffPlan =
|
|
6
|
+
| { kind: "continue"; reason: "excluded" | "missing" | "disabled" | "target-missing" | "windows-path-only" }
|
|
7
|
+
| { kind: "error"; issue: DesktopHandoffIssue }
|
|
8
|
+
| { kind: "handoff"; target: string };
|
|
9
|
+
export type DesktopCliHandoffExit =
|
|
10
|
+
| { kind: "continue" }
|
|
11
|
+
| { kind: "error"; issue: "spawn-failed" }
|
|
12
|
+
| { kind: "exit"; code: number; signal: NodeJS.Signals | null };
|
|
13
|
+
export function desktopHandoffExcluded(argv: string[], env?: NodeJS.ProcessEnv): boolean;
|
|
14
|
+
export function planDesktopCliHandoff(input?: {
|
|
15
|
+
argv?: string[]; env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform;
|
|
16
|
+
recordRead?: DesktopCliRecordRead; selfPaths?: (string | undefined)[];
|
|
17
|
+
}, deps?: { stat?: typeof statSync; access?: typeof accessSync; realpath?: (path: string) => string }): DesktopCliHandoffPlan;
|
|
18
|
+
export function runDesktopCliHandoff(plan: Extract<DesktopCliHandoffPlan, { kind: "handoff" }>, launch: {
|
|
19
|
+
argv: string[]; env: NodeJS.ProcessEnv; proof: string; context: string;
|
|
20
|
+
}, deps?: { spawn?: typeof spawn; lstat?: typeof lstatSync; parent?: Pick<NodeJS.Process, "on" | "removeListener">; platform?: NodeJS.Platform }): Promise<DesktopCliHandoffExit>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, lstatSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { posix, win32 } from "node:path";
|
|
4
|
+
import { readDesktopCliRecord } from "./desktop-cli-record.mjs";
|
|
5
|
+
import { isCodexCliUpdateInspectionArgv } from "../update/codex-cli-update-launch-policy.mjs";
|
|
6
|
+
|
|
7
|
+
const PROOF_PREFIX = "--ocx-internal-launch-proof=";
|
|
8
|
+
|
|
9
|
+
export function desktopHandoffExcluded(argv, env = process.env) {
|
|
10
|
+
const args = argv.filter(arg => !arg.startsWith(PROOF_PREFIX));
|
|
11
|
+
return env.OCX_NO_DESKTOP_HANDOFF === "1"
|
|
12
|
+
|| ["update", "uninstall", "remove"].includes(args[0])
|
|
13
|
+
|| (args[0] ?? "").startsWith("__")
|
|
14
|
+
|| isCodexCliUpdateInspectionArgv(["node", "ocx.mjs", ...args]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function planDesktopCliHandoff(input = {}, deps = {}) {
|
|
18
|
+
const platform = input.platform ?? process.platform;
|
|
19
|
+
if (platform === "win32") return { kind: "continue", reason: "windows-path-only" };
|
|
20
|
+
const argv = input.argv ?? process.argv.slice(2);
|
|
21
|
+
const env = input.env ?? process.env;
|
|
22
|
+
if (desktopHandoffExcluded(argv, env)) return { kind: "continue", reason: "excluded" };
|
|
23
|
+
const read = input.recordRead ?? readDesktopCliRecord({ env, platform: input.platform });
|
|
24
|
+
if (read.state === "missing" || read.state === "disabled") {
|
|
25
|
+
return { kind: "continue", reason: read.state };
|
|
26
|
+
}
|
|
27
|
+
if (read.state !== "ready") return { kind: "error", issue: read.issue };
|
|
28
|
+
const api = platform === "win32" ? win32 : posix;
|
|
29
|
+
const target = read.record.cliExecutable;
|
|
30
|
+
if (!api.isAbsolute(target)) return { kind: "error", issue: "target-invalid" };
|
|
31
|
+
const stat = deps.stat ?? statSync;
|
|
32
|
+
const access = deps.access ?? accessSync;
|
|
33
|
+
const realpath = deps.realpath ?? realpathSync;
|
|
34
|
+
try {
|
|
35
|
+
if (!stat(target).isFile()) return { kind: "error", issue: "target-invalid" };
|
|
36
|
+
access(target, platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
37
|
+
const physical = realpath(target);
|
|
38
|
+
const key = path => platform === "win32" ? path.toLowerCase() : path;
|
|
39
|
+
for (const self of input.selfPaths ?? [process.argv[1], process.execPath]) {
|
|
40
|
+
if (!self) continue;
|
|
41
|
+
let own;
|
|
42
|
+
try { own = realpath(self); }
|
|
43
|
+
catch (error) { if (error?.code === "ENOENT") continue; throw error; }
|
|
44
|
+
if (key(own) === key(physical)) return { kind: "error", issue: "target-self" };
|
|
45
|
+
}
|
|
46
|
+
return { kind: "handoff", target: physical };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return error?.code === "ENOENT"
|
|
49
|
+
? { kind: "continue", reason: "target-missing" }
|
|
50
|
+
: { kind: "error", issue: "target-unusable" };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function runDesktopCliHandoff(plan, launch, deps = {}) {
|
|
55
|
+
const spawnChild = deps.spawn ?? spawn;
|
|
56
|
+
const parent = deps.parent ?? process;
|
|
57
|
+
const platform = deps.platform ?? process.platform;
|
|
58
|
+
const lstat = deps.lstat ?? lstatSync;
|
|
59
|
+
const spawnFailure = error => {
|
|
60
|
+
if (error?.code === "ENOENT") {
|
|
61
|
+
try { lstat(plan.target); }
|
|
62
|
+
catch (checkError) { if (checkError?.code === "ENOENT") return { kind: "continue" }; }
|
|
63
|
+
}
|
|
64
|
+
return { kind: "error", issue: "spawn-failed" };
|
|
65
|
+
};
|
|
66
|
+
const argv = launch.argv.filter(arg => !arg.startsWith(PROOF_PREFIX));
|
|
67
|
+
const env = { ...launch.env, OCX_NO_DESKTOP_HANDOFF: "1", OCX_NODE_LAUNCH_CONTEXT: launch.context };
|
|
68
|
+
delete env.OCX_BUN_RUNTIME_SOURCE;
|
|
69
|
+
delete env.OCX_BUN_RUNTIME_PATH;
|
|
70
|
+
return new Promise(resolveExit => {
|
|
71
|
+
let child;
|
|
72
|
+
try {
|
|
73
|
+
child = spawnChild(plan.target, [`${PROOF_PREFIX}${launch.proof}`, ...argv], {
|
|
74
|
+
stdio: "inherit", shell: false, windowsHide: true, env,
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
resolveExit(spawnFailure(error));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const forwarded = platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
81
|
+
const handlers = forwarded.map(signal => {
|
|
82
|
+
const handler = () => { try { child.kill(signal); } catch { /* child already exited */ } };
|
|
83
|
+
parent.on(signal, handler);
|
|
84
|
+
return [signal, handler];
|
|
85
|
+
});
|
|
86
|
+
const clear = () => { for (const [signal, handler] of handlers) parent.removeListener(signal, handler); };
|
|
87
|
+
let done = false;
|
|
88
|
+
const finish = result => { if (done) return; done = true; clear(); resolveExit(result); };
|
|
89
|
+
child.once("error", error => finish(spawnFailure(error)));
|
|
90
|
+
child.once("exit", (code, signal) => finish({ kind: "exit", code: code ?? 1, signal }));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Stats } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export const DESKTOP_CLI_RECORD_MAX_BYTES: number;
|
|
4
|
+
export type DesktopCliRecord = Readonly<{
|
|
5
|
+
platform: "darwin" | "win32" | "linux";
|
|
6
|
+
kind: "macos-app" | "windows-install" | "linux-deb";
|
|
7
|
+
cliExecutable: string;
|
|
8
|
+
}>;
|
|
9
|
+
export type DesktopCliRecordIssue = "record-invalid" | "record-too-large" | "record-pending" | "record-unreadable" | "record-unsafe";
|
|
10
|
+
export type DesktopCliRecordRead =
|
|
11
|
+
| { state: "missing"; path: string }
|
|
12
|
+
| { state: "disabled"; path: string; cleanupPending: boolean }
|
|
13
|
+
| { state: "ready"; path: string; record: DesktopCliRecord }
|
|
14
|
+
| { state: "invalid" | "unreadable"; path: string; issue: DesktopCliRecordIssue };
|
|
15
|
+
export type DesktopCliRecordOptions = {
|
|
16
|
+
platform?: NodeJS.Platform;
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
home?: string;
|
|
19
|
+
recordPath?: string;
|
|
20
|
+
};
|
|
21
|
+
export function desktopCliRecordPath(options?: DesktopCliRecordOptions): string;
|
|
22
|
+
export type DesktopCliRecordDeps = {
|
|
23
|
+
lstat?: (path: string) => Stats;
|
|
24
|
+
fstat?: (fd: number) => Stats;
|
|
25
|
+
open?: (path: string, flags: number) => number;
|
|
26
|
+
euid?: number;
|
|
27
|
+
checkAcl?: (directory: string, path: string) => boolean;
|
|
28
|
+
};
|
|
29
|
+
export function readDesktopCliRecord(options?: DesktopCliRecordOptions, deps?: DesktopCliRecordDeps): DesktopCliRecordRead;
|