@bitkyc08/opencodex 2.65.0 → 2.66.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 +14 -0
- package/gui/dist/assets/App-DyHlFeNB.js +51 -0
- package/gui/dist/assets/App-gkoJIEOV.css +1 -0
- package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-CI8cVKWu.js} +1 -1
- package/gui/dist/assets/index-BiwnVAUm.js +86 -0
- package/gui/dist/assets/index-ComVsdSk.css +1 -0
- package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-_WyYzbMt.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +80 -0
- package/src/adapters/anthropic/passthrough.ts +221 -0
- package/src/adapters/anthropic-image-codec.ts +56 -19
- package/src/adapters/anthropic-image-normalize.ts +20 -10
- package/src/adapters/anthropic.ts +55 -24
- package/src/adapters/coding-agent/protocol.ts +29 -19
- package/src/adapters/cursor/live-transport.ts +18 -3
- package/src/adapters/cursor/native-exec-shell.ts +18 -63
- package/src/adapters/cursor/native-exec.ts +5 -2
- package/src/adapters/cursor/native-foreground-shell.ts +44 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -1
- package/src/adapters/exec-tool-result-normalize.ts +4 -1
- package/src/adapters/google.ts +12 -0
- package/src/adapters/ollama-native.ts +32 -1
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
- package/src/adapters/openai-chat.ts +6 -4
- package/src/adapters/openai-responses/passthrough.ts +1 -0
- package/src/adapters/openai-responses/reasoning.ts +45 -1
- package/src/bridge/response-json.ts +10 -2
- package/src/chat/outbound.ts +104 -52
- package/src/claude/claude-code-block.ts +16 -0
- package/src/claude/context-windows.ts +52 -4
- package/src/claude/desktop-first-party.ts +25 -8
- package/src/claude/inbound.ts +22 -0
- package/src/claude/intercept/connect-proxy.ts +37 -4
- package/src/claude/intercept/proxy-auth.ts +166 -0
- package/src/claude/intercept/runtime.ts +21 -0
- package/src/claude/intercept/settings.ts +33 -12
- package/src/claude/outbound.ts +45 -27
- package/src/cli/account-extended.ts +47 -15
- package/src/cli/account-target.ts +115 -0
- package/src/cli/account.ts +20 -28
- package/src/cli/agent.ts +10 -1
- package/src/cli/api-protocols.ts +206 -0
- package/src/cli/capabilities.ts +86 -0
- package/src/cli/combo.ts +2 -2
- package/src/cli/connect.ts +127 -3
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/doctor.ts +33 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/link.ts +288 -0
- package/src/cli/registry.ts +23 -0
- package/src/cli/runtime-api.ts +79 -0
- package/src/cli/system-command.ts +21 -2
- package/src/client/connect.ts +125 -23
- package/src/client/hub-relay.ts +12 -8
- package/src/client/link-join.ts +323 -0
- package/src/client/link-relay.ts +241 -0
- package/src/client/link-state.ts +110 -0
- package/src/client/link-teardown.ts +63 -0
- package/src/client/link-tunnel.ts +465 -0
- package/src/client/machine-listener.ts +14 -5
- package/src/client/runtime.ts +91 -42
- package/src/client/state.ts +4 -0
- package/src/codex/catalog/build-entries.ts +4 -0
- package/src/codex/catalog/derive-entry.ts +5 -0
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +4 -1
- package/src/codex/desktop-switches.ts +88 -8
- package/src/codex/inject/plan.ts +16 -3
- package/src/codex/inject.ts +3 -0
- package/src/codex/internal/catalog-writer.ts +13 -2
- package/src/combos/failover.ts +3 -2
- package/src/combos/index.ts +12 -0
- package/src/combos/jev.ts +646 -0
- package/src/combos/request.ts +5 -0
- package/src/combos/resolve.ts +17 -15
- package/src/combos/types.ts +47 -3
- package/src/config/atomic-write.ts +3 -0
- package/src/config/live-reconcile.ts +20 -4
- package/src/config/persist-unlocked.ts +20 -7
- package/src/config/schema/config-schema.ts +16 -0
- package/src/config/schema/leaf-validators.ts +38 -1
- package/src/generated/compatibility-version.json +416 -128
- package/src/integrations/omp-yaml-source.ts +109 -4
- package/src/lab/automation/orchestrator.ts +60 -4
- package/src/lab/events/limits.ts +1 -1
- package/src/lib/bounded-body.ts +55 -38
- package/src/lib/config-ownership.ts +11 -22
- package/src/lib/local-desktop-snapshot-capability.ts +56 -0
- package/src/lib/local-management-capability.ts +25 -3
- package/src/lib/optional-shutdown-hooks.ts +17 -0
- package/src/lib/state-store-registrations.ts +2 -2
- package/src/lib/windows-elevation.ts +62 -25
- package/src/lib/windows-secret-acl.ts +114 -23
- package/src/link/admission-wait.ts +73 -0
- package/src/link/compensation.ts +100 -0
- package/src/link/fingerprint.ts +21 -0
- package/src/link/paths.ts +19 -0
- package/src/link/ports.ts +12 -0
- package/src/link/routes.ts +32 -0
- package/src/link/ssh-argv.ts +170 -0
- package/src/link/ssh-config.ts +152 -0
- package/src/link/ssh-runner.ts +161 -0
- package/src/link/status-projection.ts +96 -0
- package/src/link/store.ts +139 -0
- package/src/link/supervisor.ts +404 -0
- package/src/link/tunnel-state.ts +91 -0
- package/src/protocols/baseline.ts +88 -0
- package/src/protocols/codecs/chat.ts +17 -0
- package/src/protocols/codecs/messages.ts +18 -0
- package/src/protocols/codecs/responses.ts +17 -0
- package/src/protocols/contract.ts +185 -0
- package/src/protocols/dto.ts +267 -0
- package/src/protocols/encoders/adapter-events.ts +876 -0
- package/src/protocols/encoders/chat.ts +243 -0
- package/src/protocols/encoders/messages.ts +462 -0
- package/src/protocols/envelope.ts +43 -0
- package/src/protocols/features.ts +295 -0
- package/src/protocols/guard.ts +33 -0
- package/src/protocols/opaque-state.ts +132 -0
- package/src/protocols/path.ts +39 -0
- package/src/protocols/plan-snapshot.ts +226 -0
- package/src/protocols/plan.ts +192 -0
- package/src/protocols/provider-summary.ts +101 -0
- package/src/protocols/settings.ts +118 -0
- package/src/protocols/shadow-plan.ts +36 -0
- package/src/protocols/shadow.ts +62 -0
- package/src/protocols/trace.ts +335 -0
- package/src/providers/model-rename-migration.ts +3 -3
- package/src/providers/registry/entries-core.ts +6 -1
- package/src/providers/registry/entries-extended.ts +15 -0
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/types.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/remote-control/workspace-hub.ts +5 -4
- package/src/responses/apply-patch-envelope.ts +6 -1
- package/src/responses/code-mode-shell-input.ts +4 -2
- package/src/responses/freeform-wrapper-scan.ts +51 -24
- package/src/responses/progressive-freeform-input.ts +14 -20
- package/src/responses/spill-store.ts +187 -33
- package/src/responses/state/snapshot-select.ts +49 -0
- package/src/responses/state/spill-inspect.ts +46 -0
- package/src/responses/state/spill-queue.ts +16 -0
- package/src/responses/state.ts +51 -27
- package/src/server/audio-client.ts +8 -3
- package/src/server/audio-upstream.ts +9 -4
- package/src/server/auth-cors.ts +25 -8
- package/src/server/chat-completions.ts +71 -11
- package/src/server/chat-native-eligibility.ts +74 -0
- package/src/server/chat-native.ts +139 -74
- package/src/server/claude-messages.ts +140 -22
- package/src/server/hub-usage.ts +4 -3
- package/src/server/index/link-listener.ts +195 -0
- package/src/server/index/optional-listeners.ts +115 -0
- package/src/server/index/serve-options.ts +44 -10
- package/src/server/index.ts +10 -9
- package/src/server/inference/attempt.ts +51 -0
- package/src/server/inference/client-encoder-delivery.ts +235 -0
- package/src/server/inference/client-wire-log.ts +49 -0
- package/src/server/inference/client-wire.ts +69 -0
- package/src/server/inference/context.ts +15 -0
- package/src/server/inference/final-log.ts +37 -0
- package/src/server/local-desktop-snapshot-auth.ts +57 -0
- package/src/server/management/agent-settings-routes.ts +15 -13
- package/src/server/management/api-access.ts +11 -1
- package/src/server/management/config-routes.ts +3 -6
- package/src/server/management/context.ts +25 -0
- package/src/server/management/link-routes.ts +535 -0
- package/src/server/management/logs-usage-routes.ts +31 -1
- package/src/server/management/native-integration-routes.ts +6 -11
- package/src/server/management/oauth-account-routes.ts +42 -16
- package/src/server/management/protocol-routes.ts +159 -0
- package/src/server/management/protocol-settings-patch.ts +145 -0
- package/src/server/management/provider-patch-transaction.ts +61 -0
- package/src/server/management/provider-routes.ts +39 -18
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sidebar-routes.ts +8 -3
- package/src/server/management/system-routes.ts +18 -1
- package/src/server/management/usage-aggregate-cache.ts +206 -6
- package/src/server/management-api.ts +26 -1
- package/src/server/management-auth.ts +19 -5
- package/src/server/messages-native-eligibility.ts +161 -0
- package/src/server/messages-native-oauth.ts +149 -0
- package/src/server/messages-native.ts +790 -0
- package/src/server/relay.ts +9 -0
- package/src/server/request-log-filter.ts +92 -0
- package/src/server/request-log.ts +31 -70
- package/src/server/responses/adapter-delivery.ts +43 -16
- package/src/server/responses/agent-task-recovery.ts +90 -22
- package/src/server/responses/codex-ws-exchange.ts +4 -0
- package/src/server/responses/codex-ws-wire.ts +10 -1
- package/src/server/responses/core-combo-native.ts +323 -0
- package/src/server/responses/core-combo.ts +224 -13
- package/src/server/responses/core-options.ts +22 -0
- package/src/server/responses/core.ts +14 -26
- package/src/server/responses/request-sidecar-auth.ts +8 -3
- package/src/server/responses/run-turn-execution.ts +257 -63
- package/src/server/responses/sidecar-execution.ts +7 -4
- package/src/server/system-env.ts +7 -0
- package/src/service/diagnostics.ts +7 -1
- package/src/service/launchd.ts +26 -37
- package/src/service/windows-ops.ts +24 -20
- package/src/types/config.ts +58 -1
- package/src/types.ts +2 -0
- package/src/update/transactional-install.d.mts +0 -1
- package/src/update/transactional-install.mjs +17 -17
- package/src/usage/jev-stats.ts +495 -0
- package/src/usage/log.ts +24 -3
- package/src/web-search/run-turn-loop.ts +568 -0
- package/gui/dist/assets/App-8NMiZxT0.css +0 -1
- package/gui/dist/assets/App-DGLte6IR.js +0 -50
- package/gui/dist/assets/index--EWgGQvZ.css +0 -1
- package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
package/src/server/hub-usage.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { MAX_HUB_USAGE_BYTES, parseHubUsage } from "../remote/hub-usage";
|
|
|
3
3
|
import { parseRange, parseUsageSurface, USAGE_RANGES, USAGE_SURFACES } from "../usage/summary";
|
|
4
4
|
import { parseUsageTimeWindow } from "../usage/time-range";
|
|
5
5
|
import { getFilteredUsageAggregate } from "./management/usage-aggregate-cache";
|
|
6
|
-
import { isAllowedRequestOrigin, resolveDataPlaneAdmissionSecret, withCors, type RequestPolicyView } from "./auth-cors";
|
|
6
|
+
import { isAllowedRequestOrigin, resolveDataPlaneAdmissionSecret, withCors, type DataPlaneAdmissionOptions, type RequestPolicyView } from "./auth-cors";
|
|
7
7
|
|
|
8
8
|
/** Read only the ledger rows belonging to an explicitly authenticated configured key. */
|
|
9
9
|
export async function handleHubUsage(req: Request, config: OcxConfig, policy: RequestPolicyView): Promise<Response> {
|
|
@@ -12,7 +12,8 @@ export async function handleHubUsage(req: Request, config: OcxConfig, policy: Re
|
|
|
12
12
|
}), req, policy);
|
|
13
13
|
const error = (code: string, message: string, status: number) => reply({ error: { code, message } }, status);
|
|
14
14
|
const token = req.headers.get("x-opencodex-api-key") ?? "";
|
|
15
|
-
const
|
|
15
|
+
const options: DataPlaneAdmissionOptions = { linkIngress: policy.linkIngress?.allowedKeyIds };
|
|
16
|
+
const admission = resolveDataPlaneAdmissionSecret(token, config, "dedicated", options);
|
|
16
17
|
if (admission?.kind !== "configured") return error("hub_usage_unauthorized", "A configured client data key is required", 401);
|
|
17
18
|
if (!isAllowedRequestOrigin(req, policy)) return error("origin_rejected", "Cross-origin request blocked", 403);
|
|
18
19
|
if (config.runtimeRole !== "hub") return error("hub_usage_not_a_hub", "Usage is served here only by a hub", 404);
|
|
@@ -38,7 +39,7 @@ export async function handleHubUsage(req: Request, config: OcxConfig, policy: Re
|
|
|
38
39
|
const aggregate = await getFilteredUsageAggregate({
|
|
39
40
|
apiKeyId: keyId, provider: query.get("provider"), model: query.get("model"),
|
|
40
41
|
}, window);
|
|
41
|
-
const current = resolveDataPlaneAdmissionSecret(token, config);
|
|
42
|
+
const current = resolveDataPlaneAdmissionSecret(token, config, "dedicated", options);
|
|
42
43
|
if (current?.kind !== "configured" || current.keyId !== keyId) {
|
|
43
44
|
return error("hub_usage_unauthorized", "The client key changed during the read", 401);
|
|
44
45
|
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { Server } from "bun";
|
|
2
|
+
import {
|
|
3
|
+
emptyLinkStore,
|
|
4
|
+
readLinkStore,
|
|
5
|
+
type LinkStore,
|
|
6
|
+
writeLinkStore,
|
|
7
|
+
} from "../../link/store";
|
|
8
|
+
import { linkStorePath } from "../../link/paths";
|
|
9
|
+
import { linkRouteAllowed } from "../../link/routes";
|
|
10
|
+
|
|
11
|
+
export const LINK_INGRESS_HOSTNAME = "opencodex-link.invalid";
|
|
12
|
+
|
|
13
|
+
export interface LinkListenerStartContext<T> {
|
|
14
|
+
dispatch: (req: Request, server: Server<T>) => Promise<Response>;
|
|
15
|
+
maxRequestBodySize: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface LinkListenerDeps {
|
|
19
|
+
storePath?: string;
|
|
20
|
+
readStore?: (path: string) => LinkStore;
|
|
21
|
+
writeStore?: (path: string, store: LinkStore) => void;
|
|
22
|
+
serve?: (options: Parameters<typeof Bun.serve>[0]) => Server<unknown>;
|
|
23
|
+
warn?: (message: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type LinkListenerStatus = {
|
|
27
|
+
state: "off" | "listening" | "failed";
|
|
28
|
+
port: number | null;
|
|
29
|
+
reason: string | null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface LinkListenerLifecycle<T> {
|
|
33
|
+
ownsListener(server: Server<T>): boolean;
|
|
34
|
+
start(ctx: LinkListenerStartContext<T>): void;
|
|
35
|
+
ensureStarted(): Promise<void>;
|
|
36
|
+
linkAdmissionKeyIds(): ReadonlySet<string>;
|
|
37
|
+
onAuthenticatedCatalog(listener: (apiKeyId: string) => void): () => void;
|
|
38
|
+
notifyAuthenticatedCatalog(apiKeyId: string): void;
|
|
39
|
+
status(): LinkListenerStatus;
|
|
40
|
+
close(): Promise<void>;
|
|
41
|
+
stop(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { linkRouteAllowed } from "../../link/routes";
|
|
45
|
+
|
|
46
|
+
function closeWithoutAwait<T>(server: Server<T>): void {
|
|
47
|
+
try { void server.stop(true).catch(() => {}); } catch { /* preserve the bind failure */ }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createLinkListenerLifecycle<T>(deps: LinkListenerDeps = {}): LinkListenerLifecycle<T> {
|
|
51
|
+
const storePath = deps.storePath ?? linkStorePath();
|
|
52
|
+
const readStore = deps.readStore ?? readLinkStore;
|
|
53
|
+
const writeStore = deps.writeStore ?? writeLinkStore;
|
|
54
|
+
const serve = deps.serve ?? (options => Bun.serve(options));
|
|
55
|
+
const warn = deps.warn ?? ((message: string) => console.warn(message));
|
|
56
|
+
let listener: Server<T> | null = null;
|
|
57
|
+
let startContext: LinkListenerStartContext<T> | undefined;
|
|
58
|
+
let ensureFlight: Promise<void> | undefined;
|
|
59
|
+
let closeFlight: Promise<void> | undefined;
|
|
60
|
+
let stopped = false;
|
|
61
|
+
const authenticatedCatalogListeners = new Set<(apiKeyId: string) => void>();
|
|
62
|
+
let lifecycleStatus: LinkListenerStatus = { state: "off", port: null, reason: null };
|
|
63
|
+
|
|
64
|
+
const setStatus = (state: LinkListenerStatus["state"], port: number | null, reason: string | null): void => {
|
|
65
|
+
lifecycleStatus = { state, port, reason };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const reportFailure = (operation: string, error: unknown, reason: string): void => {
|
|
69
|
+
setStatus("failed", null, reason);
|
|
70
|
+
warn(`⚠ hub-link listener ${operation} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const readStoreForAdmission = (): LinkStore => {
|
|
74
|
+
try { return readStore(storePath); } catch { return emptyLinkStore(); }
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const bindIfNeeded = (): void => {
|
|
78
|
+
if (stopped || listener || !startContext) return;
|
|
79
|
+
let store: LinkStore;
|
|
80
|
+
try {
|
|
81
|
+
store = readStore(storePath);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
reportFailure("store read", error, "bind");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (store.links.length === 0) {
|
|
87
|
+
setStatus("off", null, null);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const requestedPort = store.listenerPort ?? 0;
|
|
91
|
+
let bound: Server<unknown>;
|
|
92
|
+
try {
|
|
93
|
+
bound = serve({
|
|
94
|
+
hostname: "127.0.0.1",
|
|
95
|
+
port: requestedPort,
|
|
96
|
+
maxRequestBodySize: startContext.maxRequestBodySize,
|
|
97
|
+
fetch: (req: Request, server: Server<unknown>) => startContext!.dispatch(req, server as Server<T>),
|
|
98
|
+
} as Parameters<typeof Bun.serve>[0]);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
reportFailure("bind", error, "bind");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (stopped) {
|
|
104
|
+
closeWithoutAwait(bound);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (store.listenerPort === null) {
|
|
108
|
+
const port = bound.port;
|
|
109
|
+
if (!port || port === 0) {
|
|
110
|
+
closeWithoutAwait(bound);
|
|
111
|
+
reportFailure("bind", new Error("Bun did not report a concrete listener port"), "bind");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const current = readStore(storePath);
|
|
116
|
+
if (current.listenerPort !== null && current.listenerPort !== port) {
|
|
117
|
+
// Another writer fixed a different port while this bind ran. Tunnels target the stored
|
|
118
|
+
// port, so serving on this one would strand them: give it up and let the next
|
|
119
|
+
// ensureStarted() bind the stored port.
|
|
120
|
+
closeWithoutAwait(bound);
|
|
121
|
+
reportFailure("listenerPort persistence", new Error(`stored port ${current.listenerPort} differs from bound port ${port}`), "persist");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
writeStore(storePath, { ...current, listenerPort: port });
|
|
125
|
+
} catch (error) {
|
|
126
|
+
closeWithoutAwait(bound);
|
|
127
|
+
reportFailure("listenerPort persistence", error, "persist");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
listener = bound as Server<T>;
|
|
132
|
+
setStatus("listening", bound.port ?? (requestedPort > 0 ? requestedPort : null), null);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const ensureStarted = (): Promise<void> => {
|
|
136
|
+
if (stopped) return Promise.resolve();
|
|
137
|
+
if (ensureFlight) return ensureFlight;
|
|
138
|
+
const flight = (async () => {
|
|
139
|
+
if (closeFlight) await closeFlight;
|
|
140
|
+
if (stopped) return;
|
|
141
|
+
bindIfNeeded();
|
|
142
|
+
})();
|
|
143
|
+
ensureFlight = flight.finally(() => {
|
|
144
|
+
if (ensureFlight === sharedFlight) ensureFlight = undefined;
|
|
145
|
+
});
|
|
146
|
+
const sharedFlight = ensureFlight;
|
|
147
|
+
return sharedFlight;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const close = (): Promise<void> => {
|
|
151
|
+
if (closeFlight) return closeFlight;
|
|
152
|
+
const flight = (async () => {
|
|
153
|
+
if (ensureFlight) await ensureFlight;
|
|
154
|
+
const current = listener;
|
|
155
|
+
listener = null;
|
|
156
|
+
if (current) await current.stop(true);
|
|
157
|
+
// Closing is the caller saying "no links now": an earlier bind or persist failure no
|
|
158
|
+
// longer describes anything, so the status reads off either way.
|
|
159
|
+
setStatus("off", null, null);
|
|
160
|
+
})();
|
|
161
|
+
closeFlight = flight.finally(() => {
|
|
162
|
+
closeFlight = undefined;
|
|
163
|
+
});
|
|
164
|
+
return closeFlight;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
ownsListener: server => listener !== null && listener === server,
|
|
169
|
+
start(ctx) {
|
|
170
|
+
startContext = ctx;
|
|
171
|
+
bindIfNeeded();
|
|
172
|
+
},
|
|
173
|
+
ensureStarted,
|
|
174
|
+
linkAdmissionKeyIds() {
|
|
175
|
+
return new Set(readStoreForAdmission().links.map(link => link.apiKeyId));
|
|
176
|
+
},
|
|
177
|
+
onAuthenticatedCatalog(listener) {
|
|
178
|
+
authenticatedCatalogListeners.add(listener);
|
|
179
|
+
return () => { authenticatedCatalogListeners.delete(listener); };
|
|
180
|
+
},
|
|
181
|
+
notifyAuthenticatedCatalog(apiKeyId) {
|
|
182
|
+
for (const listener of authenticatedCatalogListeners) listener(apiKeyId);
|
|
183
|
+
},
|
|
184
|
+
status() {
|
|
185
|
+
return { ...lifecycleStatus };
|
|
186
|
+
},
|
|
187
|
+
close,
|
|
188
|
+
async stop() {
|
|
189
|
+
stopped = true;
|
|
190
|
+
await close();
|
|
191
|
+
startContext = undefined;
|
|
192
|
+
setStatus("off", null, null);
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Server } from "bun";
|
|
2
|
+
import type { OcxConfig } from "../../types";
|
|
3
|
+
import {
|
|
4
|
+
createClaudeInterceptLifecycle,
|
|
5
|
+
type ClaudeInterceptLifecycle,
|
|
6
|
+
} from "./claude-intercept-lifecycle";
|
|
7
|
+
import {
|
|
8
|
+
createLinkListenerLifecycle,
|
|
9
|
+
linkRouteAllowed,
|
|
10
|
+
type LinkListenerDeps,
|
|
11
|
+
type LinkListenerLifecycle,
|
|
12
|
+
type LinkListenerStatus,
|
|
13
|
+
} from "./link-listener";
|
|
14
|
+
import { createLinkSupervisor, type LinkSupervisor } from "../../link/supervisor";
|
|
15
|
+
import { saveConfigPreservingClaudeCode } from "../../config/live-reconcile";
|
|
16
|
+
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
17
|
+
export { LINK_INGRESS_HOSTNAME } from "./link-listener";
|
|
18
|
+
import type { ServerIngress } from "./serve-options";
|
|
19
|
+
|
|
20
|
+
export interface OptionalListenerStartContext<T> {
|
|
21
|
+
config: OcxConfig;
|
|
22
|
+
publicPort: number;
|
|
23
|
+
requestedPort?: number;
|
|
24
|
+
maxRequestBodySize: number;
|
|
25
|
+
dispatch: (req: Request, server: Server<T>) => Promise<Response>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface OptionalListenerSet<T> {
|
|
29
|
+
ingressOf(server: Server<T>): ServerIngress | undefined;
|
|
30
|
+
linkRouteAllowed(url: URL, req: Request): boolean;
|
|
31
|
+
linkAdmissionKeyIds(): ReadonlySet<string>;
|
|
32
|
+
onAuthenticatedCatalog(listener: (apiKeyId: string) => void): () => void;
|
|
33
|
+
notifyAuthenticatedCatalog(apiKeyId: string): void;
|
|
34
|
+
status(): LinkListenerStatus;
|
|
35
|
+
linkStatus(): LinkListenerStatus;
|
|
36
|
+
linkSupervisor(): LinkSupervisor;
|
|
37
|
+
start(ctx: OptionalListenerStartContext<T>): void;
|
|
38
|
+
ensureStarted(): Promise<void>;
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
registerSupervisorStop(stop: () => Promise<void>): () => void;
|
|
41
|
+
stop(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createOptionalListenerSet<T>(linkDeps: LinkListenerDeps = {}): OptionalListenerSet<T> {
|
|
45
|
+
const claudeIntercept: ClaudeInterceptLifecycle<T> = createClaudeInterceptLifecycle<T>();
|
|
46
|
+
const linkListener: LinkListenerLifecycle<T> = createLinkListenerLifecycle<T>(linkDeps);
|
|
47
|
+
let activeConfig: OcxConfig | undefined;
|
|
48
|
+
const supervisor = createLinkSupervisor({
|
|
49
|
+
apiKeys: () => activeConfig?.apiKeys ?? [],
|
|
50
|
+
revokeApiKey: id => {
|
|
51
|
+
if (!activeConfig) return false;
|
|
52
|
+
const before = activeConfig.apiKeys ?? [];
|
|
53
|
+
if (!before.some(key => key.id === id)) return false;
|
|
54
|
+
activeConfig.apiKeys = before.filter(key => key.id !== id);
|
|
55
|
+
try {
|
|
56
|
+
saveConfigPreservingClaudeCode(activeConfig);
|
|
57
|
+
reconcileLiveStateStores();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
activeConfig.apiKeys = before;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
let unregisterSupervisorAdmission: (() => void) | undefined;
|
|
66
|
+
let supervisorStop: (() => Promise<void>) | undefined;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
ingressOf(server) {
|
|
70
|
+
if (linkListener.ownsListener(server)) return "hub-link";
|
|
71
|
+
if (claudeIntercept.ownsListener(server)) return "claude-intercept";
|
|
72
|
+
return undefined;
|
|
73
|
+
},
|
|
74
|
+
linkRouteAllowed,
|
|
75
|
+
linkAdmissionKeyIds: () => linkListener.linkAdmissionKeyIds(),
|
|
76
|
+
onAuthenticatedCatalog: listener => linkListener.onAuthenticatedCatalog(listener),
|
|
77
|
+
notifyAuthenticatedCatalog: apiKeyId => linkListener.notifyAuthenticatedCatalog(apiKeyId),
|
|
78
|
+
status: () => linkListener.status(),
|
|
79
|
+
linkStatus: () => linkListener.status(),
|
|
80
|
+
linkSupervisor: () => supervisor,
|
|
81
|
+
start(ctx) {
|
|
82
|
+
activeConfig = ctx.config;
|
|
83
|
+
linkListener.start({ dispatch: ctx.dispatch, maxRequestBodySize: ctx.maxRequestBodySize });
|
|
84
|
+
unregisterSupervisorAdmission ??= linkListener.onAuthenticatedCatalog(apiKeyId => supervisor.notifyAuthenticatedRequest?.(apiKeyId));
|
|
85
|
+
supervisor.start();
|
|
86
|
+
supervisorStop = () => supervisor.stop();
|
|
87
|
+
claudeIntercept.start({
|
|
88
|
+
config: ctx.config,
|
|
89
|
+
publicPort: ctx.publicPort,
|
|
90
|
+
requestedPort: ctx.requestedPort,
|
|
91
|
+
maxRequestBodySize: ctx.maxRequestBodySize,
|
|
92
|
+
dispatch: ctx.dispatch,
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
ensureStarted: () => linkListener.ensureStarted(),
|
|
96
|
+
close: () => linkListener.close(),
|
|
97
|
+
registerSupervisorStop(stop) {
|
|
98
|
+
supervisorStop = stop;
|
|
99
|
+
return () => {
|
|
100
|
+
if (supervisorStop === stop) supervisorStop = undefined;
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
async stop() {
|
|
104
|
+
let failure: unknown;
|
|
105
|
+
if (supervisorStop) {
|
|
106
|
+
try { await supervisorStop(); } catch (error) { failure = error; }
|
|
107
|
+
}
|
|
108
|
+
try { await linkListener.stop(); } catch (error) { failure ??= error; }
|
|
109
|
+
try { await claudeIntercept.stop(); } catch (error) { failure ??= error; }
|
|
110
|
+
unregisterSupervisorAdmission?.();
|
|
111
|
+
unregisterSupervisorAdmission = undefined;
|
|
112
|
+
if (failure) throw failure;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -93,6 +93,7 @@ import {
|
|
|
93
93
|
isAllowedRequestOrigin,
|
|
94
94
|
isAllowedManagementOrigin,
|
|
95
95
|
isApiAuthRequired,
|
|
96
|
+
isLoopbackHostname,
|
|
96
97
|
jsonResponse,
|
|
97
98
|
admissionFields,
|
|
98
99
|
resolveApiAuth,
|
|
@@ -101,6 +102,7 @@ import {
|
|
|
101
102
|
withCors,
|
|
102
103
|
withManagementCors,
|
|
103
104
|
} from "../auth-cors";
|
|
105
|
+
import { managementSessionIssuance } from "../management-auth";
|
|
104
106
|
import { resolveAdmissionModelScope, routeAllowedByScope } from "../admission-model-scope";
|
|
105
107
|
import {
|
|
106
108
|
disableResponsesRequestTimeout,
|
|
@@ -199,7 +201,12 @@ import { readyProtocolMetadata } from "../../remote/protocol";
|
|
|
199
201
|
import { modelCapabilityFields } from "../models-capabilities";
|
|
200
202
|
import { createWebsocketHandler } from "./websocket-handler";
|
|
201
203
|
|
|
202
|
-
export type ServerIngress = "public" | "unauthenticated-loopback" | "hub-management" | "claude-intercept";
|
|
204
|
+
export type ServerIngress = "public" | "unauthenticated-loopback" | "hub-management" | "claude-intercept" | "hub-link";
|
|
205
|
+
|
|
206
|
+
export function trustedLoopbackForIngress(ingress: ServerIngress, hostname: string): boolean {
|
|
207
|
+
return ingress === "unauthenticated-loopback"
|
|
208
|
+
|| (ingress === "public" && isLoopbackHostname(hostname));
|
|
209
|
+
}
|
|
203
210
|
|
|
204
211
|
/**
|
|
205
212
|
* Routes the Claude intercept TLS listener may reach. Everything else on that socket is relayed
|
|
@@ -219,6 +226,9 @@ export interface ServeOptionsContext {
|
|
|
219
226
|
ingressForServer: (requestServer: Server<WsData>) => ServerIngress;
|
|
220
227
|
loopbackRouteAllowed: (url: URL, req: Request) => boolean;
|
|
221
228
|
managementIngressRouteAllowed: (url: URL, req: Request) => boolean;
|
|
229
|
+
linkRouteAllowed: (url: URL, req: Request) => boolean;
|
|
230
|
+
onAuthenticatedCatalog: (apiKeyId: string) => void;
|
|
231
|
+
linkPolicy: () => RequestPolicyView;
|
|
222
232
|
packageTreeChangedResponse: (
|
|
223
233
|
req: Request,
|
|
224
234
|
policy: RequestPolicyView,
|
|
@@ -261,6 +271,8 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
261
271
|
ingressForServer,
|
|
262
272
|
loopbackRouteAllowed,
|
|
263
273
|
managementIngressRouteAllowed,
|
|
274
|
+
linkRouteAllowed, onAuthenticatedCatalog,
|
|
275
|
+
linkPolicy,
|
|
264
276
|
packageTreeChangedResponse,
|
|
265
277
|
serverBusyResponse,
|
|
266
278
|
runAdmittedHttpTurn,
|
|
@@ -293,6 +305,8 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
293
305
|
maxRequestBodySize: inboundBodyLimitBytes,
|
|
294
306
|
async fetch(req: Request, requestServer: Server<WsData>): Promise<Response> {
|
|
295
307
|
const ingress = ingressForServer(requestServer);
|
|
308
|
+
const requestUrl = codexCompatibleUrl(req.url);
|
|
309
|
+
const linkPolicyView = ingress === "hub-link" ? linkPolicy() : undefined;
|
|
296
310
|
// The unauthenticated loopback listener (#1102) serves a fixed allowlist and nothing
|
|
297
311
|
// else. Rejecting here, before any handler runs, is what keeps the surface from growing
|
|
298
312
|
// silently when a route is added below.
|
|
@@ -303,6 +317,13 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
303
317
|
loopbackPolicy(),
|
|
304
318
|
);
|
|
305
319
|
}
|
|
320
|
+
if (ingress === "hub-link" && !linkRouteAllowed(requestUrl, req)) {
|
|
321
|
+
return withCors(
|
|
322
|
+
formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${new URL(req.url).pathname}`),
|
|
323
|
+
req,
|
|
324
|
+
linkPolicyView!,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
306
327
|
// Tailscale Serve terminates only on this separately bound loopback socket. Reject before
|
|
307
328
|
// dispatch so no data, readiness, health, WebSocket, or unknown-static handler can run.
|
|
308
329
|
if (ingress === "hub-management" && !managementIngressRouteAllowed(codexCompatibleUrl(req.url), req)) {
|
|
@@ -327,10 +348,20 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
327
348
|
// The Claude intercept listener is loopback by construction (the CONNECT proxy binds
|
|
328
349
|
// 127.0.0.1 and the request was rewritten onto a loopback origin), and its callers carry
|
|
329
350
|
// Anthropic credentials, not opencodex admission tokens — so it takes the loopback view too.
|
|
330
|
-
const policy: RequestPolicyView = ingress === "
|
|
331
|
-
?
|
|
332
|
-
:
|
|
333
|
-
|
|
351
|
+
const policy: RequestPolicyView = ingress === "hub-link"
|
|
352
|
+
? linkPolicyView!
|
|
353
|
+
: ingress === "unauthenticated-loopback" || ingress === "claude-intercept"
|
|
354
|
+
? loopbackPolicy()
|
|
355
|
+
: config;
|
|
356
|
+
if (ingress === "hub-link") {
|
|
357
|
+
const admission = resolveApiAuth(req, policy);
|
|
358
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
359
|
+
if (requestUrl.pathname === "/v1/catalog" && requestUrl.search === ""
|
|
360
|
+
&& (req.method === "GET" || req.method === "HEAD")
|
|
361
|
+
&& admission.kind === "configured") onAuthenticatedCatalog(admission.keyId);
|
|
362
|
+
}
|
|
363
|
+
const url = requestUrl;
|
|
364
|
+
const admissionOptions = { linkIngress: policy.linkIngress?.allowedKeyIds };
|
|
334
365
|
markActivity(`${req.method} ${url.pathname}`);
|
|
335
366
|
|
|
336
367
|
// Readiness is exact-GET on the literal /readyz path. Compare the DECODED
|
|
@@ -660,7 +691,10 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
660
691
|
}), req, config);
|
|
661
692
|
}
|
|
662
693
|
}
|
|
663
|
-
const mgmtResponse = await handleManagementAPI(req, url, config, requestManagementApiDeps, principal, managementSessionControl
|
|
694
|
+
const mgmtResponse = await handleManagementAPI(req, url, config, requestManagementApiDeps, principal, managementSessionControl, {
|
|
695
|
+
trustedLoopback: trustedLoopbackForIngress(ingress, config.hostname ?? "127.0.0.1"),
|
|
696
|
+
guiSessionIssuance: managementSessionIssuance(req, managementAuth),
|
|
697
|
+
});
|
|
664
698
|
if (mgmtResponse) return withManagementCors(mgmtResponse, req, config);
|
|
665
699
|
return withManagementCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
666
700
|
}
|
|
@@ -1545,7 +1579,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1545
1579
|
if (url.pathname === "/v1/audio/transcriptions" && req.method === "POST") {
|
|
1546
1580
|
disableResponsesRequestTimeout(req, requestServer);
|
|
1547
1581
|
if (isDraining()) return drainingResponse(req, policy);
|
|
1548
|
-
const admission = resolveAudioAdmission(req.headers, config);
|
|
1582
|
+
const admission = resolveAudioAdmission(req.headers, config, admissionOptions);
|
|
1549
1583
|
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1550
1584
|
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1551
1585
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin audio request blocked"), req, policy);
|
|
@@ -1576,7 +1610,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1576
1610
|
if (isDraining()) {
|
|
1577
1611
|
return drainingResponse(req, policy);
|
|
1578
1612
|
}
|
|
1579
|
-
const audioClient = resolveAudioClient(req, config);
|
|
1613
|
+
const audioClient = resolveAudioClient(req, config, false, admissionOptions);
|
|
1580
1614
|
if (audioClient instanceof Response) return withCors(audioClient, req, policy);
|
|
1581
1615
|
const admission = audioClient?.admission ?? resolveApiAuth(req, policy);
|
|
1582
1616
|
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
@@ -1620,7 +1654,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1620
1654
|
if (isDraining()) {
|
|
1621
1655
|
return drainingResponse(req, policy);
|
|
1622
1656
|
}
|
|
1623
|
-
const audioClient = resolveAudioClient(req, config, dictationSocket);
|
|
1657
|
+
const audioClient = resolveAudioClient(req, config, dictationSocket, admissionOptions);
|
|
1624
1658
|
if (audioClient instanceof Response) return withCors(audioClient, req, policy);
|
|
1625
1659
|
if (!audioClient && liveSidebandTarget && "callId" in liveSidebandTarget
|
|
1626
1660
|
&& liveSidebandTarget.callId.startsWith(EXTERNAL_CALL_PREFIX)) {
|
|
@@ -1868,7 +1902,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1868
1902
|
undefined,
|
|
1869
1903
|
guiSessionCandidate ?? undefined,
|
|
1870
1904
|
config.runtimeRole ?? "standalone",
|
|
1871
|
-
isApiAuthRequired(
|
|
1905
|
+
isApiAuthRequired(policy),
|
|
1872
1906
|
);
|
|
1873
1907
|
if (guiFile) return guiFile;
|
|
1874
1908
|
if (url.pathname === "/" && req.method === "GET") {
|
package/src/server/index.ts
CHANGED
|
@@ -196,7 +196,7 @@ import {
|
|
|
196
196
|
} from "../lib/local-management-attestation";
|
|
197
197
|
import { createReadinessGate, type ReadinessGate } from "./readiness";
|
|
198
198
|
import { createServeOptions, type ServerIngress } from "./index/serve-options";
|
|
199
|
-
import {
|
|
199
|
+
import { createOptionalListenerSet, LINK_INGRESS_HOSTNAME } from "./index/optional-listeners";
|
|
200
200
|
import { createPackageTreeIntegrityGuardForServer } from "./index/package-tree-guard";
|
|
201
201
|
import { inspectStartupOwnership, resolveInboundBodyLimitWithWarning, setStartupCacheInvalidationWrite, warnAgentTaskRecoveryStartup, warnPlaintextV2AgentMessagesStartup, type StartServerDeps } from "./index/startup-warnings";
|
|
202
202
|
import { acquireSpendLedgerServerLifecycle, recordFailedStartRollback, type SpendLedgerServerLifecycle } from "./index/spend-ledger-lifecycle";
|
|
@@ -623,15 +623,17 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
623
623
|
let server: Server<WsData>;
|
|
624
624
|
let loopbackServer: Server<WsData> | null = null;
|
|
625
625
|
let managementIngressServer: Server<WsData> | null = null;
|
|
626
|
-
const
|
|
626
|
+
const optionalListeners = createOptionalListenerSet<WsData>();
|
|
627
627
|
const inboundBodyLimitBytes = resolveInboundBodyLimitWithWarning(config);
|
|
628
628
|
|
|
629
629
|
function ingressForServer(requestServer: Server<WsData>): ServerIngress {
|
|
630
|
+
const optionalIngress = optionalListeners.ingressOf(requestServer);
|
|
631
|
+
if (optionalIngress !== undefined) return optionalIngress;
|
|
630
632
|
if (requestServer === loopbackServer) return "unauthenticated-loopback";
|
|
631
633
|
if (requestServer === managementIngressServer) return "hub-management";
|
|
632
|
-
if (claudeIntercept.ownsListener(requestServer)) return "claude-intercept";
|
|
633
634
|
return "public";
|
|
634
635
|
}
|
|
636
|
+
const linkPolicy = (): RequestPolicyView => requestPolicyView(config, LINK_INGRESS_HOSTNAME, { allowedKeyIds: optionalListeners.linkAdmissionKeyIds() });
|
|
635
637
|
let backgroundLifecycle: ReturnType<typeof acquireServerBackgroundLifecycle> | null = null;
|
|
636
638
|
let unregisterQuotaAutoRefresh: (() => void) | null = null;
|
|
637
639
|
let remoteWorkspaceStopping = false;
|
|
@@ -639,7 +641,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
639
641
|
const managementApiDeps: ManagementApiDeps = {
|
|
640
642
|
...deps.managementApi,
|
|
641
643
|
remoteWorkspaceStopping: () => remoteWorkspaceStopping,
|
|
642
|
-
onRemoteWorkspaceShutdown: shutdown => { remoteWorkspaceShutdown = shutdown; },
|
|
644
|
+
onRemoteWorkspaceShutdown: shutdown => { remoteWorkspaceShutdown = shutdown; }, linkSupervisor: () => optionalListeners.linkSupervisor(), linkListener: () => optionalListeners,
|
|
643
645
|
};
|
|
644
646
|
let workspaceRuntimeFlight: Promise<typeof import("../remote-control/workspace-runtime")> | undefined;
|
|
645
647
|
const loadRemoteWorkspaceRuntime = () => {
|
|
@@ -666,6 +668,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
666
668
|
ingressForServer,
|
|
667
669
|
loopbackRouteAllowed,
|
|
668
670
|
managementIngressRouteAllowed,
|
|
671
|
+
linkRouteAllowed: optionalListeners.linkRouteAllowed, linkPolicy, onAuthenticatedCatalog: optionalListeners.notifyAuthenticatedCatalog,
|
|
669
672
|
packageTreeChangedResponse,
|
|
670
673
|
serverBusyResponse,
|
|
671
674
|
runAdmittedHttpTurn,
|
|
@@ -731,10 +734,8 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
731
734
|
throw new AuxiliaryListenerBindError("hub.managementIngress", managementIngressPort, "127.0.0.1", error);
|
|
732
735
|
}
|
|
733
736
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
dispatch: (req, requestServer) => serveOptions.fetch(req, requestServer),
|
|
737
|
-
});
|
|
737
|
+
optionalListeners.start({ config, publicPort: server.port ?? listenPort, requestedPort: listenPort,
|
|
738
|
+
maxRequestBodySize: inboundBodyLimitBytes, dispatch: (req, requestServer) => serveOptions.fetch(req, requestServer) });
|
|
738
739
|
} catch (error) {
|
|
739
740
|
unregisterQuotaAutoRefresh?.();
|
|
740
741
|
userCostOverlayReconciler?.stop();
|
|
@@ -772,7 +773,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
772
773
|
...(managementIngressRef
|
|
773
774
|
? [() => managementIngressRef.stop(closeActiveConnections)]
|
|
774
775
|
: []),
|
|
775
|
-
() =>
|
|
776
|
+
() => optionalListeners.stop(),
|
|
776
777
|
async () => { await remoteWorkspaceShutdown?.(); },
|
|
777
778
|
async () => {
|
|
778
779
|
try {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { OcxUsage } from "../../types";
|
|
2
|
+
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
3
|
+
import {
|
|
4
|
+
beginRequestAttempt,
|
|
5
|
+
finishRequestAttempt,
|
|
6
|
+
sealRequestAttemptIdentity,
|
|
7
|
+
type RequestLogContext,
|
|
8
|
+
} from "../request-log";
|
|
9
|
+
|
|
10
|
+
export interface InferenceAttemptTarget {
|
|
11
|
+
provider: string;
|
|
12
|
+
model: string;
|
|
13
|
+
adapter: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface InferenceAttempt {
|
|
17
|
+
readonly attempt: PersistedUsageAttempt;
|
|
18
|
+
/** Epoch ms the attempt became active; the duration `finish` records is measured from it. */
|
|
19
|
+
readonly startedAt: number;
|
|
20
|
+
/** Stamp the attempt's provider/adapter identity and, before its first send, its account. */
|
|
21
|
+
seal(accountLabel?: string): void;
|
|
22
|
+
/** Close the attempt row with its status and duration; `usage` overrides the attempt's own. */
|
|
23
|
+
finish(status: number, usage?: OcxUsage): PersistedUsageAttempt;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Open one physical attempt on `logCtx`: the next ordinal, the active attempt and its start
|
|
28
|
+
* time, and the attempt appended to the request's list, in that order. The attempt is the row
|
|
29
|
+
* the final request log finishes; `finish` exists for owners that close it themselves.
|
|
30
|
+
*/
|
|
31
|
+
export function beginInferenceAttempt(
|
|
32
|
+
logCtx: RequestLogContext,
|
|
33
|
+
target: InferenceAttemptTarget,
|
|
34
|
+
): InferenceAttempt {
|
|
35
|
+
const attempt = beginRequestAttempt(
|
|
36
|
+
(logCtx.attempts?.length ?? 0) + 1,
|
|
37
|
+
target.provider,
|
|
38
|
+
target.model,
|
|
39
|
+
target.adapter,
|
|
40
|
+
);
|
|
41
|
+
logCtx.activeAttempt = attempt;
|
|
42
|
+
const startedAt = Date.now();
|
|
43
|
+
logCtx.activeAttemptStartedAt = startedAt;
|
|
44
|
+
(logCtx.attempts ??= []).push(attempt);
|
|
45
|
+
return {
|
|
46
|
+
attempt,
|
|
47
|
+
startedAt,
|
|
48
|
+
seal: accountLabel => sealRequestAttemptIdentity(attempt, target.provider, target.adapter, accountLabel),
|
|
49
|
+
finish: (status, usage) => finishRequestAttempt(attempt, status, Date.now() - startedAt, usage),
|
|
50
|
+
};
|
|
51
|
+
}
|