@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
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, posix, resolve, win32 } from "node:path";
|
|
5
|
+
|
|
6
|
+
export const DESKTOP_CLI_RECORD_MAX_BYTES = 64 * 1024;
|
|
7
|
+
|
|
8
|
+
export function desktopCliRecordPath(options = {}) {
|
|
9
|
+
const platform = options.platform ?? process.platform;
|
|
10
|
+
const env = options.env ?? process.env;
|
|
11
|
+
const home = options.home ?? (platform === "win32" ? env.USERPROFILE || homedir() : homedir());
|
|
12
|
+
return (platform === "win32" ? win32 : posix).join(home, ".opencodex-desktop", "cli.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Keep these lexical rules aligned with Rust's absolute_for in cli_command_record.rs.
|
|
16
|
+
function validTargetPath(path, platform) {
|
|
17
|
+
if ([...path].length > 4096 || /[\x00\r\n]/.test(path)) return false;
|
|
18
|
+
const absolute = platform === "win32"
|
|
19
|
+
? /^[A-Za-z]:[/\\]/.test(path) || path.startsWith("\\\\") || path.startsWith("//")
|
|
20
|
+
: path.startsWith("/");
|
|
21
|
+
return absolute && !path.split(platform === "win32" ? /[/\\]/ : /\//)
|
|
22
|
+
.some(part => part === "." || part === "..");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function noMacAcl(directory, path) {
|
|
26
|
+
const result = spawnSync("/bin/ls", ["-lde", directory, path], {
|
|
27
|
+
encoding: "utf8", timeout: 1000, shell: false, windowsHide: true,
|
|
28
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
29
|
+
});
|
|
30
|
+
if (result.error || result.status !== 0 || result.stderr) return false;
|
|
31
|
+
// Each path has one metadata line; ACL entries add numbered lines and a "+" marker.
|
|
32
|
+
const lines = result.stdout.trimEnd().split("\n");
|
|
33
|
+
return lines.length === 2 && lines.every(line => /^[d-][rwxStTs-]{9}[.@]?\s/.test(line));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function readDesktopCliRecord(options = {}, deps = {}) {
|
|
37
|
+
const platform = options.platform ?? process.platform;
|
|
38
|
+
const path = options.recordPath ?? desktopCliRecordPath(options);
|
|
39
|
+
const directory = dirname(resolve(path));
|
|
40
|
+
const lstat = deps.lstat ?? lstatSync;
|
|
41
|
+
const fstat = deps.fstat ?? fstatSync;
|
|
42
|
+
const open = deps.open ?? openSync;
|
|
43
|
+
const unsafe = () => ({ state: "invalid", path, issue: "record-unsafe" });
|
|
44
|
+
const posixHost = platform !== "win32";
|
|
45
|
+
let fd;
|
|
46
|
+
let checkingSafety = true;
|
|
47
|
+
let absenceAllowed = true;
|
|
48
|
+
try {
|
|
49
|
+
const dirInfo = lstat(directory);
|
|
50
|
+
if (dirInfo.isSymbolicLink() || !dirInfo.isDirectory()) return unsafe();
|
|
51
|
+
const uid = posixHost ? deps.euid ?? process.geteuid?.() : undefined;
|
|
52
|
+
if (posixHost && (uid === undefined || dirInfo.uid !== uid || (dirInfo.mode & 0o077) !== 0)) return unsafe();
|
|
53
|
+
const fileInfo = lstat(path);
|
|
54
|
+
absenceAllowed = false;
|
|
55
|
+
if (fileInfo.isSymbolicLink() || !fileInfo.isFile()
|
|
56
|
+
|| (posixHost && (fileInfo.uid !== uid || (fileInfo.mode & 0o077) !== 0))) return unsafe();
|
|
57
|
+
const flags = constants.O_RDONLY | (posixHost ? constants.O_NOFOLLOW | constants.O_NONBLOCK : 0);
|
|
58
|
+
fd = open(path, flags);
|
|
59
|
+
const info = fstat(fd);
|
|
60
|
+
if (!info.isFile() || ["dev", "ino", "uid", "mode"].some(key => info[key] !== fileInfo[key])) return unsafe();
|
|
61
|
+
if (platform === "darwin" && !(deps.checkAcl ?? noMacAcl)(directory, resolve(path))) return unsafe();
|
|
62
|
+
checkingSafety = false;
|
|
63
|
+
if (info.size > DESKTOP_CLI_RECORD_MAX_BYTES) {
|
|
64
|
+
return { state: "invalid", path, issue: "record-too-large" };
|
|
65
|
+
}
|
|
66
|
+
const bytes = Buffer.alloc(DESKTOP_CLI_RECORD_MAX_BYTES + 1);
|
|
67
|
+
let used = 0;
|
|
68
|
+
while (used < bytes.length) {
|
|
69
|
+
const n = readSync(fd, bytes, used, bytes.length - used, null);
|
|
70
|
+
if (n === 0) break;
|
|
71
|
+
used += n;
|
|
72
|
+
}
|
|
73
|
+
if (used > DESKTOP_CLI_RECORD_MAX_BYTES) {
|
|
74
|
+
return { state: "invalid", path, issue: "record-too-large" };
|
|
75
|
+
}
|
|
76
|
+
let value;
|
|
77
|
+
try { value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, used))); }
|
|
78
|
+
catch { return { state: "invalid", path, issue: "record-invalid" }; }
|
|
79
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
80
|
+
|| value.version !== 1 || typeof value.enabled !== "boolean") {
|
|
81
|
+
return { state: "invalid", path, issue: "record-invalid" };
|
|
82
|
+
}
|
|
83
|
+
if (!value.enabled) return { state: "disabled", path, cleanupPending: value.pending != null };
|
|
84
|
+
if (value.pending != null) return { state: "invalid", path, issue: "record-pending" };
|
|
85
|
+
const bundle = value.bundle;
|
|
86
|
+
const kind = { darwin: "macos-app", win32: "windows-install", linux: "linux-deb" }[platform];
|
|
87
|
+
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)
|
|
88
|
+
|| !kind || bundle.platform !== platform || bundle.kind !== kind
|
|
89
|
+
|| typeof bundle.cliExecutable !== "string" || !validTargetPath(bundle.cliExecutable, platform)) {
|
|
90
|
+
return { state: "invalid", path, issue: "record-invalid" };
|
|
91
|
+
}
|
|
92
|
+
return { state: "ready", path, record: {
|
|
93
|
+
platform, kind, cliExecutable: bundle.cliExecutable,
|
|
94
|
+
} };
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return error?.code === "ENOENT" && absenceAllowed
|
|
97
|
+
? { state: "missing", path }
|
|
98
|
+
: checkingSafety ? unsafe() : { state: "unreadable", path, issue: "record-unreadable" };
|
|
99
|
+
} finally {
|
|
100
|
+
if (fd !== undefined) closeSync(fd);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
UnsupportedOAuthProviderError,
|
|
16
16
|
} from "../oauth";
|
|
17
17
|
import type { OcxConfig } from "../types";
|
|
18
|
+
import { configuredAnthropicInstance, isAnthropicOAuthInstance } from "../providers/anthropic-instance";
|
|
18
19
|
import { createCredentialLease } from "../lab/live/credential-lease";
|
|
19
20
|
import {
|
|
20
21
|
liveUpstreamRequestPath,
|
|
@@ -34,7 +35,8 @@ export interface ProductionLabRouteExecutorDeps {
|
|
|
34
35
|
loadConfig: () => OcxConfig;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
/** @internal exported for the Pool 2 ownership regression test. */
|
|
39
|
+
export async function buildLabProviderAuthHeaders(
|
|
38
40
|
routeContext: LabRouteContext,
|
|
39
41
|
config: OcxConfig,
|
|
40
42
|
): Promise<Record<string, string>> {
|
|
@@ -64,6 +66,12 @@ async function buildLabProviderAuthHeaders(
|
|
|
64
66
|
if (protocol !== "https:") {
|
|
65
67
|
throw new TransportError("auth_blocked", "OAuth lab probes require HTTPS");
|
|
66
68
|
}
|
|
69
|
+
// The Pool 2 credential namespace belongs to the marked builtin row only; an unmarked or
|
|
70
|
+
// orphaned `anthropic2` OAuth row must never obtain its bearer through this generic path.
|
|
71
|
+
if (isAnthropicOAuthInstance(routeContext.providerId)
|
|
72
|
+
&& configuredAnthropicInstance(config, routeContext.providerId) !== routeContext.providerId) {
|
|
73
|
+
throw new TransportError("auth_blocked", "anthropic instance unavailable");
|
|
74
|
+
}
|
|
67
75
|
try {
|
|
68
76
|
const snapshot = await getValidAccessTokenSnapshot(routeContext.providerId);
|
|
69
77
|
headers.Authorization = `Bearer ${snapshot.accessToken}`;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
/**
|
|
2
3
|
* One logical request, one execution budget (#4546).
|
|
3
4
|
*
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
* sends plus one alternate -- by funding the alternate from a reserve that a validated
|
|
15
16
|
* sanitized repair can spend instead, but never both.
|
|
16
17
|
*/
|
|
18
|
+
import type { SpendSeed, SpendScopes, SpendReservationProof, SpendAdmissionPolicy } from "./spend-reservation-ledger";
|
|
17
19
|
import type { TransientSendBudget } from "./upstream-retry";
|
|
18
20
|
|
|
19
21
|
export type SendClass =
|
|
@@ -97,6 +99,8 @@ export interface SingleUseDispatchPermit {
|
|
|
97
99
|
* false, which is what keeps a retry thunk from sending twice on one permit.
|
|
98
100
|
*/
|
|
99
101
|
use(): boolean;
|
|
102
|
+
/** Keep the selected start rebindable until its executor finishes. */
|
|
103
|
+
execute?<T>(run: () => Promise<T>): Promise<T>;
|
|
100
104
|
/**
|
|
101
105
|
* Hand back a reservation that never dispatched -- a credential move that found no alternate,
|
|
102
106
|
* a rebuild abandoned before the send. Idempotent, and a no-op once the permit was used or
|
|
@@ -145,9 +149,18 @@ export interface RequestSendObserver {
|
|
|
145
149
|
* would cross a ceiling never joins the total, the total stays just under, and the ceiling
|
|
146
150
|
* never fires for any later request either.
|
|
147
151
|
*/
|
|
148
|
-
charge(options?: { alreadySent?: boolean }): boolean;
|
|
152
|
+
charge(options?: { alreadySent?: boolean; deferDispatch?: boolean; onReserved?: (proof: SpendReservationProof) => void; targetKey?: string }): boolean;
|
|
153
|
+
/** Confirm the exact reservation once its dispatch is known. */
|
|
154
|
+
readonly enforced?: boolean;
|
|
155
|
+
readonly policyStarted?: boolean;
|
|
156
|
+
readonly spendAdmissionPolicy?: SpendAdmissionPolicy;
|
|
157
|
+
/** Capture request policy at its first admission or actual physical start. */
|
|
158
|
+
startRequest?(target?: SpendScopes & { targetKey?: string }): void;
|
|
159
|
+
ensureSeed?(target: SpendScopes & { targetKey?: string }, proof?: SpendReservationProof): SpendSeed | undefined;
|
|
160
|
+
beginReporter?(): { start(seed: SpendSeed, ordinal: number): void; rebindTarget(target: SpendScopes & { targetKey?: string }, ordinal: number): boolean; report(sends: number): void; close(): void };
|
|
161
|
+
dispatch?(proof: SpendReservationProof): void;
|
|
149
162
|
/** Give back a booking whose send never happened. */
|
|
150
|
-
refund(): void;
|
|
163
|
+
refund(proof?: SpendReservationProof | null): void;
|
|
151
164
|
}
|
|
152
165
|
|
|
153
166
|
/**
|
|
@@ -156,6 +169,14 @@ export interface RequestSendObserver {
|
|
|
156
169
|
* model send; the reserve is what the fourth send draws on once the base allowance is gone.
|
|
157
170
|
*/
|
|
158
171
|
export interface RequestExecutionBudget extends TransientSendBudget {
|
|
172
|
+
readonly physicalStarted?: number;
|
|
173
|
+
readonly physicalLimit?: number;
|
|
174
|
+
readonly spendEnforced?: boolean;
|
|
175
|
+
readonly spendPolicyStarted?: boolean;
|
|
176
|
+
readonly spendAdmissionPolicy?: SpendAdmissionPolicy;
|
|
177
|
+
startRequest?(target?: SpendScopes & { targetKey?: string }): void;
|
|
178
|
+
claimPhysicalSend?(): number | undefined;
|
|
179
|
+
beginSpendProducer?(): { close(): void } | undefined;
|
|
159
180
|
readonly logicalRequestId: string;
|
|
160
181
|
readonly policyVersion: string;
|
|
161
182
|
readonly policy: RequestExecutionBudgetPolicy;
|
|
@@ -173,6 +194,8 @@ export interface RequestExecutionBudget extends TransientSendBudget {
|
|
|
173
194
|
readonly alternateTargetSends: number;
|
|
174
195
|
readonly targetTransitions: number;
|
|
175
196
|
readonly lastTargetKey: string | undefined;
|
|
197
|
+
/** Seed a fresh derived scope with the actual endpoint of its already-paid initial send. */
|
|
198
|
+
bindPrepaidTarget?(targetKey: string): void;
|
|
176
199
|
/**
|
|
177
200
|
* Spend one operator-granted replacement for an AMBIGUOUS failure of this logical request,
|
|
178
201
|
* up to `limit`. False once the request has none left.
|
|
@@ -223,7 +246,9 @@ let logicalRequestSeq = 0;
|
|
|
223
246
|
*/
|
|
224
247
|
interface SharedSendLedger {
|
|
225
248
|
spent: number;
|
|
226
|
-
|
|
249
|
+
physicalStarted: number;
|
|
250
|
+
physicalLimit?: number;
|
|
251
|
+
pendingExternalSends: Set<object>;
|
|
227
252
|
/**
|
|
228
253
|
* Spend one of this logical request's replacements for an ambiguous failure. Beside `spent`
|
|
229
254
|
* for the same reason `pendingExternalSends` is: a derived scope that shared one without the
|
|
@@ -239,6 +264,102 @@ interface SharedSendLedger {
|
|
|
239
264
|
}
|
|
240
265
|
|
|
241
266
|
const sharedSendLedgers = new WeakMap<RequestExecutionBudget, SharedSendLedger>();
|
|
267
|
+
const dispatchSpendProofs = new WeakMap<SingleUseDispatchPermit, {
|
|
268
|
+
owner: SharedSendLedger;
|
|
269
|
+
claim(): SpendReservationProof | undefined;
|
|
270
|
+
report(): boolean;
|
|
271
|
+
}>();
|
|
272
|
+
|
|
273
|
+
/** One preflight for the exact prepaid dispatch, never another budget or a replayed permit. */
|
|
274
|
+
export function claimDispatchSpendProof(budget: RequestExecutionBudget, permit?: SingleUseDispatchPermit): SpendReservationProof | undefined {
|
|
275
|
+
const proof = permit && dispatchSpendProofs.get(permit);
|
|
276
|
+
return proof && proof.owner === sharedSendLedgers.get(budget) ? proof.claim() : undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Reports actual sends against only the named permit on this shared ledger. */
|
|
280
|
+
export function reportDispatchSends(
|
|
281
|
+
budget: TransientSendBudget,
|
|
282
|
+
sends: number,
|
|
283
|
+
permit?: SingleUseDispatchPermit,
|
|
284
|
+
): void {
|
|
285
|
+
const count = Number.isFinite(sends) ? Math.max(0, Math.trunc(sends)) : 0;
|
|
286
|
+
if (count === 0) return;
|
|
287
|
+
const receipt = permit && dispatchSpendProofs.get(permit);
|
|
288
|
+
const prepaid = receipt && receipt.owner === sharedSendLedgers.get(budget as RequestExecutionBudget)
|
|
289
|
+
&& receipt.report() ? 1 : 0;
|
|
290
|
+
// Unnamed, foreign, released or already-reported permits cannot consume another receipt.
|
|
291
|
+
budget.used += count - prepaid;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** A helper owns its starts and can never consume another helper's receipts. */
|
|
295
|
+
export type PhysicalSendReporter = ((sends: number) => void) & {
|
|
296
|
+
beforeSend?: () => boolean;
|
|
297
|
+
close?: () => void;
|
|
298
|
+
execute?: <T>(run: () => Promise<T>) => Promise<T>;
|
|
299
|
+
bind?: (target: SpendScopes & { targetKey?: string }) => boolean;
|
|
300
|
+
};
|
|
301
|
+
const activePhysicalReporters = new AsyncLocalStorage<PhysicalSendReporter>();
|
|
302
|
+
/** Selection owners rebind this helper's current start before invoking the executor. */
|
|
303
|
+
export function rebindPhysicalSend(budget: TransientSendBudget | undefined, target: SpendScopes & { targetKey?: string }): boolean {
|
|
304
|
+
if (!(budget as RequestExecutionBudget | undefined)?.spendEnforced) return true;
|
|
305
|
+
return activePhysicalReporters.getStore()?.bind?.(target) ?? true;
|
|
306
|
+
}
|
|
307
|
+
export function createPhysicalSendReporter(
|
|
308
|
+
budget: TransientSendBudget,
|
|
309
|
+
target: () => SpendScopes & { targetKey?: string },
|
|
310
|
+
permit?: SingleUseDispatchPermit,
|
|
311
|
+
telemetry?: (sends: number) => void,
|
|
312
|
+
): PhysicalSendReporter {
|
|
313
|
+
const counter = sharedSendLedgers.get(budget as RequestExecutionBudget);
|
|
314
|
+
const observer = counter?.observer;
|
|
315
|
+
let producer: ReturnType<NonNullable<RequestSendObserver["beginReporter"]>> | undefined;
|
|
316
|
+
let closed = false;
|
|
317
|
+
let started = 0;
|
|
318
|
+
let reported = 0;
|
|
319
|
+
let currentOrdinal: number | undefined;
|
|
320
|
+
const report: PhysicalSendReporter = (sends) => {
|
|
321
|
+
if (closed) return;
|
|
322
|
+
if (!producer) { if (observer?.enforced !== true) { reportDispatchSends(budget, sends, permit); telemetry?.(sends); } return; }
|
|
323
|
+
const count = Math.min(Math.max(0, Math.trunc(sends)), started - reported);
|
|
324
|
+
try { producer.report(count); }
|
|
325
|
+
finally {
|
|
326
|
+
reported += count;
|
|
327
|
+
const receipt = permit && dispatchSpendProofs.get(permit);
|
|
328
|
+
const prepaid = count > 0 && receipt && receipt.owner === counter && receipt.report() ? 1 : 0;
|
|
329
|
+
if (counter) counter.spent += count - prepaid;
|
|
330
|
+
telemetry?.(count);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
if (observer?.startRequest || observer?.enforced === true) report.beforeSend = () => {
|
|
334
|
+
if (closed) return false;
|
|
335
|
+
const selected = target();
|
|
336
|
+
if ((budget as RequestExecutionBudget).startRequest) (budget as RequestExecutionBudget).startRequest!(selected);
|
|
337
|
+
else observer?.startRequest?.(selected);
|
|
338
|
+
if (observer?.enforced !== true) return true;
|
|
339
|
+
producer ??= observer.beginReporter?.();
|
|
340
|
+
const receipt = permit && dispatchSpendProofs.get(permit);
|
|
341
|
+
const proof = receipt?.owner === counter ? receipt?.claim() : undefined;
|
|
342
|
+
const seed = observer.ensureSeed?.(selected, proof);
|
|
343
|
+
if (!seed) return false;
|
|
344
|
+
const ordinal = (budget as RequestExecutionBudget).claimPhysicalSend?.();
|
|
345
|
+
if (ordinal === undefined) return false;
|
|
346
|
+
producer?.start(seed, ordinal);
|
|
347
|
+
currentOrdinal = ordinal;
|
|
348
|
+
started++;
|
|
349
|
+
return true;
|
|
350
|
+
};
|
|
351
|
+
report.execute = run => activePhysicalReporters.run(report, run);
|
|
352
|
+
report.bind = (selected: SpendScopes & { targetKey?: string }) => {
|
|
353
|
+
if (!producer || currentOrdinal === undefined || observer?.enforced !== true) return true;
|
|
354
|
+
return producer.rebindTarget(selected, currentOrdinal);
|
|
355
|
+
};
|
|
356
|
+
report.close = () => {
|
|
357
|
+
if (closed) return;
|
|
358
|
+
try { if (producer) report(started - reported); }
|
|
359
|
+
finally { closed = true; producer?.close(); }
|
|
360
|
+
};
|
|
361
|
+
return report;
|
|
362
|
+
}
|
|
242
363
|
|
|
243
364
|
/**
|
|
244
365
|
* One logical request's replacement grant: how many it has spent, and the ceiling it is held
|
|
@@ -271,6 +392,7 @@ function createAmbiguousResendGrant(): Pick<SharedSendLedger, "claimAmbiguousRes
|
|
|
271
392
|
};
|
|
272
393
|
}
|
|
273
394
|
|
|
395
|
+
/** Keep target-local transition state over the shared ledger and its single-use bookings. */
|
|
274
396
|
function createRequestExecutionBudgetWithLedger(
|
|
275
397
|
policy: RequestExecutionBudgetPolicy,
|
|
276
398
|
logicalRequestId: string | undefined,
|
|
@@ -281,22 +403,35 @@ function createRequestExecutionBudgetWithLedger(
|
|
|
281
403
|
let alternateTargetSends = 0;
|
|
282
404
|
let targetTransitions = 0;
|
|
283
405
|
let lastTargetKey: string | undefined;
|
|
406
|
+
const targetReservations: Array<{ targetKey: string }> = [];
|
|
284
407
|
|
|
285
408
|
const budget: RequestExecutionBudget = {
|
|
409
|
+
get physicalStarted(): number { return counter.physicalStarted; },
|
|
410
|
+
get physicalLimit(): number | undefined { return counter.physicalLimit; },
|
|
411
|
+
get spendEnforced(): boolean { return observer?.enforced === true; },
|
|
412
|
+
get spendPolicyStarted(): boolean { return observer?.policyStarted === true; },
|
|
413
|
+
get spendAdmissionPolicy(): SpendAdmissionPolicy | undefined { return observer?.spendAdmissionPolicy; },
|
|
414
|
+
startRequest(target) {
|
|
415
|
+
observer?.startRequest?.(target);
|
|
416
|
+
if (observer?.enforced && counter.physicalLimit === undefined) {
|
|
417
|
+
counter.physicalLimit = Number.isSafeInteger(policy.maxTotalModelSends) && policy.maxTotalModelSends > 0 ? policy.maxTotalModelSends : 0;
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
beginSpendProducer() { return observer?.enforced ? observer.beginReporter?.() : undefined; },
|
|
421
|
+
claimPhysicalSend(): number | undefined {
|
|
422
|
+
budget.startRequest?.();
|
|
423
|
+
if (observer?.enforced !== true || counter.physicalLimit === undefined) return undefined;
|
|
424
|
+
if (counter.physicalStarted >= counter.physicalLimit) return undefined;
|
|
425
|
+
return ++counter.physicalStarted;
|
|
426
|
+
},
|
|
286
427
|
get used(): number { return counter.spent; },
|
|
287
428
|
set used(next: number) {
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
const settled = Math.min(delta, counter.pendingExternalSends);
|
|
297
|
-
counter.pendingExternalSends -= settled;
|
|
298
|
-
const charged = delta - settled;
|
|
299
|
-
counter.spent += charged;
|
|
429
|
+
// A numeric report has no receipt identity. Only reportDispatchSends may settle a
|
|
430
|
+
// prepaid permit; guessing by reservation order can refund another leg's actual send.
|
|
431
|
+
// Enforced sends can only be reconciled through their producer's claimed starts.
|
|
432
|
+
if (observer?.enforced === true) return;
|
|
433
|
+
const charged = next - counter.spent;
|
|
434
|
+
counter.spent = Math.max(0, next);
|
|
300
435
|
// These sends have already left. The ledger records them even past a ceiling it would
|
|
301
436
|
// have refused, because refusing after the fact only hides spend that was really
|
|
302
437
|
// incurred -- the refusal has to happen at the reservation below, or not at all.
|
|
@@ -309,6 +444,11 @@ function createRequestExecutionBudgetWithLedger(
|
|
|
309
444
|
get alternateTargetSends() { return alternateTargetSends; },
|
|
310
445
|
get targetTransitions() { return targetTransitions; },
|
|
311
446
|
get lastTargetKey() { return lastTargetKey; },
|
|
447
|
+
/** Seed only a fresh scope; never reset recovery history on an existing target. */
|
|
448
|
+
bindPrepaidTarget(targetKey: string): void {
|
|
449
|
+
// Only a fresh scope may bind: this is initial identity, never a recovery rebase.
|
|
450
|
+
if (lastTargetKey === undefined) lastTargetKey = targetKey;
|
|
451
|
+
},
|
|
312
452
|
remainingBaseSends(cap: number): number {
|
|
313
453
|
const capped = Number.isFinite(cap) ? Math.trunc(cap) : 0;
|
|
314
454
|
return Math.max(0, Math.min(capped, policy.baseSendAllowance - counter.spent));
|
|
@@ -349,59 +489,104 @@ function createRequestExecutionBudgetWithLedger(
|
|
|
349
489
|
// Consulted last, because it is the only bound here that WRITES. A ledger entry booked
|
|
350
490
|
// for a dispatch a cheaper check above would have refused is spend this request never
|
|
351
491
|
// makes, and it would hold those tokens against the scope until retention expired.
|
|
352
|
-
|
|
492
|
+
budget.startRequest?.({ targetKey: intent.targetKey });
|
|
493
|
+
let spendProof: SpendReservationProof | undefined;
|
|
494
|
+
if (observer && !observer.charge({ targetKey: intent.targetKey, deferDispatch: true, onReserved: proof => { spendProof = proof; } })) {
|
|
495
|
+
return { allowed: false, reason: "spend-exhausted" };
|
|
496
|
+
}
|
|
353
497
|
|
|
354
498
|
// THE RESERVATION IS THE CHARGE. Deciding here and charging in `use()` left a window in
|
|
355
499
|
// which two legs read the same remainder, both received a permit, and both dispatched:
|
|
356
500
|
// one remaining send admitted two physical sends, which is the per-request multiplication
|
|
357
501
|
// this budget exists to stop. Everything is booked now; `release()` is the way back.
|
|
358
|
-
const previousTargetKey = lastTargetKey;
|
|
359
502
|
counter.spent += 1;
|
|
360
|
-
|
|
503
|
+
const receipt = { targetKey: intent.targetKey };
|
|
504
|
+
targetReservations.push(receipt);
|
|
505
|
+
if (intent.countedExternally === true) counter.pendingExternalSends.add(receipt);
|
|
361
506
|
if (drawsReserve) reserveSpent = true;
|
|
362
507
|
if (chargesAlternateTarget) alternateTargetSends += 1;
|
|
363
508
|
if (chargesTransition) targetTransitions += 1;
|
|
364
509
|
lastTargetKey = intent.targetKey;
|
|
365
510
|
|
|
511
|
+
let producer: ReturnType<NonNullable<RequestSendObserver["beginReporter"]>> | undefined;
|
|
512
|
+
let executing = false;
|
|
513
|
+
let physicalOrdinal: number | undefined;
|
|
514
|
+
const startPhysical = (): boolean => {
|
|
515
|
+
if (observer?.enforced !== true) return true;
|
|
516
|
+
const seed = observer.ensureSeed?.({ targetKey: intent.targetKey }, spendProof);
|
|
517
|
+
if (!seed) return false;
|
|
518
|
+
const ordinal = budget.claimPhysicalSend?.();
|
|
519
|
+
if (ordinal === undefined) return false;
|
|
520
|
+
producer = observer.beginReporter?.();
|
|
521
|
+
producer?.start(seed, ordinal);
|
|
522
|
+
physicalOrdinal = ordinal;
|
|
523
|
+
if (!executing) { try { producer?.report(1); } finally { producer?.close(); } }
|
|
524
|
+
return true;
|
|
525
|
+
};
|
|
366
526
|
let settled: "open" | "used" | "released" = "open";
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
527
|
+
const permit: SingleUseDispatchPermit = {
|
|
528
|
+
sendClass: intent.sendClass,
|
|
529
|
+
async execute<T>(run: () => Promise<T>): Promise<T> {
|
|
530
|
+
if (executing) throw new Error("Dispatch permit executor already active");
|
|
531
|
+
executing = true;
|
|
532
|
+
const binding: PhysicalSendReporter = () => {};
|
|
533
|
+
binding.bind = (target: SpendScopes & { targetKey?: string }) => physicalOrdinal === undefined || !producer
|
|
534
|
+
? true : producer.rebindTarget(target, physicalOrdinal);
|
|
535
|
+
try { return await activePhysicalReporters.run(binding, run); }
|
|
536
|
+
finally {
|
|
537
|
+
executing = false;
|
|
538
|
+
try { producer?.report(1); } finally { producer?.close(); }
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
use(): boolean {
|
|
542
|
+
if (settled !== "open") return false;
|
|
543
|
+
if (intent.countedExternally !== true && !startPhysical()) return false;
|
|
544
|
+
settled = "used";
|
|
545
|
+
if (intent.countedExternally !== true && spendProof) observer?.dispatch?.(spendProof);
|
|
546
|
+
return true;
|
|
547
|
+
},
|
|
548
|
+
assumeCharge(): boolean {
|
|
549
|
+
if (settled !== "open" || (intent.countedExternally === true && !counter.pendingExternalSends.has(receipt))) return false;
|
|
550
|
+
if (!startPhysical()) return false;
|
|
551
|
+
settled = "used";
|
|
552
|
+
// The booking this reservation made for an external reporter is now owned by the
|
|
553
|
+
// caller. Close only its own receipt so a later reporter cannot spend it again.
|
|
554
|
+
if (intent.countedExternally === true) counter.pendingExternalSends.delete(receipt);
|
|
555
|
+
if (spendProof) observer?.dispatch?.(spendProof);
|
|
556
|
+
return true;
|
|
557
|
+
},
|
|
558
|
+
release(): void {
|
|
559
|
+
if (settled !== "open") return;
|
|
560
|
+
settled = "released";
|
|
561
|
+
// An externally counted reservation the reporter already settled paid for a send
|
|
562
|
+
// that physically happened. Refunding it would hand the request a free send back.
|
|
563
|
+
if (intent.countedExternally === true) {
|
|
564
|
+
if (!counter.pendingExternalSends.delete(receipt)) return;
|
|
565
|
+
}
|
|
566
|
+
counter.spent -= 1;
|
|
567
|
+
observer?.refund(spendProof ?? null);
|
|
568
|
+
if (drawsReserve) reserveSpent = false;
|
|
569
|
+
if (chargesAlternateTarget) alternateTargetSends -= 1;
|
|
570
|
+
if (chargesTransition) targetTransitions -= 1;
|
|
571
|
+
targetReservations.splice(targetReservations.indexOf(receipt), 1);
|
|
572
|
+
lastTargetKey = targetReservations.at(-1)?.targetKey;
|
|
403
573
|
},
|
|
404
574
|
};
|
|
575
|
+
let preflightClaimed = false;
|
|
576
|
+
dispatchSpendProofs.set(permit, { owner: counter, report: () => {
|
|
577
|
+
if (!counter.pendingExternalSends.has(receipt)) return false;
|
|
578
|
+
if (spendProof) observer?.dispatch?.(spendProof);
|
|
579
|
+
counter.pendingExternalSends.delete(receipt);
|
|
580
|
+
// Reset-only helpers report just BEFORE calling the dispatch thunk. Its one use()
|
|
581
|
+
// remains available, but release/proof cannot refund or reuse this reported receipt.
|
|
582
|
+
return true;
|
|
583
|
+
}, claim: () => {
|
|
584
|
+
if (preflightClaimed || settled === "released"
|
|
585
|
+
|| (intent.countedExternally === true ? !counter.pendingExternalSends.has(receipt) : settled !== "open")) return undefined;
|
|
586
|
+
preflightClaimed = true;
|
|
587
|
+
return spendProof;
|
|
588
|
+
} });
|
|
589
|
+
return { allowed: true, permit };
|
|
405
590
|
},
|
|
406
591
|
};
|
|
407
592
|
sharedSendLedgers.set(budget, counter);
|
|
@@ -416,7 +601,8 @@ export function createRequestExecutionBudget(
|
|
|
416
601
|
const grant = createAmbiguousResendGrant();
|
|
417
602
|
return createRequestExecutionBudgetWithLedger(policy, logicalRequestId, {
|
|
418
603
|
spent: 0,
|
|
419
|
-
|
|
604
|
+
physicalStarted: 0,
|
|
605
|
+
pendingExternalSends: new Set(),
|
|
420
606
|
claimAmbiguousResend: grant.claimAmbiguousResend,
|
|
421
607
|
get ambiguousResendSpent(): boolean { return grant.ambiguousResendSpent; },
|
|
422
608
|
...(observer ? { observer } : {}),
|
|
@@ -460,7 +646,7 @@ const bridgedGrantClaims = new WeakMap<RequestExecutionBudget, { claimed: boolea
|
|
|
460
646
|
function ledgerFor(parent: RequestExecutionBudget): SharedSendLedger {
|
|
461
647
|
const existing = sharedSendLedgers.get(parent);
|
|
462
648
|
if (existing) return existing;
|
|
463
|
-
|
|
649
|
+
const pendingExternalSends = new Set<object>();
|
|
464
650
|
let bridged = bridgedGrantClaims.get(parent);
|
|
465
651
|
if (!bridged) {
|
|
466
652
|
bridged = { claimed: false };
|
|
@@ -470,8 +656,8 @@ function ledgerFor(parent: RequestExecutionBudget): SharedSendLedger {
|
|
|
470
656
|
return {
|
|
471
657
|
get spent(): number { return parent.used; },
|
|
472
658
|
set spent(next: number) { parent.used = next; },
|
|
473
|
-
|
|
474
|
-
|
|
659
|
+
pendingExternalSends,
|
|
660
|
+
physicalStarted: 0,
|
|
475
661
|
// Asked of the parent rather than counted here. A local counter is a SECOND grant: two
|
|
476
662
|
// scopes derived from one bridged parent, or one scope beside the parent it was derived
|
|
477
663
|
// from, each replaced an unknown-state send once. Pending bookings and the durable-spend
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants, lstatSync, openSync, fstatSync, readFileSync, closeSync, type Stats } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/** Read an existing single-owner salt without creating or hardening any file. */
|
|
6
|
+
export function readExistingSpendSalt(home: string): string | undefined {
|
|
7
|
+
let fd: number | undefined;
|
|
8
|
+
let salt: string | undefined;
|
|
9
|
+
try {
|
|
10
|
+
const path = join(home, "spend-ledger.salt");
|
|
11
|
+
const before = lstatSync(path);
|
|
12
|
+
if (!before.isFile() || before.nlink !== 1) return undefined;
|
|
13
|
+
const matches = (stat: Stats): boolean => stat.isFile() && stat.nlink === 1
|
|
14
|
+
&& stat.dev === before.dev && stat.ino === before.ino;
|
|
15
|
+
fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
16
|
+
if (!matches(fstatSync(fd))) return undefined;
|
|
17
|
+
const contents = readFileSync(fd, "utf8").trim();
|
|
18
|
+
if (matches(fstatSync(fd)) && matches(lstatSync(path)) && /^[0-9a-f]{32,}$/.test(contents)) salt = contents;
|
|
19
|
+
} catch { salt = undefined; }
|
|
20
|
+
finally {
|
|
21
|
+
if (fd !== undefined) {
|
|
22
|
+
try { closeSync(fd); } catch { salt = undefined; }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return salt;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The one owner of the spend ledger's salted alias hash. The ledger, config diagnostics and
|
|
30
|
+
* runtime policy validation must all compute the same alias for the same (salt, domain, id),
|
|
31
|
+
* so the hash lives here and nowhere else. The raw id never leaves this function.
|
|
32
|
+
*/
|
|
33
|
+
export function hashSpendAlias(salt: string, kind: string, id: string): string {
|
|
34
|
+
return createHash("sha256").update(salt).update("\u0000").update(kind).update("\u0000").update(id)
|
|
35
|
+
.digest("hex").slice(0, 32);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isAlias = (value: unknown): value is string =>
|
|
39
|
+
typeof value === "string" && /^[0-9a-f]{32}$/.test(value);
|
|
40
|
+
|
|
41
|
+
/** Shape and optional roster check, outside `spend` so old schemas keep configured ceilings. */
|
|
42
|
+
export function spendPoolAliasesError(value: unknown, providerIds?: Iterable<string>): string | undefined {
|
|
43
|
+
if (value === undefined) return undefined;
|
|
44
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "spendPoolAliases must be an object";
|
|
45
|
+
if (Object.keys(value).length > 4_096) return "spendPoolAliases has too many entries";
|
|
46
|
+
const providers = providerIds === undefined ? undefined : new Set(providerIds);
|
|
47
|
+
for (const [alias, provider] of Object.entries(value)) {
|
|
48
|
+
if (!isAlias(alias) || typeof provider !== "string" || !provider.trim() || provider.trim() !== provider || provider.length > 256) {
|
|
49
|
+
return "spendPoolAliases requires exact 32-character salted pool aliases and nonempty provider IDs";
|
|
50
|
+
}
|
|
51
|
+
if (providers && !providers.has(provider)) return "spendPoolAliases targets a provider that is not configured";
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Semantic owner check: an alias that is the salted pool alias of a configured provider P
|
|
58
|
+
* belongs to P. Mapping it to any other provider Q would move P's own history into Q's pool,
|
|
59
|
+
* so `aliases[h("pool", P)] = Q` with Q !== P is rejected. Re-run whenever the provider set
|
|
60
|
+
* changes: adding P later turns an existing mapping into a conflict.
|
|
61
|
+
*/
|
|
62
|
+
export function validatePoolAliasOwners(
|
|
63
|
+
aliases: unknown,
|
|
64
|
+
salt: string,
|
|
65
|
+
providerIds: Iterable<string>,
|
|
66
|
+
): string | undefined {
|
|
67
|
+
const shape = spendPoolAliasesError(aliases);
|
|
68
|
+
if (shape) return shape;
|
|
69
|
+
if (aliases === undefined) return undefined;
|
|
70
|
+
const mapping = aliases as Record<string, string>;
|
|
71
|
+
for (const provider of new Set(providerIds)) {
|
|
72
|
+
const owned = hashSpendAlias(salt, "pool", provider);
|
|
73
|
+
const target = Object.hasOwn(mapping, owned) ? mapping[owned] : undefined;
|
|
74
|
+
if (target !== undefined && target !== provider) {
|
|
75
|
+
return "spendPoolAliases maps a configured provider's own pool alias to a different provider";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|