@lamplitisles/dsh-nanocodex 0.1.0-beta.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/LICENSE +65 -0
- package/NOTICE +2 -0
- package/README.md +182 -0
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/cordis.patch.yml +52 -0
- package/dist/index.d.ts +435 -0
- package/dist/index.js +3405 -0
- package/dist/standard-preset.d.ts +7 -0
- package/dist/standard-preset.js +25 -0
- package/engine-release.json +14 -0
- package/package.json +119 -0
- package/presets/standard/agent.cordis.yml +6 -0
- package/presets/standard/preset.yml +2 -0
- package/vendor/nanocodex/README.md +1344 -0
- package/vendor/nanocodex/actions/events.d.mts +10 -0
- package/vendor/nanocodex/actions/events.mjs +163 -0
- package/vendor/nanocodex/actions/index.d.mts +13 -0
- package/vendor/nanocodex/actions/index.mjs +35 -0
- package/vendor/nanocodex/actions/session.d.mts +66 -0
- package/vendor/nanocodex/actions/session.mjs +59 -0
- package/vendor/nanocodex/actions/turn.d.mts +59 -0
- package/vendor/nanocodex/actions/turn.mjs +37 -0
- package/vendor/nanocodex/actions/voice.d.mts +22 -0
- package/vendor/nanocodex/actions/voice.mjs +50 -0
- package/vendor/nanocodex/browser/Agent.d.mts +55 -0
- package/vendor/nanocodex/browser/Agent.mjs +11 -0
- package/vendor/nanocodex/browser/ChatGptSubscription.d.mts +7 -0
- package/vendor/nanocodex/browser/ChatGptSubscription.mjs +19 -0
- package/vendor/nanocodex/browser/InlineAgent.mjs +266 -0
- package/vendor/nanocodex/browser/Transport.d.mts +74 -0
- package/vendor/nanocodex/browser/Transport.mjs +61 -0
- package/vendor/nanocodex/browser/Voice.d.mts +71 -0
- package/vendor/nanocodex/browser/Voice.mjs +291 -0
- package/vendor/nanocodex/browser/VoiceSession.mjs +688 -0
- package/vendor/nanocodex/browser/WorkerAgent.d.mts +65 -0
- package/vendor/nanocodex/browser/WorkerAgent.mjs +1472 -0
- package/vendor/nanocodex/browser/agent.worker.mjs +3 -0
- package/vendor/nanocodex/browser/config.d.mts +36 -0
- package/vendor/nanocodex/browser/config.mjs +439 -0
- package/vendor/nanocodex/browser/engine.mjs +13 -0
- package/vendor/nanocodex/browser/harness.mjs +55 -0
- package/vendor/nanocodex/browser/host.d.mts +83 -0
- package/vendor/nanocodex/browser/host.mjs +555 -0
- package/vendor/nanocodex/browser/hostManagedWebSocket.d.mts +14 -0
- package/vendor/nanocodex/browser/hostManagedWebSocket.mjs +110 -0
- package/vendor/nanocodex/browser/index.d.mts +92 -0
- package/vendor/nanocodex/browser/index.mjs +23 -0
- package/vendor/nanocodex/browser/indexeddb-durability-store.mjs +197 -0
- package/vendor/nanocodex/browser/workspace.d.mts +16 -0
- package/vendor/nanocodex/browser/workspace.mjs +107 -0
- package/vendor/nanocodex/cloud/Client.d.mts +83 -0
- package/vendor/nanocodex/cloud/Client.mjs +231 -0
- package/vendor/nanocodex/cloud/Decorator.d.mts +31 -0
- package/vendor/nanocodex/cloud/Decorator.mjs +37 -0
- package/vendor/nanocodex/cloud/Dialog.d.mts +113 -0
- package/vendor/nanocodex/cloud/Dialog.mjs +366 -0
- package/vendor/nanocodex/cloud/Errors.d.mts +22 -0
- package/vendor/nanocodex/cloud/Errors.mjs +39 -0
- package/vendor/nanocodex/cloud/Principal.d.mts +26 -0
- package/vendor/nanocodex/cloud/Principal.mjs +88 -0
- package/vendor/nanocodex/cloud/RemoteProvider.mjs +88 -0
- package/vendor/nanocodex/cloud/Transport.d.mts +47 -0
- package/vendor/nanocodex/cloud/Transport.mjs +454 -0
- package/vendor/nanocodex/cloud/actions/account.d.mts +9 -0
- package/vendor/nanocodex/cloud/actions/account.mjs +19 -0
- package/vendor/nanocodex/cloud/actions/agent.d.mts +18 -0
- package/vendor/nanocodex/cloud/actions/agent.mjs +428 -0
- package/vendor/nanocodex/cloud/actions/connection.d.mts +133 -0
- package/vendor/nanocodex/cloud/actions/connection.mjs +580 -0
- package/vendor/nanocodex/cloud/actions/grant.d.mts +10 -0
- package/vendor/nanocodex/cloud/actions/grant.mjs +12 -0
- package/vendor/nanocodex/cloud/actions/index.d.mts +7 -0
- package/vendor/nanocodex/cloud/actions/index.mjs +7 -0
- package/vendor/nanocodex/cloud/actions/machineUsd.d.mts +23 -0
- package/vendor/nanocodex/cloud/actions/machineUsd.mjs +37 -0
- package/vendor/nanocodex/cloud/actions/model.d.mts +12 -0
- package/vendor/nanocodex/cloud/actions/model.mjs +50 -0
- package/vendor/nanocodex/cloud/actions/mpp.d.mts +28 -0
- package/vendor/nanocodex/cloud/actions/mpp.mjs +39 -0
- package/vendor/nanocodex/cloud/index.d.mts +30 -0
- package/vendor/nanocodex/cloud/index.mjs +9 -0
- package/vendor/nanocodex/cloud/internal.mjs +533 -0
- package/vendor/nanocodex/cloud/mercator.mjs +108 -0
- package/vendor/nanocodex/cloud/server/HostPrincipal.d.mts +34 -0
- package/vendor/nanocodex/cloud/server/HostPrincipal.mjs +202 -0
- package/vendor/nanocodex/cloud/server/index.d.mts +1 -0
- package/vendor/nanocodex/cloud/server/index.mjs +1 -0
- package/vendor/nanocodex/cloud/types.d.mts +211 -0
- package/vendor/nanocodex/cloudflare/Agent.d.mts +133 -0
- package/vendor/nanocodex/cloudflare/Agent.mjs +811 -0
- package/vendor/nanocodex/cloudflare/egress-subject.mjs +19 -0
- package/vendor/nanocodex/cloudflare/egress.d.mts +41 -0
- package/vendor/nanocodex/cloudflare/egress.mjs +119 -0
- package/vendor/nanocodex/cloudflare/event-socket.mjs +316 -0
- package/vendor/nanocodex/cloudflare/index.d.mts +7 -0
- package/vendor/nanocodex/cloudflare/index.mjs +6 -0
- package/vendor/nanocodex/host/Agent.d.mts +55 -0
- package/vendor/nanocodex/host/Agent.mjs +1 -0
- package/vendor/nanocodex/host/index.d.mts +82 -0
- package/vendor/nanocodex/host/index.mjs +10 -0
- package/vendor/nanocodex/index.d.mts +81 -0
- package/vendor/nanocodex/index.mjs +12 -0
- package/vendor/nanocodex/internal.mjs +1275 -0
- package/vendor/nanocodex/managed/Agent.d.mts +380 -0
- package/vendor/nanocodex/managed/Agent.mjs +1694 -0
- package/vendor/nanocodex/managed/ManagedError.d.mts +9 -0
- package/vendor/nanocodex/managed/ManagedError.mjs +8 -0
- package/vendor/nanocodex/managed/README.md +241 -0
- package/vendor/nanocodex/managed/Voice.mjs +202 -0
- package/vendor/nanocodex/managed/index.d.mts +20 -0
- package/vendor/nanocodex/managed/index.mjs +2 -0
- package/vendor/nanocodex/managed/internal.mjs +119 -0
- package/vendor/nanocodex/node/Agent.d.mts +50 -0
- package/vendor/nanocodex/node/Agent.mjs +203 -0
- package/vendor/nanocodex/node/ChatGptSubscription.d.mts +9 -0
- package/vendor/nanocodex/node/ChatGptSubscription.mjs +17 -0
- package/vendor/nanocodex/node/Transport.d.mts +44 -0
- package/vendor/nanocodex/node/Transport.mjs +46 -0
- package/vendor/nanocodex/node/host.mjs +731 -0
- package/vendor/nanocodex/node/index.d.mts +73 -0
- package/vendor/nanocodex/node/index.mjs +17 -0
- package/vendor/nanocodex/node/workspace.d.mts +10 -0
- package/vendor/nanocodex/node/workspace.mjs +198 -0
- package/vendor/nanocodex/package.json +200 -0
- package/vendor/nanocodex/pkg-node/nanocodex.d.ts +675 -0
- package/vendor/nanocodex/pkg-node/nanocodex.js +2628 -0
- package/vendor/nanocodex/pkg-node/nanocodex_bg.wasm.d.ts +105 -0
- package/vendor/nanocodex/pkg-node/package.json +1 -0
- package/vendor/nanocodex/pkg-web/.nanocodex-bindgen-stamp +6 -0
- package/vendor/nanocodex/pkg-web/nanocodex-build.json +1 -0
- package/vendor/nanocodex/pkg-web/nanocodex.d.ts +805 -0
- package/vendor/nanocodex/pkg-web/nanocodex.js +2714 -0
- package/vendor/nanocodex/pkg-web/nanocodex_bg.js +2615 -0
- package/vendor/nanocodex/pkg-web/nanocodex_bg.wasm +0 -0
- package/vendor/nanocodex/pkg-web/nanocodex_bg.wasm.d.ts +105 -0
- package/vendor/nanocodex/pkg-web/nanocodex_worker.js +9 -0
- package/vendor/nanocodex/pkg-web/package.json +1 -0
- package/vendor/nanocodex/runtime/chatgpt-subscription.mjs +212 -0
- package/vendor/nanocodex/runtime/cloudflare-durability-store.d.mts +26 -0
- package/vendor/nanocodex/runtime/cloudflare-durability-store.mjs +168 -0
- package/vendor/nanocodex/runtime/code-evaluator.worker.mjs +137 -0
- package/vendor/nanocodex/runtime/code-runtime.mjs +1 -0
- package/vendor/nanocodex/runtime/durability-store.d.mts +85 -0
- package/vendor/nanocodex/runtime/durability-store.mjs +631 -0
- package/vendor/nanocodex/runtime/durability.mjs +153 -0
- package/vendor/nanocodex/runtime/managed-transport.mjs +460 -0
- package/vendor/nanocodex/runtime/mcp-runtime.mjs +534 -0
- package/vendor/nanocodex/runtime/postgres-durability-store.d.mts +64 -0
- package/vendor/nanocodex/runtime/postgres-durability-store.mjs +532 -0
- package/vendor/nanocodex/runtime/quickjs-evaluator.d.mts +17 -0
- package/vendor/nanocodex/runtime/quickjs-evaluator.mjs +236 -0
- package/vendor/nanocodex/runtime/responses-transport.mjs +22 -0
- package/vendor/nanocodex/runtime/subagents.d.mts +107 -0
- package/vendor/nanocodex/runtime/subagents.mjs +54 -0
- package/vendor/nanocodex/runtime/subscription-store.d.mts +8 -0
- package/vendor/nanocodex/runtime/subscription-store.mjs +42 -0
- package/vendor/nanocodex/runtime/tempo-provider.d.mts +125 -0
- package/vendor/nanocodex/runtime/tempo-provider.mjs +264 -0
- package/vendor/nanocodex/runtime/tool-configuration.mjs +1 -0
- package/vendor/nanocodex/runtime/tool-router.mjs +1 -0
- package/vendor/nanocodex/runtime/utf8.mjs +1 -0
- package/vendor/nanocodex/runtime/worker-evaluator.mjs +162 -0
- package/vendor/nanocodex/runtime/workspace.d.mts +1 -0
- package/vendor/nanocodex/runtime/workspace.mjs +1 -0
- package/vendor/nanocodex/tools/Tools.d.mts +56 -0
- package/vendor/nanocodex/tools/Tools.mjs +136 -0
- package/vendor/nanocodex/tools/artifact.d.mts +1 -0
- package/vendor/nanocodex/tools/artifact.mjs +1 -0
- package/vendor/nanocodex/tools/attachment.mjs +1 -0
- package/vendor/nanocodex/tools/bash.d.mts +1 -0
- package/vendor/nanocodex/tools/bash.mjs +1 -0
- package/vendor/nanocodex/tools/browser/accountInfo.mjs +795 -0
- package/vendor/nanocodex/tools/browser/browserBuffer.mjs +18 -0
- package/vendor/nanocodex/tools/browser/browserCompiler.mjs +155 -0
- package/vendor/nanocodex/tools/browser/browserEgress.mjs +161 -0
- package/vendor/nanocodex/tools/browser/browserPython.mjs +133 -0
- package/vendor/nanocodex/tools/browser/browserShell.mjs +1075 -0
- package/vendor/nanocodex/tools/browser/browserSprintf.mjs +4 -0
- package/vendor/nanocodex/tools/browser/browserSsh.mjs +201 -0
- package/vendor/nanocodex/tools/browser/browserZlib.mjs +136 -0
- package/vendor/nanocodex/tools/browser/compiler.worker.mjs +160 -0
- package/vendor/nanocodex/tools/browser/devTunnelsSshBrowser.mjs +13 -0
- package/vendor/nanocodex/tools/browser/index.d.mts +209 -0
- package/vendor/nanocodex/tools/browser/index.mjs +174 -0
- package/vendor/nanocodex/tools/browser/opfsGit.mjs +248 -0
- package/vendor/nanocodex/tools/browser/python.worker.mjs +114 -0
- package/vendor/nanocodex/tools/browser/threadGit.mjs +197 -0
- package/vendor/nanocodex/tools/browser/unsupportedNodeRsa.mjs +5 -0
- package/vendor/nanocodex/tools/browser/workspace.mjs +83 -0
- package/vendor/nanocodex/tools/dataset.d.mts +1 -0
- package/vendor/nanocodex/tools/dataset.mjs +1 -0
- package/vendor/nanocodex/tools/datasetContract.mjs +1 -0
- package/vendor/nanocodex/tools/datasetEngine.mjs +1 -0
- package/vendor/nanocodex/tools/hostedCatalog.d.mts +1 -0
- package/vendor/nanocodex/tools/hostedCatalog.mjs +1 -0
- package/vendor/nanocodex/tools/index.d.mts +49 -0
- package/vendor/nanocodex/tools/index.mjs +19 -0
- package/vendor/nanocodex/tools/namedTool.mjs +1 -0
- package/vendor/nanocodex/tools/repository-workspace.d.mts +1 -0
- package/vendor/nanocodex/tools/repository-workspace.mjs +1 -0
- package/vendor/nanocodex/tools/ssh.d.mts +1 -0
- package/vendor/nanocodex/tools/ssh.mjs +1 -0
- package/vendor/nanocodex/tools/standard.mjs +1 -0
- package/vendor/nanocodex/tools/standardDescriptions.mjs +1 -0
- package/vendor/nanocodex/types.d.mts +663 -0
- package/vendor/nanocodex/wasm.d.mts +4 -0
- package/vendor/nanocodex/worker/ChatGptSubscription.d.mts +9 -0
- package/vendor/nanocodex/worker/ChatGptSubscription.mjs +29 -0
- package/vendor/nanocodex/worker/index.d.mts +18 -0
- package/vendor/nanocodex/worker/index.mjs +5 -0
- package/vendor/nanocodex-tools/README.md +65 -0
- package/vendor/nanocodex-tools/dist/hosted/app-tool-catalog.d.ts +7 -0
- package/vendor/nanocodex-tools/dist/hosted/app-tool-catalog.js +14 -0
- package/vendor/nanocodex-tools/dist/hosted/broker-core.d.ts +172 -0
- package/vendor/nanocodex-tools/dist/hosted/broker-core.js +1278 -0
- package/vendor/nanocodex-tools/dist/hosted/index.d.ts +3 -0
- package/vendor/nanocodex-tools/dist/hosted/index.js +3 -0
- package/vendor/nanocodex-tools/dist/hosted/protocol.d.ts +120 -0
- package/vendor/nanocodex-tools/dist/hosted/protocol.js +415 -0
- package/vendor/nanocodex-tools/dist/index.d.ts +19 -0
- package/vendor/nanocodex-tools/dist/index.js +14 -0
- package/vendor/nanocodex-tools/dist/memory.d.ts +204 -0
- package/vendor/nanocodex-tools/dist/memory.js +411 -0
- package/vendor/nanocodex-tools/dist/namespace.d.ts +87 -0
- package/vendor/nanocodex-tools/dist/namespace.js +338 -0
- package/vendor/nanocodex-tools/dist/repository-archive.d.ts +4 -0
- package/vendor/nanocodex-tools/dist/repository-archive.js +104 -0
- package/vendor/nanocodex-tools/dist/runtime.d.ts +30 -0
- package/vendor/nanocodex-tools/dist/runtime.js +37 -0
- package/vendor/nanocodex-tools/dist/session.d.ts +167 -0
- package/vendor/nanocodex-tools/dist/session.js +286 -0
- package/vendor/nanocodex-tools/dist/shell.d.ts +51 -0
- package/vendor/nanocodex-tools/dist/shell.js +553 -0
- package/vendor/nanocodex-tools/dist/ssh.d.ts +29 -0
- package/vendor/nanocodex-tools/dist/ssh.js +66 -0
- package/vendor/nanocodex-tools/dist/workspace.d.ts +45 -0
- package/vendor/nanocodex-tools/dist/workspace.js +237 -0
- package/vendor/nanocodex-tools/package.json +122 -0
- package/vendor/nanocodex-tools/runtime/code-runtime.mjs +817 -0
- package/vendor/nanocodex-tools/runtime/tool-configuration.mjs +50 -0
- package/vendor/nanocodex-tools/runtime/tool-router.mjs +730 -0
- package/vendor/nanocodex-tools/runtime/utf8.d.mts +2 -0
- package/vendor/nanocodex-tools/runtime/utf8.mjs +25 -0
- package/vendor/nanocodex-tools/tools/artifact.d.mts +69 -0
- package/vendor/nanocodex-tools/tools/artifact.mjs +270 -0
- package/vendor/nanocodex-tools/tools/attachment.mjs +603 -0
- package/vendor/nanocodex-tools/tools/bash.d.mts +90 -0
- package/vendor/nanocodex-tools/tools/bash.mjs +742 -0
- package/vendor/nanocodex-tools/tools/dataset.d.mts +9 -0
- package/vendor/nanocodex-tools/tools/dataset.mjs +47 -0
- package/vendor/nanocodex-tools/tools/datasetContract.mjs +54 -0
- package/vendor/nanocodex-tools/tools/datasetEngine.mjs +1167 -0
- package/vendor/nanocodex-tools/tools/execution-contract.d.mts +5 -0
- package/vendor/nanocodex-tools/tools/execution-contract.mjs +63 -0
- package/vendor/nanocodex-tools/tools/hostedCatalog.d.mts +20 -0
- package/vendor/nanocodex-tools/tools/hostedCatalog.mjs +67 -0
- package/vendor/nanocodex-tools/tools/hostedMachine.d.mts +10 -0
- package/vendor/nanocodex-tools/tools/hostedMachine.mjs +56 -0
- package/vendor/nanocodex-tools/tools/just-bash-browser.d.mts +1 -0
- package/vendor/nanocodex-tools/tools/just-bash-browser.mjs +1 -0
- package/vendor/nanocodex-tools/tools/namedTool.d.mts +3 -0
- package/vendor/nanocodex-tools/tools/namedTool.mjs +3 -0
- package/vendor/nanocodex-tools/tools/nodeProcess.d.mts +10 -0
- package/vendor/nanocodex-tools/tools/nodeProcess.mjs +314 -0
- package/vendor/nanocodex-tools/tools/processOutput.mjs +56 -0
- package/vendor/nanocodex-tools/tools/repository-workspace.d.mts +55 -0
- package/vendor/nanocodex-tools/tools/repository-workspace.mjs +397 -0
- package/vendor/nanocodex-tools/tools/ssh.d.mts +56 -0
- package/vendor/nanocodex-tools/tools/ssh.mjs +503 -0
- package/vendor/nanocodex-tools/tools/standard.mjs +458 -0
- package/vendor/nanocodex-tools/tools/standardDescriptions.mjs +3 -0
- package/vendor/nanocodex-tools/tools/types.d.mts +83 -0
- package/vendor/nanocodex-tools/tools/workspace.d.mts +25 -0
- package/vendor/nanocodex-tools/tools/workspace.mjs +282 -0
- package/vendor/nanocodex-tools/tools/x.d.mts +30 -0
- package/vendor/nanocodex-tools/tools/x.mjs +91 -0
|
@@ -0,0 +1,1278 @@
|
|
|
1
|
+
import { HOSTED_TOOL_CALL_TIMEOUT_MS, HOSTED_TOOLS_LEASE_MS, MAX_HOSTED_TOOL_OUTPUT_BYTES, HostedToolsProtocolError, parseHostedToolsHostFrame, parseHostedToolsManagedFrame, } from "./protocol.js";
|
|
2
|
+
import { hostedToolCatalogDigest } from "../../tools/hostedCatalog.mjs";
|
|
3
|
+
import { EXEC_COMMAND_PARAMETERS, EXECUTION_OUTPUT_SCHEMA, MACHINE_PREVIEW_PARAMETERS, PREVIEW_OUTPUT_SCHEMA, WRITE_STDIN_PARAMETERS, } from "../../tools/execution-contract.mjs";
|
|
4
|
+
const SOCKET_TAG = "hosted-tools";
|
|
5
|
+
const INVALID_CONNECT_GRANT_ID = "invalid-connect-grant";
|
|
6
|
+
const DEFAULT_MAX_IN_FLIGHT = 64;
|
|
7
|
+
const OPEN = 1;
|
|
8
|
+
const MAX_RETAINED_RECEIPTS = 512;
|
|
9
|
+
const MAX_CALLS_PER_GENERATION = 512;
|
|
10
|
+
const REVOKED_ROUTE_LEASE_EXPIRES_AT = -1;
|
|
11
|
+
const TOOL_RESULT = Symbol.for("nanocodex.toolResult");
|
|
12
|
+
const LEGACY_ROUTE_ID = "$legacy";
|
|
13
|
+
export const HOSTED_MACHINE_TOOL_NAMES = Object.freeze([
|
|
14
|
+
"exec_command",
|
|
15
|
+
"write_stdin",
|
|
16
|
+
"preview",
|
|
17
|
+
]);
|
|
18
|
+
const MACHINE_TOOL_NAMES = new Set(HOSTED_MACHINE_TOOL_NAMES);
|
|
19
|
+
const encoder = new TextEncoder();
|
|
20
|
+
/**
|
|
21
|
+
* Marks the one case where an attached source is known to be absent before a
|
|
22
|
+
* durable admission. The unified ToolRouter may then select the exact
|
|
23
|
+
* same-name cloud contract. It must never infer this from an outcome message:
|
|
24
|
+
* every other unavailable, cancellation, timeout, and ambiguous outcome is
|
|
25
|
+
* pinned to the attached source and is final.
|
|
26
|
+
*/
|
|
27
|
+
export const HOSTED_TOOLS_PRE_ADMISSION_UNAVAILABLE = Symbol.for("nanocodex.tool.preDispatchUnavailable");
|
|
28
|
+
/** Owns one reverse-tool attachment over an injected socket and durable ledger. */
|
|
29
|
+
export class HostedToolsBrokerCore {
|
|
30
|
+
context;
|
|
31
|
+
#provider;
|
|
32
|
+
#pending = new Map();
|
|
33
|
+
#now;
|
|
34
|
+
#randomUUID;
|
|
35
|
+
#maxInFlight;
|
|
36
|
+
#maxCallsPerGeneration;
|
|
37
|
+
#persistence;
|
|
38
|
+
#onCatalogChanged;
|
|
39
|
+
#entryAllowed;
|
|
40
|
+
#renewLeasedAttachment;
|
|
41
|
+
#catalogValidator;
|
|
42
|
+
#nextCandidateGeneration;
|
|
43
|
+
constructor(context, options) {
|
|
44
|
+
this.context = context;
|
|
45
|
+
this.#now = options.now ?? Date.now;
|
|
46
|
+
this.#randomUUID = options.randomUUID ?? (() => crypto.randomUUID());
|
|
47
|
+
this.#maxInFlight = options.maxInFlight ?? DEFAULT_MAX_IN_FLIGHT;
|
|
48
|
+
if (!Number.isSafeInteger(this.#maxInFlight) || this.#maxInFlight < 1
|
|
49
|
+
|| this.#maxInFlight > DEFAULT_MAX_IN_FLIGHT) {
|
|
50
|
+
throw new TypeError(`maxInFlight must be an integer from 1 through ${DEFAULT_MAX_IN_FLIGHT}`);
|
|
51
|
+
}
|
|
52
|
+
this.#maxCallsPerGeneration = options.maxCallsPerGeneration ?? MAX_CALLS_PER_GENERATION;
|
|
53
|
+
if (!Number.isSafeInteger(this.#maxCallsPerGeneration) || this.#maxCallsPerGeneration < 1
|
|
54
|
+
|| this.#maxCallsPerGeneration > MAX_CALLS_PER_GENERATION) {
|
|
55
|
+
throw new TypeError(`maxCallsPerGeneration must be an integer from 1 through ${MAX_CALLS_PER_GENERATION}`);
|
|
56
|
+
}
|
|
57
|
+
this.#persistence = options.persistence;
|
|
58
|
+
this.#onCatalogChanged = options.onCatalogChanged;
|
|
59
|
+
this.#entryAllowed = options.entryAllowed ?? (() => true);
|
|
60
|
+
this.#renewLeasedAttachment = options.renewLeasedAttachment;
|
|
61
|
+
const now = this.#now();
|
|
62
|
+
const sockets = this.context.sockets();
|
|
63
|
+
const retainHosts = options.resumeRetainedSockets === true && sockets.length > 0;
|
|
64
|
+
const retained = this.#persistence.initialize(now);
|
|
65
|
+
this.#nextCandidateGeneration = Math.max(0, ...this.#persistence.states().map((state) => state.generation));
|
|
66
|
+
for (const socket of sockets) {
|
|
67
|
+
const generation = this.#attachment(socket)?.generation;
|
|
68
|
+
if (generation !== undefined)
|
|
69
|
+
this.#nextCandidateGeneration = Math.max(this.#nextCandidateGeneration, generation);
|
|
70
|
+
}
|
|
71
|
+
for (const state of retained) {
|
|
72
|
+
if (!state.lease_id)
|
|
73
|
+
continue;
|
|
74
|
+
const resumed = retainHosts
|
|
75
|
+
&& state.lease_expires_at > now
|
|
76
|
+
&& this.#routingSocketForState(state) !== undefined;
|
|
77
|
+
if (resumed)
|
|
78
|
+
continue;
|
|
79
|
+
this.#persistence.clearHost(state.lease_id, state.generation);
|
|
80
|
+
for (const socket of sockets) {
|
|
81
|
+
const attachment = this.#attachment(socket);
|
|
82
|
+
if (attachment?.routeId !== state.route_id
|
|
83
|
+
|| attachment.leaseId !== state.lease_id
|
|
84
|
+
|| attachment.generation !== state.generation)
|
|
85
|
+
continue;
|
|
86
|
+
closeSocket(socket, 1012, "Hosted Tools owner restarted");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
this.#provider = Object.freeze({
|
|
90
|
+
// ToolRouter owns the one aggregate tool_search. This provider exposes
|
|
91
|
+
// only the current attached definitions, which stay deferred and can be
|
|
92
|
+
// overlaid onto exact cloud contracts by that router.
|
|
93
|
+
definitions: () => {
|
|
94
|
+
return this.#publicCatalogBindings()
|
|
95
|
+
.filter((binding) => this.#entryAllowed(binding.entry, binding.connectGrantId, binding.appToolCatalogDigest))
|
|
96
|
+
.map((binding) => Object.freeze({
|
|
97
|
+
...binding.entry.definition,
|
|
98
|
+
defer_loading: true,
|
|
99
|
+
}));
|
|
100
|
+
},
|
|
101
|
+
resolve: (name) => {
|
|
102
|
+
const prepared = this.#resolve(name);
|
|
103
|
+
if (!prepared || !this.#entryAllowed(prepared.entry, prepared.connectGrantId, prepared.appToolCatalogDigest))
|
|
104
|
+
return undefined;
|
|
105
|
+
return this.#codeTool(name, prepared);
|
|
106
|
+
},
|
|
107
|
+
setCatalogValidator: (validator) => {
|
|
108
|
+
this.#catalogValidator = validator;
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
owns(socket) { return this.handles(socket); }
|
|
113
|
+
async message(socket, message) {
|
|
114
|
+
await this.webSocketMessage(socket, message);
|
|
115
|
+
}
|
|
116
|
+
close(socket, reason) {
|
|
117
|
+
if (this.handles(socket))
|
|
118
|
+
this.#retire(socket, reason);
|
|
119
|
+
}
|
|
120
|
+
shutdown(reason) {
|
|
121
|
+
const sockets = this.context.sockets();
|
|
122
|
+
for (const socket of sockets)
|
|
123
|
+
this.#fence(socket, reason);
|
|
124
|
+
for (const state of this.#persistence.states())
|
|
125
|
+
this.#retireState(state, reason);
|
|
126
|
+
}
|
|
127
|
+
/** Fences one attachment only when its exact durable route is still current. */
|
|
128
|
+
revokeRoute(routeId, reason) {
|
|
129
|
+
const state = this.#persistence.state(routeId) ?? emptyState(routeId);
|
|
130
|
+
const active = state.lease_id !== null;
|
|
131
|
+
if (active) {
|
|
132
|
+
const socket = this.#socketForState(state);
|
|
133
|
+
if (socket)
|
|
134
|
+
this.#fence(socket, reason);
|
|
135
|
+
else
|
|
136
|
+
this.#retireState(state, reason);
|
|
137
|
+
}
|
|
138
|
+
// Revocation can race ahead of catalog publication in another Durable
|
|
139
|
+
// Object. Retain an exact route tombstone so a previously validated socket
|
|
140
|
+
// cannot publish after its control-plane fence has completed.
|
|
141
|
+
const retired = this.#persistence.state(routeId) ?? state;
|
|
142
|
+
this.#persistence.replaceHost({
|
|
143
|
+
...retired,
|
|
144
|
+
host_id: null,
|
|
145
|
+
lease_id: null,
|
|
146
|
+
lease_expires_at: REVOKED_ROUTE_LEASE_EXPIRES_AT,
|
|
147
|
+
catalog_json: null,
|
|
148
|
+
});
|
|
149
|
+
return active;
|
|
150
|
+
}
|
|
151
|
+
isReady() { return this.#definitions().length > 0; }
|
|
152
|
+
hasPendingCalls() { return this.#pending.size > 0; }
|
|
153
|
+
provider() { return this.#provider; }
|
|
154
|
+
/** Resolves one canonical machine primitive against its exact admitted attachment generation. */
|
|
155
|
+
machineTool(machineId, name, context) {
|
|
156
|
+
if (!MACHINE_TOOL_NAMES.has(name))
|
|
157
|
+
return undefined;
|
|
158
|
+
const binding = this.#catalogBindings().find((candidate) => (candidate.machine?.id === machineId && candidate.wireName === name));
|
|
159
|
+
if (!binding)
|
|
160
|
+
return undefined;
|
|
161
|
+
const prepared = this.#preparedTool(binding);
|
|
162
|
+
if (!this.#entryAllowed(prepared.entry, prepared.connectGrantId, prepared.appToolCatalogDigest, context))
|
|
163
|
+
return undefined;
|
|
164
|
+
return this.#codeTool(name, prepared);
|
|
165
|
+
}
|
|
166
|
+
/** Resolves one canonical machine primitive only on the named durable route. */
|
|
167
|
+
machineToolOnRoute(routeId, machineId, name, context) {
|
|
168
|
+
if (!MACHINE_TOOL_NAMES.has(name))
|
|
169
|
+
return undefined;
|
|
170
|
+
const binding = this.#catalogBindings(undefined, true).find((candidate) => (candidate.routeId === routeId
|
|
171
|
+
&& candidate.machine?.id === machineId
|
|
172
|
+
&& candidate.wireName === name));
|
|
173
|
+
if (!binding)
|
|
174
|
+
return undefined;
|
|
175
|
+
const prepared = this.#preparedTool(binding);
|
|
176
|
+
if (!this.#entryAllowed(prepared.entry, prepared.connectGrantId, prepared.appToolCatalogDigest, context))
|
|
177
|
+
return undefined;
|
|
178
|
+
return this.#codeTool(name, prepared);
|
|
179
|
+
}
|
|
180
|
+
/** Returns one live machine only when its exact durable route still owns it. */
|
|
181
|
+
machineOnRoute(routeId, machineId) {
|
|
182
|
+
const state = this.#persistence.state(routeId);
|
|
183
|
+
if (state === undefined)
|
|
184
|
+
return undefined;
|
|
185
|
+
const socket = this.#liveRoutingSocketForState(state);
|
|
186
|
+
if (socket === undefined)
|
|
187
|
+
return undefined;
|
|
188
|
+
const attachment = this.#attachment(socket);
|
|
189
|
+
if (attachment?.connectGrantId !== undefined)
|
|
190
|
+
return undefined;
|
|
191
|
+
return attachment?.machines?.find(({ id }) => id === machineId);
|
|
192
|
+
}
|
|
193
|
+
/** Returns the live, non-secret user-machine snapshot for the account-owned host. */
|
|
194
|
+
machines() {
|
|
195
|
+
const machines = [];
|
|
196
|
+
const ids = new Set();
|
|
197
|
+
for (const state of this.#sortedStates()) {
|
|
198
|
+
const socket = this.#liveRoutingSocketForState(state);
|
|
199
|
+
if (socket === undefined)
|
|
200
|
+
continue;
|
|
201
|
+
const attachment = this.#attachment(socket);
|
|
202
|
+
if (attachment?.connectGrantId !== undefined)
|
|
203
|
+
continue;
|
|
204
|
+
for (const machine of attachment?.machines ?? []) {
|
|
205
|
+
if (ids.has(machine.id))
|
|
206
|
+
return [];
|
|
207
|
+
ids.add(machine.id);
|
|
208
|
+
machines.push({ routeId: state.route_id, machine });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return machines
|
|
212
|
+
.sort((left, right) => left.machine.id.localeCompare(right.machine.id)
|
|
213
|
+
|| left.routeId.localeCompare(right.routeId))
|
|
214
|
+
.map(({ machine }) => machine);
|
|
215
|
+
}
|
|
216
|
+
accept(socket, sessionId, allowedMcpIds, appToolCatalogDigest, connectGrantId, leasedAttachment) {
|
|
217
|
+
if (allowedMcpIds !== undefined && !isConnectGrantId(connectGrantId)) {
|
|
218
|
+
throw new TypeError("Connect Hosted Tools requires an exact grant ID");
|
|
219
|
+
}
|
|
220
|
+
if (leasedAttachment !== undefined && (connectGrantId !== undefined
|
|
221
|
+
|| leasedAttachment.expectedAttachmentId.length === 0
|
|
222
|
+
|| leasedAttachment.fixedRouteId.length === 0
|
|
223
|
+
|| leasedAttachment.renewalToken.length === 0
|
|
224
|
+
|| leasedAttachment.renewalToken.length > 2_048
|
|
225
|
+
|| !Number.isSafeInteger(leasedAttachment.maximumLeaseExpiresAt))) {
|
|
226
|
+
throw new TypeError("leased Hosted Tools requires one complete ordinary-route policy");
|
|
227
|
+
}
|
|
228
|
+
this.context.writeAttachment(socket, {
|
|
229
|
+
kind: SOCKET_TAG,
|
|
230
|
+
sessionId,
|
|
231
|
+
...(allowedMcpIds === undefined ? {} : { allowedMcpIds: [...allowedMcpIds] }),
|
|
232
|
+
...(appToolCatalogDigest === undefined ? {} : { appToolCatalogDigest }),
|
|
233
|
+
...(connectGrantId === undefined ? {} : { connectGrantId }),
|
|
234
|
+
...(leasedAttachment === undefined ? {} : {
|
|
235
|
+
expectedAttachmentId: leasedAttachment.expectedAttachmentId,
|
|
236
|
+
maximumLeaseExpiresAt: leasedAttachment.maximumLeaseExpiresAt,
|
|
237
|
+
fixedRouteId: leasedAttachment.fixedRouteId,
|
|
238
|
+
renewalToken: leasedAttachment.renewalToken,
|
|
239
|
+
}),
|
|
240
|
+
});
|
|
241
|
+
this.context.accept(socket);
|
|
242
|
+
}
|
|
243
|
+
handles(socket) {
|
|
244
|
+
return this.#attachment(socket)?.kind === SOCKET_TAG;
|
|
245
|
+
}
|
|
246
|
+
async webSocketMessage(socket, message) {
|
|
247
|
+
if (!this.handles(socket))
|
|
248
|
+
return;
|
|
249
|
+
if (typeof message !== "string") {
|
|
250
|
+
this.#fence(socket, "Hosted Tools requires bounded text frames", 1003);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
let frame;
|
|
254
|
+
try {
|
|
255
|
+
frame = parseHostedToolsHostFrame(message);
|
|
256
|
+
await this.#dispatchHostFrame(socket, frame);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
const protocol = error instanceof HostedToolsProtocolError
|
|
260
|
+
? error
|
|
261
|
+
: new HostedToolsProtocolError("broker_failure", errorMessage(error));
|
|
262
|
+
this.#fence(socket, `${protocol.code}: ${protocol.message}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
webSocketClose(socket, code, reason) {
|
|
266
|
+
if (!this.handles(socket))
|
|
267
|
+
return;
|
|
268
|
+
this.#retire(socket, reason || `peer closed with code ${code}`);
|
|
269
|
+
closeSocket(socket, code, reason || "Hosted Tools peer closed");
|
|
270
|
+
}
|
|
271
|
+
webSocketError(socket) {
|
|
272
|
+
if (!this.handles(socket))
|
|
273
|
+
return;
|
|
274
|
+
this.#retire(socket, "WebSocket failed");
|
|
275
|
+
closeSocket(socket, 1011, "Hosted Tools WebSocket failed");
|
|
276
|
+
}
|
|
277
|
+
/** May be called by an owning alarm; normal reads and call timers also expire leases lazily. */
|
|
278
|
+
expire() {
|
|
279
|
+
for (const state of this.#persistence.states()) {
|
|
280
|
+
if (!state.lease_id || state.lease_expires_at > this.#now())
|
|
281
|
+
continue;
|
|
282
|
+
const socket = this.#socketForState(state);
|
|
283
|
+
if (socket)
|
|
284
|
+
this.#fence(socket, "Hosted Tools lease expired");
|
|
285
|
+
else
|
|
286
|
+
this.#retireState(state, "Hosted Tools lease expired");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
cancel(callId) {
|
|
290
|
+
const pending = this.#pending.get(callId);
|
|
291
|
+
if (!pending)
|
|
292
|
+
return false;
|
|
293
|
+
const row = this.#persistence.call(callId);
|
|
294
|
+
if (!row || row.state !== "dispatched")
|
|
295
|
+
return false;
|
|
296
|
+
const state = this.#stateForLease(row.lease_id, row.generation);
|
|
297
|
+
const socket = this.#socketForState(state);
|
|
298
|
+
if (!socket
|
|
299
|
+
|| !state
|
|
300
|
+
|| state.lease_expires_at <= this.#now()) {
|
|
301
|
+
this.#finishAmbiguous(row, "Hosted Tools cancellation lost its pinned attachment");
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
const cancelRequested = this.#persistence.markCancelRequested(callId, this.#now());
|
|
305
|
+
if (!cancelRequested || cancelRequested.state !== "dispatched"
|
|
306
|
+
|| cancelRequested.cancel_requested !== 1)
|
|
307
|
+
return false;
|
|
308
|
+
try {
|
|
309
|
+
this.#send(socket, {
|
|
310
|
+
type: "cancel",
|
|
311
|
+
call_id: row.call_id,
|
|
312
|
+
});
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
this.#retire(socket, "cancellation delivery failed");
|
|
317
|
+
closeSocket(socket, 1011, "Hosted Tools cancellation delivery failed");
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async #dispatchHostFrame(socket, frame) {
|
|
322
|
+
if (frame.type === "catalog")
|
|
323
|
+
await this.#publishCatalog(socket, frame);
|
|
324
|
+
else if (frame.type === "ping")
|
|
325
|
+
await this.#heartbeat(socket, frame);
|
|
326
|
+
else if (frame.type === "drain")
|
|
327
|
+
this.#drain(socket);
|
|
328
|
+
else
|
|
329
|
+
this.#completeResult(socket, frame);
|
|
330
|
+
}
|
|
331
|
+
#activeAttachment(socket) {
|
|
332
|
+
const attachment = this.#attachment(socket);
|
|
333
|
+
const state = attachment?.routeId === undefined
|
|
334
|
+
? undefined
|
|
335
|
+
: this.#persistence.state(attachment.routeId);
|
|
336
|
+
if (!attachment?.active || !attachment.leaseId || attachment.generation === undefined || !state
|
|
337
|
+
|| state.lease_id !== attachment.leaseId
|
|
338
|
+
|| state.generation !== attachment.generation
|
|
339
|
+
|| state.lease_expires_at <= this.#now()) {
|
|
340
|
+
throw new HostedToolsProtocolError("stale_socket", "socket no longer owns the tool attachment");
|
|
341
|
+
}
|
|
342
|
+
return attachment;
|
|
343
|
+
}
|
|
344
|
+
async #heartbeat(socket, frame) {
|
|
345
|
+
let attachment = this.#activeAttachment(socket);
|
|
346
|
+
if (attachment.renewalToken !== undefined) {
|
|
347
|
+
const renewal = {
|
|
348
|
+
expectedAttachmentId: attachment.expectedAttachmentId,
|
|
349
|
+
fixedRouteId: attachment.fixedRouteId,
|
|
350
|
+
renewalToken: attachment.renewalToken,
|
|
351
|
+
};
|
|
352
|
+
let maximumLeaseExpiresAt;
|
|
353
|
+
try {
|
|
354
|
+
maximumLeaseExpiresAt = await this.#renewLeasedAttachment?.(renewal);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
this.#retire(socket, "leased Hosted Tools validation unavailable");
|
|
358
|
+
closeSocket(socket, 1011, "leased Hosted Tools validation unavailable");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (!Number.isSafeInteger(maximumLeaseExpiresAt)
|
|
362
|
+
|| Number(maximumLeaseExpiresAt) <= this.#now()) {
|
|
363
|
+
this.revokeRoute(renewal.fixedRouteId, "leased Hosted Tools validation failed");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const renewedExpiry = Number(maximumLeaseExpiresAt);
|
|
367
|
+
const renewed = this.#activeAttachment(socket);
|
|
368
|
+
if (renewed.routeId !== attachment.routeId
|
|
369
|
+
|| renewed.leaseId !== attachment.leaseId
|
|
370
|
+
|| renewed.generation !== attachment.generation
|
|
371
|
+
|| renewed.fixedRouteId !== renewal.fixedRouteId
|
|
372
|
+
|| renewed.expectedAttachmentId !== renewal.expectedAttachmentId
|
|
373
|
+
|| renewed.renewalToken !== renewal.renewalToken) {
|
|
374
|
+
throw new HostedToolsProtocolError("stale_socket", "socket changed while its leased attachment was being validated");
|
|
375
|
+
}
|
|
376
|
+
attachment = { ...renewed, maximumLeaseExpiresAt: renewedExpiry };
|
|
377
|
+
this.context.writeAttachment(socket, attachment);
|
|
378
|
+
}
|
|
379
|
+
const expiresAt = Math.min(this.#now() + HOSTED_TOOLS_LEASE_MS, attachment.maximumLeaseExpiresAt ?? Number.MAX_SAFE_INTEGER);
|
|
380
|
+
const state = this.#persistence.state(attachment.routeId);
|
|
381
|
+
this.#persistence.replaceHost({ ...state, lease_expires_at: expiresAt });
|
|
382
|
+
this.#send(socket, {
|
|
383
|
+
type: "pong",
|
|
384
|
+
nonce: frame.nonce,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
async #publishCatalog(socket, frame) {
|
|
388
|
+
const initial = this.#attachment(socket);
|
|
389
|
+
if (!initial || initial.leaseId || initial.generation !== undefined || initial.active) {
|
|
390
|
+
throw new HostedToolsProtocolError("catalog_immutable", "one immutable catalog is allowed per socket");
|
|
391
|
+
}
|
|
392
|
+
if (this.#nextCandidateGeneration >= Number.MAX_SAFE_INTEGER) {
|
|
393
|
+
throw new HostedToolsProtocolError("generation_exhausted", "Hosted Tools generation is exhausted");
|
|
394
|
+
}
|
|
395
|
+
const routeId = initial.fixedRouteId ?? scopedRouteId(initial.connectGrantId, frame.attachment_id);
|
|
396
|
+
let maximumLeaseExpiresAt = initial.maximumLeaseExpiresAt;
|
|
397
|
+
if (initial.renewalToken !== undefined) {
|
|
398
|
+
const renewal = {
|
|
399
|
+
expectedAttachmentId: initial.expectedAttachmentId,
|
|
400
|
+
fixedRouteId: routeId,
|
|
401
|
+
renewalToken: initial.renewalToken,
|
|
402
|
+
};
|
|
403
|
+
try {
|
|
404
|
+
const renewed = await this.#renewLeasedAttachment?.(renewal);
|
|
405
|
+
if (!Number.isSafeInteger(renewed) || Number(renewed) <= this.#now()) {
|
|
406
|
+
this.revokeRoute(routeId, "leased Hosted Tools validation failed before admission");
|
|
407
|
+
throw new HostedToolsProtocolError("route_revoked", "leased Hosted Tools validation failed before admission");
|
|
408
|
+
}
|
|
409
|
+
maximumLeaseExpiresAt = Number(renewed);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
if (error instanceof HostedToolsProtocolError)
|
|
413
|
+
throw error;
|
|
414
|
+
throw new HostedToolsProtocolError("lease_validation_unavailable", "leased Hosted Tools validation was unavailable before admission");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
let state = this.#persistence.state(routeId) ?? emptyState(routeId);
|
|
418
|
+
if (state.lease_id === null && state.lease_expires_at === REVOKED_ROUTE_LEASE_EXPIRES_AT) {
|
|
419
|
+
throw new HostedToolsProtocolError("route_revoked", "tool attachment route was revoked before admission");
|
|
420
|
+
}
|
|
421
|
+
if (state.lease_id && state.lease_expires_at <= this.#now()) {
|
|
422
|
+
const expiredSocket = this.#socketForState(state);
|
|
423
|
+
if (expiredSocket)
|
|
424
|
+
this.#fence(expiredSocket, "Hosted Tools lease expired");
|
|
425
|
+
else
|
|
426
|
+
this.#retireState(state, "Hosted Tools lease expired");
|
|
427
|
+
state = this.#persistence.state(routeId) ?? emptyState(routeId);
|
|
428
|
+
}
|
|
429
|
+
const activeSocket = this.#socketForState(state);
|
|
430
|
+
const activeGrantId = activeSocket === undefined
|
|
431
|
+
? undefined
|
|
432
|
+
: this.#activeConnectGrantId(state);
|
|
433
|
+
if (activeSocket !== undefined && activeGrantId !== initial.connectGrantId) {
|
|
434
|
+
throw new HostedToolsProtocolError("grant_conflict", "another Connect grant already owns this agent's tool host");
|
|
435
|
+
}
|
|
436
|
+
const generation = ++this.#nextCandidateGeneration;
|
|
437
|
+
const leaseId = this.#randomUUID();
|
|
438
|
+
const expiresAt = Math.min(this.#now() + HOSTED_TOOLS_LEASE_MS, maximumLeaseExpiresAt ?? Number.MAX_SAFE_INTEGER);
|
|
439
|
+
const candidate = {
|
|
440
|
+
...initial,
|
|
441
|
+
...(maximumLeaseExpiresAt === undefined ? {} : { maximumLeaseExpiresAt }),
|
|
442
|
+
routeId,
|
|
443
|
+
leaseId,
|
|
444
|
+
generation,
|
|
445
|
+
};
|
|
446
|
+
this.context.writeAttachment(socket, candidate);
|
|
447
|
+
const catalogJson = JSON.stringify(frame.tools);
|
|
448
|
+
const machine = frame.machines?.[0];
|
|
449
|
+
const candidateEntries = frame.tools
|
|
450
|
+
.filter((entry) => !reservedMachineEntry(entry, machine))
|
|
451
|
+
.map((entry) => exposedEntry(entry, machine));
|
|
452
|
+
const candidateDefinitions = candidateEntries.map((entry) => Object.freeze({
|
|
453
|
+
...entry,
|
|
454
|
+
definition: Object.freeze({
|
|
455
|
+
...entry.definition,
|
|
456
|
+
defer_loading: true,
|
|
457
|
+
}),
|
|
458
|
+
}));
|
|
459
|
+
try {
|
|
460
|
+
if (initial.connectGrantId !== undefined && (frame.machines?.length ?? 0) > 0) {
|
|
461
|
+
throw new Error("Connect tool hosts cannot publish account machine metadata");
|
|
462
|
+
}
|
|
463
|
+
if ((frame.machines?.length ?? 0) > 0
|
|
464
|
+
&& (frame.machines?.length !== 1 || frame.attachment_id !== frame.machines[0]?.id)) {
|
|
465
|
+
throw new Error("an account machine route requires one machine whose id equals attachment_id");
|
|
466
|
+
}
|
|
467
|
+
if (initial.expectedAttachmentId !== undefined
|
|
468
|
+
&& (frame.attachment_id !== initial.expectedAttachmentId
|
|
469
|
+
|| frame.machines?.length !== 1
|
|
470
|
+
|| frame.machines[0]?.id !== initial.expectedAttachmentId)) {
|
|
471
|
+
throw new Error("leased tool attachment must publish its exact assigned machine ID");
|
|
472
|
+
}
|
|
473
|
+
if (initial.expectedAttachmentId !== undefined) {
|
|
474
|
+
const extra = frame.tools.find((entry) => !MACHINE_TOOL_NAMES.has(entry.definition.name));
|
|
475
|
+
if (extra !== undefined) {
|
|
476
|
+
throw new Error("leased tool attachments may publish only canonical machine primitives");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (machine !== undefined)
|
|
480
|
+
validateMachineToolContracts(frame.tools);
|
|
481
|
+
if (initial.allowedMcpIds !== undefined) {
|
|
482
|
+
if (!isConnectGrantId(initial.connectGrantId)) {
|
|
483
|
+
throw new Error("Connect tool host is missing its exact grant binding");
|
|
484
|
+
}
|
|
485
|
+
const allowed = new Set(initial.allowedMcpIds);
|
|
486
|
+
const forbiddenMcp = frame.tools.find((entry) => {
|
|
487
|
+
const match = /^mcp:([A-Za-z0-9_-]{43})$/.exec(entry.provider);
|
|
488
|
+
return entry.provider.startsWith("mcp:")
|
|
489
|
+
&& (match === null || !allowed.has(match[1]));
|
|
490
|
+
});
|
|
491
|
+
if (forbiddenMcp) {
|
|
492
|
+
throw new Error(`tool ${forbiddenMcp.provider}:${forbiddenMcp.remote_name} is not authorized by the Connect grant`);
|
|
493
|
+
}
|
|
494
|
+
const appTools = frame.tools.filter((entry) => !entry.provider.startsWith("mcp:"));
|
|
495
|
+
const candidateDigest = appTools.length === 0
|
|
496
|
+
? undefined
|
|
497
|
+
: await hostedToolCatalogDigest(appTools);
|
|
498
|
+
if (candidateDigest !== initial.appToolCatalogDigest) {
|
|
499
|
+
throw new Error("the app-local tool catalog does not match the signed Connect grant");
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const otherBindings = this.#publicCatalogBindings(routeId, true);
|
|
503
|
+
const exposedNames = new Set(otherBindings.map((binding) => binding.entry.definition.name));
|
|
504
|
+
const duplicateTool = candidateEntries.find((entry) => exposedNames.has(entry.definition.name));
|
|
505
|
+
if (duplicateTool) {
|
|
506
|
+
throw new Error(`tool name ${duplicateTool.definition.name} is already exposed by another attachment`);
|
|
507
|
+
}
|
|
508
|
+
if (initial.connectGrantId === undefined) {
|
|
509
|
+
const machineIds = new Set(this.#machineIds(routeId));
|
|
510
|
+
const duplicateMachine = frame.machines?.find((machine) => machineIds.has(machine.id));
|
|
511
|
+
if (duplicateMachine) {
|
|
512
|
+
throw new Error(`machine ID ${duplicateMachine.id} is already published by another attachment`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const validator = this.#catalogValidator;
|
|
516
|
+
const aggregateDefinitions = [
|
|
517
|
+
...otherBindings.map((binding) => Object.freeze({
|
|
518
|
+
...binding.entry,
|
|
519
|
+
definition: Object.freeze({
|
|
520
|
+
...binding.entry.definition,
|
|
521
|
+
defer_loading: true,
|
|
522
|
+
}),
|
|
523
|
+
})),
|
|
524
|
+
...candidateDefinitions,
|
|
525
|
+
].sort((left, right) => left.definition.name.localeCompare(right.definition.name));
|
|
526
|
+
if (validator !== undefined && validator(aggregateDefinitions) !== true) {
|
|
527
|
+
throw new Error("ToolRouter rejected the candidate catalog");
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
throw new HostedToolsProtocolError("catalog_contract_mismatch", `candidate catalog is incompatible with the managed tool route: ${errorMessage(error)}`);
|
|
532
|
+
}
|
|
533
|
+
const now = this.#now();
|
|
534
|
+
const replaced = state.lease_id ? state : undefined;
|
|
535
|
+
// A failed ready send must leave the previous attachment routable.
|
|
536
|
+
this.#send(socket, { type: "ready" });
|
|
537
|
+
this.#persistence.transaction(() => {
|
|
538
|
+
if (state.lease_id) {
|
|
539
|
+
this.#persistence.markGenerationAmbiguous(state.lease_id, state.generation, JSON.stringify(hostedToolsAmbiguous("Hosted Tools call became ambiguous when its host was replaced")), now);
|
|
540
|
+
}
|
|
541
|
+
this.#persistence.replaceHost({
|
|
542
|
+
route_id: routeId,
|
|
543
|
+
generation,
|
|
544
|
+
host_id: candidate.sessionId,
|
|
545
|
+
lease_id: leaseId,
|
|
546
|
+
lease_expires_at: expiresAt,
|
|
547
|
+
catalog_json: catalogJson,
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
if (replaced?.lease_id) {
|
|
551
|
+
const outcome = hostedToolsAmbiguous("Hosted Tools call became ambiguous when its host was replaced");
|
|
552
|
+
this.#resolveGeneration(replaced.lease_id, replaced.generation, outcome);
|
|
553
|
+
for (const existing of this.context.sockets()) {
|
|
554
|
+
if (existing === socket)
|
|
555
|
+
continue;
|
|
556
|
+
const old = this.#attachment(existing);
|
|
557
|
+
if (!old?.active || old.leaseId !== replaced.lease_id || old.generation !== replaced.generation)
|
|
558
|
+
continue;
|
|
559
|
+
closeSocket(existing, 1008, "Hosted Tools attachment replaced");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
this.context.writeAttachment(socket, {
|
|
563
|
+
...candidate,
|
|
564
|
+
active: true,
|
|
565
|
+
...(frame.machines === undefined ? {} : { machines: frame.machines }),
|
|
566
|
+
});
|
|
567
|
+
this.#notifyCatalogChanged();
|
|
568
|
+
}
|
|
569
|
+
#drain(socket) {
|
|
570
|
+
const attachment = this.#activeAttachment(socket);
|
|
571
|
+
if (attachment.draining) {
|
|
572
|
+
throw new HostedToolsProtocolError("already_draining", "socket is already draining");
|
|
573
|
+
}
|
|
574
|
+
const state = this.#persistence.state(attachment.routeId);
|
|
575
|
+
// Visibility is removed before the peer is told that draining began.
|
|
576
|
+
this.#persistence.clearCatalog(state.lease_id, state.generation);
|
|
577
|
+
this.context.writeAttachment(socket, { ...attachment, draining: true });
|
|
578
|
+
this.#notifyCatalogChanged();
|
|
579
|
+
this.#send(socket, { type: "draining" });
|
|
580
|
+
}
|
|
581
|
+
#completeResult(socket, frame) {
|
|
582
|
+
const attachment = this.#activeAttachment(socket);
|
|
583
|
+
const row = this.#persistence.call(frame.call_id);
|
|
584
|
+
const stored = JSON.stringify(frame.outcome);
|
|
585
|
+
if (!row
|
|
586
|
+
|| row.lease_id !== attachment.leaseId
|
|
587
|
+
|| row.generation !== attachment.generation) {
|
|
588
|
+
throw new HostedToolsProtocolError("unknown_call", "result does not match an admitted pinned call");
|
|
589
|
+
}
|
|
590
|
+
if (row.state === "ambiguous") {
|
|
591
|
+
const receiptJson = JSON.stringify({ type: "result", outcome: frame.outcome });
|
|
592
|
+
const recorded = this.#persistence.recordLateReceipt(row.call_id, receiptJson, this.#now());
|
|
593
|
+
if (!recorded || recorded.receipt_json !== receiptJson) {
|
|
594
|
+
throw new HostedToolsProtocolError("result_conflict", "late terminal receipt conflicts with retained proof");
|
|
595
|
+
}
|
|
596
|
+
this.#ackResult(socket, frame);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (row.state !== "dispatched") {
|
|
600
|
+
if (row.result_json === stored && row.state === outcomeState(frame.outcome)) {
|
|
601
|
+
this.#ackResult(socket, frame);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
throw new HostedToolsProtocolError("result_conflict", "terminal call result cannot be changed");
|
|
605
|
+
}
|
|
606
|
+
if (this.#now() >= row.deadline_at) {
|
|
607
|
+
this.#finishAmbiguous(row, "Hosted Tools call result arrived after its durable deadline");
|
|
608
|
+
const receiptJson = JSON.stringify({ type: "result", outcome: frame.outcome });
|
|
609
|
+
const recorded = this.#persistence.recordLateReceipt(row.call_id, receiptJson, this.#now());
|
|
610
|
+
if (!recorded || recorded.receipt_json !== receiptJson) {
|
|
611
|
+
throw new HostedToolsProtocolError("result_conflict", "late terminal receipt conflicts with retained proof");
|
|
612
|
+
}
|
|
613
|
+
this.#ackResult(socket, frame);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (frame.outcome.status === "completed"
|
|
617
|
+
&& encoder.encode(JSON.stringify(frame.outcome.output)).byteLength > row.output_byte_budget) {
|
|
618
|
+
throw new HostedToolsProtocolError("output_budget_exceeded", "completed output exceeds the byte budget pinned to the call");
|
|
619
|
+
}
|
|
620
|
+
const completed = this.#persistence.transitionCall(row.call_id, ["dispatched"], outcomeState(frame.outcome), stored, this.#now());
|
|
621
|
+
if (!completed || completed.result_json !== stored) {
|
|
622
|
+
throw new HostedToolsProtocolError("result_conflict", "call result lost durable ownership");
|
|
623
|
+
}
|
|
624
|
+
const pending = this.#takePending(row.call_id);
|
|
625
|
+
this.#pruneReceipts();
|
|
626
|
+
this.#ackResult(socket, frame);
|
|
627
|
+
pending?.resolve(frame.outcome);
|
|
628
|
+
}
|
|
629
|
+
#ackResult(socket, frame) {
|
|
630
|
+
try {
|
|
631
|
+
this.#send(socket, {
|
|
632
|
+
type: "ack",
|
|
633
|
+
call_id: frame.call_id,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
this.#retire(socket, "result acknowledgement delivery failed");
|
|
638
|
+
closeSocket(socket, 1011, "Hosted Tools result acknowledgement failed");
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
#definitions() {
|
|
642
|
+
return this.#publicCatalogBindings().map((binding) => binding.entry);
|
|
643
|
+
}
|
|
644
|
+
#resolve(name) {
|
|
645
|
+
const binding = this.#publicCatalogBindings()
|
|
646
|
+
.find((candidate) => candidate.entry.definition.name === name);
|
|
647
|
+
if (!binding)
|
|
648
|
+
return undefined;
|
|
649
|
+
return this.#preparedTool(binding);
|
|
650
|
+
}
|
|
651
|
+
#preparedTool(binding) {
|
|
652
|
+
return Object.freeze({
|
|
653
|
+
routeToken: JSON.stringify([
|
|
654
|
+
binding.routeId,
|
|
655
|
+
binding.generation,
|
|
656
|
+
binding.leaseId,
|
|
657
|
+
binding.wireName,
|
|
658
|
+
]),
|
|
659
|
+
...(binding.connectGrantId === undefined ? {} : { connectGrantId: binding.connectGrantId }),
|
|
660
|
+
...(binding.appToolCatalogDigest === undefined
|
|
661
|
+
? {}
|
|
662
|
+
: { appToolCatalogDigest: binding.appToolCatalogDigest }),
|
|
663
|
+
canonicalName: binding.wireName,
|
|
664
|
+
...(binding.machine === undefined ? {} : { machine: binding.machine }),
|
|
665
|
+
entry: binding.entry,
|
|
666
|
+
invoke: (request) => this.#invoke(binding, request),
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
#codeTool(name, prepared) {
|
|
670
|
+
return Object.freeze({
|
|
671
|
+
name,
|
|
672
|
+
parallelSafe: prepared.entry.parallel_safe,
|
|
673
|
+
routeToken: prepared.routeToken,
|
|
674
|
+
provider: prepared.entry.provider,
|
|
675
|
+
remoteName: prepared.entry.remote_name,
|
|
676
|
+
summary: prepared.entry.summary,
|
|
677
|
+
timeoutMs: prepared.entry.timeout_ms,
|
|
678
|
+
handler: async (input, context) => {
|
|
679
|
+
if (!this.#entryAllowed(prepared.entry, prepared.connectGrantId, prepared.appToolCatalogDigest, context)) {
|
|
680
|
+
return toolResult("Hosted tool is outside the active grant", {
|
|
681
|
+
status: "unavailable",
|
|
682
|
+
message: "Hosted tool is outside the active grant",
|
|
683
|
+
}, false, null);
|
|
684
|
+
}
|
|
685
|
+
const outcome = await prepared.invoke({
|
|
686
|
+
sessionId: context.sessionId,
|
|
687
|
+
callId: context.callId,
|
|
688
|
+
model: context.model ?? "unknown",
|
|
689
|
+
input: input,
|
|
690
|
+
outputTokenBudget: 10_000,
|
|
691
|
+
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
692
|
+
});
|
|
693
|
+
if (outcome.status === "completed") {
|
|
694
|
+
return wireToolResult(outcome.output, prepared.canonicalName, prepared.machine);
|
|
695
|
+
}
|
|
696
|
+
const result = toolResult(outcome.message, outcome, false, null);
|
|
697
|
+
return outcome[HOSTED_TOOLS_PRE_ADMISSION_UNAVAILABLE] === true
|
|
698
|
+
? Object.freeze({
|
|
699
|
+
...result,
|
|
700
|
+
[HOSTED_TOOLS_PRE_ADMISSION_UNAVAILABLE]: true,
|
|
701
|
+
})
|
|
702
|
+
: result;
|
|
703
|
+
},
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
#invoke(binding, request) {
|
|
707
|
+
const retained = this.#persistence.callBySource(request.sessionId, request.callId);
|
|
708
|
+
const leaseId = binding.leaseId;
|
|
709
|
+
const now = this.#now();
|
|
710
|
+
const deadlineAt = request.deadlineAt === undefined && retained
|
|
711
|
+
? retained.deadline_at
|
|
712
|
+
: Math.min(request.deadlineAt ?? Number.MAX_SAFE_INTEGER, now + Math.min(binding.entry.timeout_ms, HOSTED_TOOL_CALL_TIMEOUT_MS));
|
|
713
|
+
const outputByteBudget = request.outputByteBudget ?? MAX_HOSTED_TOOL_OUTPUT_BYTES;
|
|
714
|
+
const transportCallId = retained?.call_id ?? this.#randomUUID();
|
|
715
|
+
const hostId = retained?.host_id ?? binding.hostId;
|
|
716
|
+
const pinnedLeaseId = retained?.lease_id ?? binding.leaseId;
|
|
717
|
+
const generation = retained?.generation ?? binding.generation;
|
|
718
|
+
let call;
|
|
719
|
+
try {
|
|
720
|
+
call = parseHostedToolsManagedFrame(JSON.stringify({
|
|
721
|
+
type: "call",
|
|
722
|
+
session_id: request.sessionId,
|
|
723
|
+
call_id: transportCallId,
|
|
724
|
+
model: request.model,
|
|
725
|
+
name: binding.wireName,
|
|
726
|
+
input: request.input,
|
|
727
|
+
output_token_budget: request.outputTokenBudget,
|
|
728
|
+
output_byte_budget: outputByteBudget,
|
|
729
|
+
deadline_at: deadlineAt,
|
|
730
|
+
}));
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
return Promise.resolve(hostedToolsUnavailable(`Hosted Tools call was invalid before dispatch: ${errorMessage(error)}`));
|
|
734
|
+
}
|
|
735
|
+
const inputJson = JSON.stringify(call.input);
|
|
736
|
+
const proposed = {
|
|
737
|
+
call_id: call.call_id,
|
|
738
|
+
session_id: call.session_id,
|
|
739
|
+
source_call_id: request.callId,
|
|
740
|
+
host_id: hostId,
|
|
741
|
+
lease_id: pinnedLeaseId,
|
|
742
|
+
generation,
|
|
743
|
+
model: call.model,
|
|
744
|
+
name: call.name,
|
|
745
|
+
input_json: inputJson,
|
|
746
|
+
output_token_budget: call.output_token_budget,
|
|
747
|
+
output_byte_budget: call.output_byte_budget,
|
|
748
|
+
deadline_at: call.deadline_at,
|
|
749
|
+
cancel_requested: 0,
|
|
750
|
+
state: "admitted",
|
|
751
|
+
result_json: null,
|
|
752
|
+
receipt_json: null,
|
|
753
|
+
};
|
|
754
|
+
if (retained)
|
|
755
|
+
return this.#repeatedCall(retained, proposed);
|
|
756
|
+
const existing = this.#persistence.call(call.call_id);
|
|
757
|
+
if (existing)
|
|
758
|
+
return this.#repeatedCall(existing, proposed);
|
|
759
|
+
if (!this.#attachmentIsPresent(binding, now)) {
|
|
760
|
+
return Promise.resolve(preAdmissionUnavailable("Hosted Tools attachment was absent before durable admission"));
|
|
761
|
+
}
|
|
762
|
+
if (this.#persistence.generationCallCount(leaseId, binding.generation)
|
|
763
|
+
>= this.#maxCallsPerGeneration) {
|
|
764
|
+
const state = this.#persistence.state(binding.routeId);
|
|
765
|
+
const socket = state?.lease_id === leaseId && state.generation === binding.generation
|
|
766
|
+
? this.#socketForState(state)
|
|
767
|
+
: undefined;
|
|
768
|
+
if (socket)
|
|
769
|
+
this.#fence(socket, "Hosted Tools generation exhausted its durable call ledger");
|
|
770
|
+
else if (state?.lease_id === leaseId && state.generation === binding.generation) {
|
|
771
|
+
this.#retireState(state, "Hosted Tools generation exhausted its durable call ledger");
|
|
772
|
+
}
|
|
773
|
+
return Promise.resolve(hostedToolsUnavailable("Hosted Tools generation reached its durable call limit"));
|
|
774
|
+
}
|
|
775
|
+
try {
|
|
776
|
+
this.#persistence.insertCall(proposed, now);
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
const recovered = this.#persistence.callBySource(request.sessionId, request.callId)
|
|
780
|
+
?? this.#persistence.call(call.call_id);
|
|
781
|
+
if (recovered)
|
|
782
|
+
return this.#repeatedCall(recovered, proposed);
|
|
783
|
+
return Promise.resolve(hostedToolsAmbiguous("Hosted Tools admission may have persisted; replay is unsafe"));
|
|
784
|
+
}
|
|
785
|
+
if (request.signal?.aborted) {
|
|
786
|
+
return Promise.resolve(this.#finishBeforeDispatch(proposed, "cancelled", {
|
|
787
|
+
status: "cancelled",
|
|
788
|
+
message: "Hosted Tools call was cancelled before dispatch",
|
|
789
|
+
}));
|
|
790
|
+
}
|
|
791
|
+
const current = this.#persistence.state(binding.routeId);
|
|
792
|
+
const dispatchNow = this.#now();
|
|
793
|
+
const socket = this.#routingSocketForState(current);
|
|
794
|
+
if (!socket
|
|
795
|
+
|| !current
|
|
796
|
+
|| current.host_id !== binding.hostId
|
|
797
|
+
|| current.lease_id !== leaseId
|
|
798
|
+
|| current.generation !== binding.generation
|
|
799
|
+
|| current.lease_expires_at <= dispatchNow
|
|
800
|
+
|| deadlineAt <= dispatchNow) {
|
|
801
|
+
return Promise.resolve(this.#finishBeforeDispatch(proposed, "unavailable", hostedToolsUnavailable("Hosted Tools binding became unavailable before dispatch")));
|
|
802
|
+
}
|
|
803
|
+
if (this.#persistence.activeCallCount(leaseId, binding.generation) > this.#maxInFlight) {
|
|
804
|
+
return Promise.resolve(this.#finishBeforeDispatch(proposed, "unavailable", hostedToolsUnavailable("Hosted Tools host is at its bounded in-flight limit")));
|
|
805
|
+
}
|
|
806
|
+
const dispatched = this.#persistence.transitionCall(call.call_id, ["admitted"], "dispatched", "", dispatchNow);
|
|
807
|
+
if (!dispatched || dispatched.state !== "dispatched") {
|
|
808
|
+
return Promise.resolve(hostedToolsAmbiguous("Hosted Tools call lost durable dispatch ownership"));
|
|
809
|
+
}
|
|
810
|
+
let resolve;
|
|
811
|
+
const promise = new Promise((completed) => { resolve = completed; });
|
|
812
|
+
const pending = {
|
|
813
|
+
leaseId,
|
|
814
|
+
generation: binding.generation,
|
|
815
|
+
deadlineAt,
|
|
816
|
+
promise,
|
|
817
|
+
resolve,
|
|
818
|
+
};
|
|
819
|
+
this.#pending.set(call.call_id, pending);
|
|
820
|
+
if (request.signal) {
|
|
821
|
+
const cancel = () => { this.cancel(call.call_id); };
|
|
822
|
+
request.signal.addEventListener("abort", cancel, { once: true });
|
|
823
|
+
pending.removeAbort = () => request.signal?.removeEventListener("abort", cancel);
|
|
824
|
+
}
|
|
825
|
+
this.#armExpiry(call.call_id, pending, Math.min(current.lease_expires_at, deadlineAt));
|
|
826
|
+
try {
|
|
827
|
+
this.#send(socket, call);
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
this.#retire(socket, "call delivery failed");
|
|
831
|
+
closeSocket(socket, 1011, "Hosted Tools call delivery failed");
|
|
832
|
+
}
|
|
833
|
+
return promise;
|
|
834
|
+
}
|
|
835
|
+
#attachmentIsPresent(binding, now) {
|
|
836
|
+
const current = this.#persistence.state(binding.routeId);
|
|
837
|
+
return current !== undefined
|
|
838
|
+
&& current.host_id === binding.hostId
|
|
839
|
+
&& current.lease_id === binding.leaseId
|
|
840
|
+
&& current.generation === binding.generation
|
|
841
|
+
&& current.lease_expires_at > now
|
|
842
|
+
&& this.#routingSocketForState(current) !== undefined;
|
|
843
|
+
}
|
|
844
|
+
#repeatedCall(existing, proposed) {
|
|
845
|
+
if (!sameImmutableCall(existing, proposed)) {
|
|
846
|
+
const state = this.#stateForLease(existing.lease_id, existing.generation);
|
|
847
|
+
const socket = this.#socketForState(state);
|
|
848
|
+
if (socket)
|
|
849
|
+
this.#fence(socket, "call ID was reused with different immutable fields");
|
|
850
|
+
return Promise.resolve(hostedToolsAmbiguous("Hosted Tools call ID conflicts with retained durable state"));
|
|
851
|
+
}
|
|
852
|
+
if (existing.result_json)
|
|
853
|
+
return Promise.resolve(JSON.parse(existing.result_json));
|
|
854
|
+
const pending = this.#pending.get(existing.call_id);
|
|
855
|
+
if (existing.state === "dispatched" && pending)
|
|
856
|
+
return pending.promise;
|
|
857
|
+
return Promise.resolve(existing.state === "admitted"
|
|
858
|
+
? hostedToolsUnavailable("Hosted Tools call was admitted but never dispatched")
|
|
859
|
+
: hostedToolsAmbiguous("Hosted Tools call has no retained terminal receipt"));
|
|
860
|
+
}
|
|
861
|
+
#finishBeforeDispatch(row, state, outcome) {
|
|
862
|
+
this.#persistence.transitionCall(row.call_id, ["admitted"], state, JSON.stringify(outcome), this.#now());
|
|
863
|
+
this.#pruneReceipts();
|
|
864
|
+
return outcome;
|
|
865
|
+
}
|
|
866
|
+
#armExpiry(callId, pending, at) {
|
|
867
|
+
if (pending.timeout !== undefined)
|
|
868
|
+
clearTimeout(pending.timeout);
|
|
869
|
+
pending.timeout = setTimeout(() => {
|
|
870
|
+
const current = this.#pending.get(callId);
|
|
871
|
+
if (current !== pending)
|
|
872
|
+
return;
|
|
873
|
+
const state = this.#stateForLease(pending.leaseId, pending.generation);
|
|
874
|
+
const now = this.#now();
|
|
875
|
+
if (state?.lease_id === pending.leaseId
|
|
876
|
+
&& state.generation === pending.generation
|
|
877
|
+
&& state.lease_expires_at > now
|
|
878
|
+
&& pending.deadlineAt > now) {
|
|
879
|
+
this.#armExpiry(callId, pending, Math.min(state.lease_expires_at, pending.deadlineAt));
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (state?.lease_id === pending.leaseId
|
|
883
|
+
&& state.generation === pending.generation
|
|
884
|
+
&& state.lease_expires_at <= now) {
|
|
885
|
+
const socket = this.#socketForState(state);
|
|
886
|
+
if (socket)
|
|
887
|
+
this.#fence(socket, "Hosted Tools lease expired during a call");
|
|
888
|
+
else
|
|
889
|
+
this.#retireState(state, "Hosted Tools lease expired during a call");
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const row = this.#persistence.call(callId);
|
|
893
|
+
if (row) {
|
|
894
|
+
this.cancel(callId);
|
|
895
|
+
this.#finishAmbiguous(row, "Hosted Tools call deadline expired after dispatch");
|
|
896
|
+
}
|
|
897
|
+
}, Math.max(1, at - this.#now()));
|
|
898
|
+
}
|
|
899
|
+
#finishAmbiguous(row, message) {
|
|
900
|
+
const outcome = hostedToolsAmbiguous(message);
|
|
901
|
+
this.#persistence.transitionCall(row.call_id, ["dispatched"], "ambiguous", JSON.stringify(outcome), this.#now());
|
|
902
|
+
this.#pruneReceipts();
|
|
903
|
+
this.#takePending(row.call_id)?.resolve(outcome);
|
|
904
|
+
}
|
|
905
|
+
#retire(socket, reason) {
|
|
906
|
+
const attachment = this.#attachment(socket);
|
|
907
|
+
if (!attachment?.leaseId || attachment.generation === undefined)
|
|
908
|
+
return;
|
|
909
|
+
const state = attachment.routeId === undefined
|
|
910
|
+
? undefined
|
|
911
|
+
: this.#persistence.state(attachment.routeId);
|
|
912
|
+
if (state)
|
|
913
|
+
this.#retireState(state, reason, attachment.leaseId, attachment.generation);
|
|
914
|
+
}
|
|
915
|
+
#retireState(state, reason, leaseId = state.lease_id ?? undefined, generation = state.generation) {
|
|
916
|
+
if (!leaseId)
|
|
917
|
+
return;
|
|
918
|
+
const outcome = hostedToolsAmbiguous(`Hosted Tools outcome is ambiguous after transport loss: ${reason}`);
|
|
919
|
+
this.#persistence.transaction(() => {
|
|
920
|
+
this.#persistence.markGenerationAmbiguous(leaseId, generation, JSON.stringify(outcome), this.#now());
|
|
921
|
+
this.#persistence.clearHost(leaseId, generation);
|
|
922
|
+
});
|
|
923
|
+
this.#pruneReceipts();
|
|
924
|
+
this.#resolveGeneration(leaseId, generation, outcome);
|
|
925
|
+
this.#notifyCatalogChanged();
|
|
926
|
+
}
|
|
927
|
+
#resolveGeneration(leaseId, generation, outcome) {
|
|
928
|
+
for (const [callId, pending] of this.#pending) {
|
|
929
|
+
if (pending.leaseId !== leaseId || pending.generation !== generation)
|
|
930
|
+
continue;
|
|
931
|
+
this.#takePending(callId)?.resolve(outcome);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
#pruneReceipts() {
|
|
935
|
+
this.#persistence.pruneReceipts(MAX_RETAINED_RECEIPTS);
|
|
936
|
+
}
|
|
937
|
+
#takePending(callId) {
|
|
938
|
+
const pending = this.#pending.get(callId);
|
|
939
|
+
if (!pending)
|
|
940
|
+
return undefined;
|
|
941
|
+
if (pending.timeout !== undefined)
|
|
942
|
+
clearTimeout(pending.timeout);
|
|
943
|
+
pending.removeAbort?.();
|
|
944
|
+
this.#pending.delete(callId);
|
|
945
|
+
return pending;
|
|
946
|
+
}
|
|
947
|
+
#fence(socket, reason, code = 1008) {
|
|
948
|
+
const attachment = this.#attachment(socket);
|
|
949
|
+
if (attachment?.leaseId && attachment.generation !== undefined) {
|
|
950
|
+
this.#retire(socket, reason);
|
|
951
|
+
}
|
|
952
|
+
closeSocket(socket, code, boundedReason(reason));
|
|
953
|
+
}
|
|
954
|
+
#socketForState(state) {
|
|
955
|
+
if (!state)
|
|
956
|
+
return undefined;
|
|
957
|
+
if (!state.host_id || !state.lease_id)
|
|
958
|
+
return undefined;
|
|
959
|
+
return this.context.sockets().find((socket) => {
|
|
960
|
+
const attachment = this.#attachment(socket);
|
|
961
|
+
return socket.readyState === OPEN
|
|
962
|
+
&& attachment?.leaseId === state.lease_id
|
|
963
|
+
&& attachment.generation === state.generation
|
|
964
|
+
&& attachment.routeId === state.route_id
|
|
965
|
+
&& attachment.active === true;
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
#routingSocketForState(state) {
|
|
969
|
+
if (!state)
|
|
970
|
+
return undefined;
|
|
971
|
+
if (!state.catalog_json)
|
|
972
|
+
return undefined;
|
|
973
|
+
const socket = this.#socketForState(state);
|
|
974
|
+
return socket && this.#attachment(socket)?.draining !== true ? socket : undefined;
|
|
975
|
+
}
|
|
976
|
+
#liveRoutingSocketForState(state) {
|
|
977
|
+
if (state.lease_id && state.lease_expires_at <= this.#now()) {
|
|
978
|
+
const socket = this.#socketForState(state);
|
|
979
|
+
if (socket)
|
|
980
|
+
this.#fence(socket, "Hosted Tools lease expired");
|
|
981
|
+
else
|
|
982
|
+
this.#retireState(state, "Hosted Tools lease expired");
|
|
983
|
+
return undefined;
|
|
984
|
+
}
|
|
985
|
+
return this.#routingSocketForState(state);
|
|
986
|
+
}
|
|
987
|
+
#activeConnectGrantId(state) {
|
|
988
|
+
const socket = this.#socketForState(state);
|
|
989
|
+
if (socket === undefined)
|
|
990
|
+
return undefined;
|
|
991
|
+
const attachment = this.#attachment(socket);
|
|
992
|
+
if (attachment?.connectGrantId === undefined)
|
|
993
|
+
return undefined;
|
|
994
|
+
return isConnectGrantId(attachment.connectGrantId)
|
|
995
|
+
? attachment.connectGrantId
|
|
996
|
+
: INVALID_CONNECT_GRANT_ID;
|
|
997
|
+
}
|
|
998
|
+
#activeAppToolCatalogDigest(state) {
|
|
999
|
+
const socket = this.#socketForState(state);
|
|
1000
|
+
return socket === undefined ? undefined : this.#attachment(socket)?.appToolCatalogDigest;
|
|
1001
|
+
}
|
|
1002
|
+
#sortedStates() {
|
|
1003
|
+
return [...this.#persistence.states()].sort((left, right) => left.route_id.localeCompare(right.route_id));
|
|
1004
|
+
}
|
|
1005
|
+
#stateForLease(leaseId, generation) {
|
|
1006
|
+
return this.#persistence.states().find((state) => state.lease_id === leaseId
|
|
1007
|
+
&& state.generation === generation);
|
|
1008
|
+
}
|
|
1009
|
+
#catalogBindings(excludeRouteId, includeAmbiguous = false) {
|
|
1010
|
+
const bindings = [];
|
|
1011
|
+
for (const state of this.#sortedStates()) {
|
|
1012
|
+
if (state.route_id === excludeRouteId || !state.host_id || !state.lease_id || !state.catalog_json)
|
|
1013
|
+
continue;
|
|
1014
|
+
const socket = this.#liveRoutingSocketForState(state);
|
|
1015
|
+
if (!socket)
|
|
1016
|
+
continue;
|
|
1017
|
+
const attachment = this.#attachment(socket);
|
|
1018
|
+
const connectGrantId = this.#activeConnectGrantId(state);
|
|
1019
|
+
const appToolCatalogDigest = this.#activeAppToolCatalogDigest(state);
|
|
1020
|
+
let entries;
|
|
1021
|
+
try {
|
|
1022
|
+
entries = JSON.parse(state.catalog_json);
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
for (const entry of entries) {
|
|
1028
|
+
const machine = attachment?.machines?.[0];
|
|
1029
|
+
bindings.push(Object.freeze({
|
|
1030
|
+
routeId: state.route_id,
|
|
1031
|
+
hostId: state.host_id,
|
|
1032
|
+
leaseId: state.lease_id,
|
|
1033
|
+
generation: state.generation,
|
|
1034
|
+
wireName: entry.definition.name,
|
|
1035
|
+
...(machine === undefined ? {} : { machine }),
|
|
1036
|
+
entry: exposedEntry(entry, machine),
|
|
1037
|
+
...(connectGrantId === undefined ? {} : { connectGrantId }),
|
|
1038
|
+
...(appToolCatalogDigest === undefined ? {} : { appToolCatalogDigest }),
|
|
1039
|
+
}));
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
bindings.sort((left, right) => left.routeId.localeCompare(right.routeId)
|
|
1043
|
+
|| left.entry.definition.name.localeCompare(right.entry.definition.name));
|
|
1044
|
+
if (includeAmbiguous)
|
|
1045
|
+
return bindings;
|
|
1046
|
+
const counts = new Map();
|
|
1047
|
+
for (const binding of bindings) {
|
|
1048
|
+
const name = binding.entry.definition.name;
|
|
1049
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
1050
|
+
}
|
|
1051
|
+
return bindings.filter((binding) => counts.get(binding.entry.definition.name) === 1);
|
|
1052
|
+
}
|
|
1053
|
+
#publicCatalogBindings(excludeRouteId, includeAmbiguous = false) {
|
|
1054
|
+
return this.#catalogBindings(excludeRouteId, includeAmbiguous)
|
|
1055
|
+
.filter((binding) => !reservedMachineBinding(binding));
|
|
1056
|
+
}
|
|
1057
|
+
#machineIds(excludeRouteId) {
|
|
1058
|
+
const ids = [];
|
|
1059
|
+
for (const state of this.#sortedStates()) {
|
|
1060
|
+
if (state.route_id === excludeRouteId)
|
|
1061
|
+
continue;
|
|
1062
|
+
const socket = this.#liveRoutingSocketForState(state);
|
|
1063
|
+
if (!socket)
|
|
1064
|
+
continue;
|
|
1065
|
+
const attachment = this.#attachment(socket);
|
|
1066
|
+
if (attachment?.connectGrantId !== undefined)
|
|
1067
|
+
continue;
|
|
1068
|
+
for (const machine of attachment?.machines ?? [])
|
|
1069
|
+
ids.push(machine.id);
|
|
1070
|
+
}
|
|
1071
|
+
return ids;
|
|
1072
|
+
}
|
|
1073
|
+
#attachment(socket) {
|
|
1074
|
+
const value = this.context.readAttachment(socket);
|
|
1075
|
+
return value?.kind === SOCKET_TAG ? value : undefined;
|
|
1076
|
+
}
|
|
1077
|
+
#send(socket, frame) {
|
|
1078
|
+
socket.send(JSON.stringify(frame));
|
|
1079
|
+
}
|
|
1080
|
+
#notifyCatalogChanged() {
|
|
1081
|
+
this.#onCatalogChanged?.(this.#definitions());
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
function emptyState(routeId) {
|
|
1085
|
+
return {
|
|
1086
|
+
route_id: routeId,
|
|
1087
|
+
generation: 0,
|
|
1088
|
+
host_id: null,
|
|
1089
|
+
lease_id: null,
|
|
1090
|
+
lease_expires_at: 0,
|
|
1091
|
+
catalog_json: null,
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
function scopedRouteId(connectGrantId, attachmentId) {
|
|
1095
|
+
return `${connectGrantId === undefined ? "user" : "connect"}:${attachmentId ?? LEGACY_ROUTE_ID}`;
|
|
1096
|
+
}
|
|
1097
|
+
function reservedMachineEntry(entry, machine) {
|
|
1098
|
+
return machine !== undefined && MACHINE_TOOL_NAMES.has(entry.definition.name);
|
|
1099
|
+
}
|
|
1100
|
+
function reservedMachineBinding(binding) {
|
|
1101
|
+
return binding.machine !== undefined && MACHINE_TOOL_NAMES.has(binding.wireName);
|
|
1102
|
+
}
|
|
1103
|
+
function validateMachineToolContracts(entries) {
|
|
1104
|
+
for (const entry of entries) {
|
|
1105
|
+
const name = entry.definition.name;
|
|
1106
|
+
if (!MACHINE_TOOL_NAMES.has(name))
|
|
1107
|
+
continue;
|
|
1108
|
+
if (entry.definition.type !== "function") {
|
|
1109
|
+
throw new Error(`machine tool ${name} must use its canonical function schema`);
|
|
1110
|
+
}
|
|
1111
|
+
switch (name) {
|
|
1112
|
+
case "exec_command":
|
|
1113
|
+
validateCanonicalObjectSchema(name, entry.definition.parameters, EXEC_COMMAND_PARAMETERS);
|
|
1114
|
+
validateCanonicalObjectSchema(`${name} output`, entry.definition.output_schema, EXECUTION_OUTPUT_SCHEMA);
|
|
1115
|
+
break;
|
|
1116
|
+
case "write_stdin":
|
|
1117
|
+
validateCanonicalObjectSchema(name, entry.definition.parameters, WRITE_STDIN_PARAMETERS);
|
|
1118
|
+
validateCanonicalObjectSchema(`${name} output`, entry.definition.output_schema, EXECUTION_OUTPUT_SCHEMA);
|
|
1119
|
+
break;
|
|
1120
|
+
case "preview":
|
|
1121
|
+
validateCanonicalObjectSchema(name, entry.definition.parameters, MACHINE_PREVIEW_PARAMETERS);
|
|
1122
|
+
validateCanonicalObjectSchema(`${name} output`, entry.definition.output_schema, PREVIEW_OUTPUT_SCHEMA);
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function validateCanonicalObjectSchema(label, schema, canonical) {
|
|
1128
|
+
const properties = objectRecord(canonical.properties);
|
|
1129
|
+
const required = canonical.required;
|
|
1130
|
+
if (properties === undefined || !Array.isArray(required)) {
|
|
1131
|
+
throw new Error(`invalid canonical machine tool schema for ${label}`);
|
|
1132
|
+
}
|
|
1133
|
+
const propertyTypes = Object.fromEntries(Object.entries(properties).map(([name, property]) => {
|
|
1134
|
+
const type = objectRecord(property)?.type;
|
|
1135
|
+
if (typeof type !== "string")
|
|
1136
|
+
throw new Error(`invalid canonical type for ${label}.${name}`);
|
|
1137
|
+
return [name, type];
|
|
1138
|
+
}));
|
|
1139
|
+
validateObjectSchema(label, schema, required, propertyTypes);
|
|
1140
|
+
}
|
|
1141
|
+
function validateObjectSchema(label, schema, required, propertyTypes) {
|
|
1142
|
+
const properties = objectRecord(schema?.properties);
|
|
1143
|
+
const actualRequired = schema?.required;
|
|
1144
|
+
if (schema?.type !== "object"
|
|
1145
|
+
|| schema?.additionalProperties !== false
|
|
1146
|
+
|| properties === undefined
|
|
1147
|
+
|| !Array.isArray(actualRequired)
|
|
1148
|
+
|| actualRequired.some((value) => typeof value !== "string")
|
|
1149
|
+
|| !sameStrings(actualRequired, required)
|
|
1150
|
+
|| !sameStrings(Object.keys(properties), Object.keys(propertyTypes))) {
|
|
1151
|
+
throw new Error(`machine tool ${label} must use its canonical object schema`);
|
|
1152
|
+
}
|
|
1153
|
+
for (const [property, type] of Object.entries(propertyTypes)) {
|
|
1154
|
+
if (objectRecord(properties[property])?.type !== type) {
|
|
1155
|
+
throw new Error(`machine tool ${label}.${property} must use canonical type ${type}`);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
function objectRecord(value) {
|
|
1160
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
1161
|
+
? value
|
|
1162
|
+
: undefined;
|
|
1163
|
+
}
|
|
1164
|
+
function sameStrings(left, right) {
|
|
1165
|
+
return left.length === right.length && left.every((value) => right.includes(value));
|
|
1166
|
+
}
|
|
1167
|
+
function exposedEntry(entry, machine) {
|
|
1168
|
+
if (machine === undefined)
|
|
1169
|
+
return entry;
|
|
1170
|
+
const routeName = `user:${machine.id}:${entry.definition.name}`;
|
|
1171
|
+
const candidate = `user_${machine.id}_${entry.definition.name}`;
|
|
1172
|
+
const safeCandidate = candidate.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
1173
|
+
const exposedName = candidate === safeCandidate && candidate.length <= 128
|
|
1174
|
+
? candidate
|
|
1175
|
+
: `${safeCandidate.slice(0, 111)}_${stableHash(routeName)}`;
|
|
1176
|
+
const definition = {
|
|
1177
|
+
...entry.definition,
|
|
1178
|
+
name: exposedName,
|
|
1179
|
+
description: `Routes to ${routeName}. ${entry.definition.description}`,
|
|
1180
|
+
};
|
|
1181
|
+
return Object.freeze({
|
|
1182
|
+
...entry,
|
|
1183
|
+
definition: Object.freeze(definition),
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
function stableHash(value) {
|
|
1187
|
+
let hash = 0xcbf29ce484222325n;
|
|
1188
|
+
for (const byte of encoder.encode(value)) {
|
|
1189
|
+
hash ^= BigInt(byte);
|
|
1190
|
+
hash = BigInt.asUintN(64, hash * 0x100000001b3n);
|
|
1191
|
+
}
|
|
1192
|
+
return hash.toString(16).padStart(16, "0");
|
|
1193
|
+
}
|
|
1194
|
+
function isConnectGrantId(value) {
|
|
1195
|
+
return typeof value === "string" && /^0x[0-9a-f]{64}$/.test(value);
|
|
1196
|
+
}
|
|
1197
|
+
function wireToolResult(output, canonicalName, machine) {
|
|
1198
|
+
return toolResult(output.output, output.structured_result, output.success, toolExecutionMetadata(output.metadata, canonicalName, machine));
|
|
1199
|
+
}
|
|
1200
|
+
function toolExecutionMetadata(metadata, canonicalName, machine) {
|
|
1201
|
+
if (machine === undefined)
|
|
1202
|
+
return metadata;
|
|
1203
|
+
const execution = {
|
|
1204
|
+
machine_id: machine.id,
|
|
1205
|
+
machine_name: machine.name,
|
|
1206
|
+
tool_name: canonicalName,
|
|
1207
|
+
};
|
|
1208
|
+
if (metadata === null || metadata === undefined)
|
|
1209
|
+
return Object.freeze(execution);
|
|
1210
|
+
if (typeof metadata === "object" && !Array.isArray(metadata)) {
|
|
1211
|
+
return Object.freeze({ ...metadata, ...execution });
|
|
1212
|
+
}
|
|
1213
|
+
return Object.freeze({ ...execution, provider_metadata: metadata });
|
|
1214
|
+
}
|
|
1215
|
+
function toolResult(output, structuredResult, success, metadata) {
|
|
1216
|
+
return Object.freeze({
|
|
1217
|
+
[TOOL_RESULT]: true,
|
|
1218
|
+
metadata,
|
|
1219
|
+
output,
|
|
1220
|
+
structuredResult,
|
|
1221
|
+
success,
|
|
1222
|
+
value: structuredResult ?? output,
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
function sameImmutableCall(left, right) {
|
|
1226
|
+
return left.call_id === right.call_id
|
|
1227
|
+
&& left.session_id === right.session_id
|
|
1228
|
+
&& left.source_call_id === right.source_call_id
|
|
1229
|
+
&& left.host_id === right.host_id
|
|
1230
|
+
&& left.lease_id === right.lease_id
|
|
1231
|
+
&& left.generation === right.generation
|
|
1232
|
+
&& left.model === right.model
|
|
1233
|
+
&& left.name === right.name
|
|
1234
|
+
&& left.input_json === right.input_json
|
|
1235
|
+
&& left.output_token_budget === right.output_token_budget
|
|
1236
|
+
&& left.output_byte_budget === right.output_byte_budget
|
|
1237
|
+
&& left.deadline_at === right.deadline_at;
|
|
1238
|
+
}
|
|
1239
|
+
function outcomeState(outcome) {
|
|
1240
|
+
return outcome.status;
|
|
1241
|
+
}
|
|
1242
|
+
export function hostedToolsUnavailable(message) {
|
|
1243
|
+
return { status: "unavailable", message: boundedReason(message) };
|
|
1244
|
+
}
|
|
1245
|
+
function preAdmissionUnavailable(message) {
|
|
1246
|
+
return Object.freeze({
|
|
1247
|
+
...hostedToolsUnavailable(message),
|
|
1248
|
+
[HOSTED_TOOLS_PRE_ADMISSION_UNAVAILABLE]: true,
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
export function hostedToolsAmbiguous(message) {
|
|
1252
|
+
return { status: "ambiguous", message: boundedReason(message) };
|
|
1253
|
+
}
|
|
1254
|
+
function boundedReason(message) {
|
|
1255
|
+
if (encoder.encode(message).byteLength <= 2 * 1024)
|
|
1256
|
+
return message;
|
|
1257
|
+
return "Hosted Tools protocol failure exceeded the bounded reason limit";
|
|
1258
|
+
}
|
|
1259
|
+
function errorMessage(error) {
|
|
1260
|
+
return error instanceof Error ? error.message : String(error);
|
|
1261
|
+
}
|
|
1262
|
+
function closeSocket(socket, code, reason) {
|
|
1263
|
+
try {
|
|
1264
|
+
socket.close(code, websocketCloseReason(reason));
|
|
1265
|
+
}
|
|
1266
|
+
catch { /* The socket is already closed or never reached an open state. */ }
|
|
1267
|
+
}
|
|
1268
|
+
function websocketCloseReason(reason) {
|
|
1269
|
+
if (encoder.encode(reason).byteLength <= 123)
|
|
1270
|
+
return reason;
|
|
1271
|
+
let bounded = "";
|
|
1272
|
+
for (const scalar of reason) {
|
|
1273
|
+
if (encoder.encode(bounded + scalar).byteLength > 123)
|
|
1274
|
+
break;
|
|
1275
|
+
bounded += scalar;
|
|
1276
|
+
}
|
|
1277
|
+
return bounded;
|
|
1278
|
+
}
|