@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,603 @@
|
|
|
1
|
+
import { toolRouterRuntime } from "../runtime/tool-router.mjs";
|
|
2
|
+
import { utf8ByteLength } from "../runtime/utf8.mjs";
|
|
3
|
+
import { hostedCatalog } from "./hostedCatalog.mjs";
|
|
4
|
+
import { normalizeHostedMachines } from "./hostedMachine.mjs";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_HEARTBEAT_MS = 30_000;
|
|
7
|
+
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000;
|
|
8
|
+
const DEFAULT_DRAIN_TIMEOUT_MS = 10_000;
|
|
9
|
+
const OPEN = 1;
|
|
10
|
+
const TOOL_RESULT = Symbol.for("nanocodex.toolResult");
|
|
11
|
+
const ATTACHMENT_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,122}$/;
|
|
12
|
+
|
|
13
|
+
export class AttachmentRejectedError extends Error {
|
|
14
|
+
constructor(reason) {
|
|
15
|
+
super(`tool attachment rejected: ${reason || "policy violation"}`);
|
|
16
|
+
this.name = "AttachmentRejectedError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class AttachmentTransportError extends Error {
|
|
21
|
+
constructor(error) {
|
|
22
|
+
super(errorMessage(error));
|
|
23
|
+
this.name = "AttachmentTransportError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Creates the executor side of the socket-owned reverse tool protocol. */
|
|
28
|
+
export function createAttachment(owner, target, options = {}) {
|
|
29
|
+
const router = owner?.[toolRouterRuntime];
|
|
30
|
+
if (!router || typeof router.execute !== "function") {
|
|
31
|
+
throw new TypeError("attach requires a Tools runtime");
|
|
32
|
+
}
|
|
33
|
+
validateAttachmentOptions(target, options);
|
|
34
|
+
const machines = normalizeHostedMachines(options.machines);
|
|
35
|
+
const attachmentId = options.attachmentId;
|
|
36
|
+
if (machines.length > 0 && attachmentId !== machines[0].id) {
|
|
37
|
+
throw new TypeError("a machine attachment requires one machine whose id equals attachmentId");
|
|
38
|
+
}
|
|
39
|
+
const endpoint = attachmentEndpoint(target);
|
|
40
|
+
const transport = attachmentTransport(target);
|
|
41
|
+
let client;
|
|
42
|
+
let starting;
|
|
43
|
+
let stopped = false;
|
|
44
|
+
let resolveClosed;
|
|
45
|
+
const closed = new Promise((resolve) => { resolveClosed = resolve; });
|
|
46
|
+
return Object.freeze({
|
|
47
|
+
async connect() {
|
|
48
|
+
if (stopped) throw new Error("tool attachment connector is closed");
|
|
49
|
+
if (!starting) starting = (async () => {
|
|
50
|
+
await router.settled?.();
|
|
51
|
+
if (stopped) throw new Error("tool attachment connector is closed");
|
|
52
|
+
const admission = router.snapshot();
|
|
53
|
+
if (stopped) {
|
|
54
|
+
admission.release();
|
|
55
|
+
throw new Error("tool attachment connector is closed");
|
|
56
|
+
}
|
|
57
|
+
const created = createClient(endpoint, transport, options, admission, machines, attachmentId);
|
|
58
|
+
void created.public.closed().then(resolveClosed);
|
|
59
|
+
return created;
|
|
60
|
+
})();
|
|
61
|
+
client = await starting;
|
|
62
|
+
await client.ready;
|
|
63
|
+
return client.public;
|
|
64
|
+
},
|
|
65
|
+
close() {
|
|
66
|
+
stopped = true;
|
|
67
|
+
if (client) return client.public.close();
|
|
68
|
+
if (!starting) {
|
|
69
|
+
resolveClosed();
|
|
70
|
+
return closed;
|
|
71
|
+
}
|
|
72
|
+
resolveClosed();
|
|
73
|
+
void starting.then(
|
|
74
|
+
(created) => created.public.close(),
|
|
75
|
+
() => {},
|
|
76
|
+
);
|
|
77
|
+
return closed;
|
|
78
|
+
},
|
|
79
|
+
closed() { return closed; },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createClient(endpoint, transport, options, admission, machines, attachmentId) {
|
|
84
|
+
const state = {
|
|
85
|
+
socket: undefined,
|
|
86
|
+
catalog: hostedCatalog(admission.catalog(options.provider ?? "javascript")),
|
|
87
|
+
machines,
|
|
88
|
+
attachmentId,
|
|
89
|
+
calls: new Map(),
|
|
90
|
+
receipts: new Map(),
|
|
91
|
+
heartbeat: undefined,
|
|
92
|
+
handshakeTimer: undefined,
|
|
93
|
+
drainTimer: undefined,
|
|
94
|
+
reconnectTimer: undefined,
|
|
95
|
+
pendingNonce: undefined,
|
|
96
|
+
catalogSent: false,
|
|
97
|
+
readyReceived: false,
|
|
98
|
+
stopped: false,
|
|
99
|
+
draining: false,
|
|
100
|
+
drainAcknowledged: false,
|
|
101
|
+
connected: false,
|
|
102
|
+
readySettled: false,
|
|
103
|
+
admissionReleased: false,
|
|
104
|
+
};
|
|
105
|
+
let resolveReady;
|
|
106
|
+
let rejectReady;
|
|
107
|
+
let resolveClosed;
|
|
108
|
+
const ready = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
|
|
109
|
+
const closed = new Promise((resolve) => { resolveClosed = resolve; });
|
|
110
|
+
const publicClient = Object.freeze({
|
|
111
|
+
get connected() { return state.connected; },
|
|
112
|
+
closed() { return closed; },
|
|
113
|
+
close() {
|
|
114
|
+
if (state.stopped) return closed;
|
|
115
|
+
state.stopped = true;
|
|
116
|
+
clearTimeout(state.reconnectTimer);
|
|
117
|
+
state.reconnectTimer = undefined;
|
|
118
|
+
const socket = state.socket;
|
|
119
|
+
if (!socket || !state.readySettled) {
|
|
120
|
+
clearTimers(state);
|
|
121
|
+
if (!state.readySettled) {
|
|
122
|
+
state.readySettled = true;
|
|
123
|
+
rejectReady(new Error("tool attachment detached"));
|
|
124
|
+
}
|
|
125
|
+
abortGeneration(state, new Error("tool attachment detached"));
|
|
126
|
+
releaseAdmission();
|
|
127
|
+
if (socket) closeSocket(socket, 1000, "tool attachment detached");
|
|
128
|
+
else resolveClosed();
|
|
129
|
+
return closed;
|
|
130
|
+
}
|
|
131
|
+
if (!state.draining) {
|
|
132
|
+
state.draining = true;
|
|
133
|
+
try { send(socket, { type: "drain" }); }
|
|
134
|
+
catch (error) {
|
|
135
|
+
closeSocket(socket, 1011, closeReason(`tool attachment drain failed: ${errorMessage(error)}`));
|
|
136
|
+
return closed;
|
|
137
|
+
}
|
|
138
|
+
if (state.socket !== socket) return closed;
|
|
139
|
+
state.drainTimer = setTimeout(() => {
|
|
140
|
+
if (state.socket === socket) closeSocket(socket, 1000, "tool attachment drain timed out");
|
|
141
|
+
}, positiveOption(options.drainTimeoutMs, DEFAULT_DRAIN_TIMEOUT_MS, "drainTimeoutMs"));
|
|
142
|
+
}
|
|
143
|
+
maybeFinishDrain(socket);
|
|
144
|
+
return closed;
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
const client = { ready, public: publicClient };
|
|
148
|
+
void connectGeneration();
|
|
149
|
+
return client;
|
|
150
|
+
|
|
151
|
+
async function connectGeneration() {
|
|
152
|
+
if (state.stopped) return;
|
|
153
|
+
try {
|
|
154
|
+
const socket = await openSocket(endpoint, transport);
|
|
155
|
+
if (state.stopped) { socket.close(1000, "attachment stopped"); return; }
|
|
156
|
+
state.socket = socket;
|
|
157
|
+
state.catalogSent = false;
|
|
158
|
+
state.readyReceived = false;
|
|
159
|
+
state.draining = false;
|
|
160
|
+
state.drainAcknowledged = false;
|
|
161
|
+
bindSocket(socket, {
|
|
162
|
+
open() {
|
|
163
|
+
if (state.socket !== socket) return;
|
|
164
|
+
try { publishCatalog(socket); }
|
|
165
|
+
catch (error) { transportFailure(socket, error); }
|
|
166
|
+
},
|
|
167
|
+
message(encoded) {
|
|
168
|
+
if (state.socket !== socket) return;
|
|
169
|
+
if (typeof encoded !== "string") {
|
|
170
|
+
rejectProtocol(socket, "tool attachments require text frames");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
void handleFrame(encoded, socket).catch((error) => {
|
|
174
|
+
if (error instanceof AttachmentTransportError) transportFailure(socket, error);
|
|
175
|
+
else rejectProtocol(socket, errorMessage(error));
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
close(event) { socketClosed(socket, event); },
|
|
179
|
+
error(error) {
|
|
180
|
+
if (state.socket !== socket) return;
|
|
181
|
+
if (!state.readySettled) {
|
|
182
|
+
state.readySettled = true;
|
|
183
|
+
state.stopped = true;
|
|
184
|
+
releaseAdmission();
|
|
185
|
+
rejectReady(error);
|
|
186
|
+
closeSocket(socket, 1011, "tool attachment initial connection failed");
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
state.handshakeTimer = setTimeout(() => {
|
|
191
|
+
if (state.socket !== socket || state.readyReceived) return;
|
|
192
|
+
if (!state.readySettled) {
|
|
193
|
+
state.readySettled = true;
|
|
194
|
+
state.stopped = true;
|
|
195
|
+
rejectReady(new Error("tool attachment handshake timed out before ready"));
|
|
196
|
+
}
|
|
197
|
+
closeSocket(socket, 1012, "tool attachment handshake timed out");
|
|
198
|
+
}, positiveOption(options.handshakeTimeoutMs, DEFAULT_HANDSHAKE_TIMEOUT_MS, "handshakeTimeoutMs"));
|
|
199
|
+
if (socket.readyState === OPEN) queueMicrotask(() => {
|
|
200
|
+
if (state.socket !== socket) return;
|
|
201
|
+
try { publishCatalog(socket); }
|
|
202
|
+
catch (error) { transportFailure(socket, error); }
|
|
203
|
+
});
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (!state.stopped && state.readySettled && options.reconnect !== false) {
|
|
206
|
+
state.reconnectTimer = setTimeout(connectGeneration, options.reconnectDelayMs ?? 250);
|
|
207
|
+
} else if (!state.readySettled) {
|
|
208
|
+
state.readySettled = true;
|
|
209
|
+
releaseAdmission();
|
|
210
|
+
rejectReady(error);
|
|
211
|
+
}
|
|
212
|
+
if (state.stopped || options.reconnect === false) resolveClosed();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function publishCatalog(socket) {
|
|
217
|
+
if (state.catalogSent) return;
|
|
218
|
+
state.catalogSent = true;
|
|
219
|
+
send(socket, {
|
|
220
|
+
type: "catalog",
|
|
221
|
+
tools: state.catalog,
|
|
222
|
+
...(state.machines.length === 0 ? {} : { machines: state.machines }),
|
|
223
|
+
...(state.attachmentId === undefined ? {} : { attachment_id: state.attachmentId }),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function handleFrame(encoded, socket) {
|
|
228
|
+
if (state.socket !== socket) return;
|
|
229
|
+
const frame = parseFrame(encoded);
|
|
230
|
+
switch (frame.type) {
|
|
231
|
+
case "ready":
|
|
232
|
+
if (!state.catalogSent || state.readyReceived) throw new Error("ready received outside the catalog handshake");
|
|
233
|
+
state.readyReceived = true;
|
|
234
|
+
state.connected = true;
|
|
235
|
+
clearTimeout(state.handshakeTimer);
|
|
236
|
+
state.handshakeTimer = undefined;
|
|
237
|
+
startHeartbeat(socket);
|
|
238
|
+
if (!state.readySettled) { state.readySettled = true; resolveReady(publicClient); }
|
|
239
|
+
break;
|
|
240
|
+
case "call":
|
|
241
|
+
if (!state.readyReceived || state.drainAcknowledged) throw new Error("call received outside a routing-ready socket");
|
|
242
|
+
await handleCall(frame, socket);
|
|
243
|
+
break;
|
|
244
|
+
case "cancel":
|
|
245
|
+
if (!state.readyReceived) throw new Error("cancel received outside a routing-ready socket");
|
|
246
|
+
handleCancel(frame, socket);
|
|
247
|
+
break;
|
|
248
|
+
case "ack":
|
|
249
|
+
handleAck(frame, socket);
|
|
250
|
+
break;
|
|
251
|
+
case "pong":
|
|
252
|
+
if (state.pendingNonce === undefined || frame.nonce !== state.pendingNonce) {
|
|
253
|
+
throw new Error("pong nonce did not match the outstanding ping");
|
|
254
|
+
}
|
|
255
|
+
state.pendingNonce = undefined;
|
|
256
|
+
break;
|
|
257
|
+
case "draining":
|
|
258
|
+
if (!state.draining || state.drainAcknowledged) throw new Error("unexpected draining acknowledgement");
|
|
259
|
+
state.drainAcknowledged = true;
|
|
260
|
+
maybeFinishDrain(socket);
|
|
261
|
+
break;
|
|
262
|
+
default:
|
|
263
|
+
throw new Error(`unsupported durable-object frame: ${frame.type}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function handleCall(frame, socket) {
|
|
268
|
+
const callId = frame.call_id;
|
|
269
|
+
const identity = callIdentity(frame);
|
|
270
|
+
const receipt = state.receipts.get(callId);
|
|
271
|
+
if (receipt) {
|
|
272
|
+
if (receipt.identity !== undefined && receipt.identity !== identity) throw new Error("completed call ID was reused with different immutable fields");
|
|
273
|
+
send(socket, receipt.frame);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const active = state.calls.get(callId);
|
|
277
|
+
if (active) {
|
|
278
|
+
if (active.identity !== identity) throw new Error("active call ID was reused with different immutable fields");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (state.calls.size >= 64) {
|
|
282
|
+
retainAndSend(callId, identity, { status: "unavailable", message: "tool attachment has 64 active calls" }, socket);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (state.receipts.size >= 512) throw new Error("tool attachment retained receipt bound is exhausted");
|
|
286
|
+
if (frame.deadline_at <= Date.now()) {
|
|
287
|
+
retainAndSend(callId, identity, { status: "unavailable", message: "tool attachment call deadline elapsed before dispatch" }, socket);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const controller = new AbortController();
|
|
291
|
+
const call = { controller, identity };
|
|
292
|
+
state.calls.set(callId, call);
|
|
293
|
+
let deadline;
|
|
294
|
+
const deadlinePromise = new Promise((resolve) => {
|
|
295
|
+
const arm = () => {
|
|
296
|
+
const remaining = frame.deadline_at - Date.now();
|
|
297
|
+
if (remaining <= 0) {
|
|
298
|
+
controller.abort(new Error("tool attachment call deadline elapsed"));
|
|
299
|
+
resolve({ deadline: true });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
deadline = setTimeout(arm, Math.min(remaining, 2_147_483_647));
|
|
303
|
+
};
|
|
304
|
+
arm();
|
|
305
|
+
});
|
|
306
|
+
let outcome;
|
|
307
|
+
let value;
|
|
308
|
+
try {
|
|
309
|
+
value = await Promise.race([
|
|
310
|
+
admission.invoke(frame.name, frame.input, {
|
|
311
|
+
sessionId: frame.session_id,
|
|
312
|
+
parentCallId: "",
|
|
313
|
+
callId,
|
|
314
|
+
model: frame.model,
|
|
315
|
+
signal: controller.signal,
|
|
316
|
+
}),
|
|
317
|
+
deadlinePromise,
|
|
318
|
+
]);
|
|
319
|
+
if (value?.deadline) outcome = { status: "ambiguous", message: "tool attachment call crossed its admitted deadline after dispatch" };
|
|
320
|
+
} catch (error) {
|
|
321
|
+
outcome = controller.signal.aborted
|
|
322
|
+
? { status: "ambiguous", message: "tool attachment call ended after local deadline or transport cancellation" }
|
|
323
|
+
: { status: "completed", output: failedOutput(error) };
|
|
324
|
+
}
|
|
325
|
+
clearTimeout(deadline);
|
|
326
|
+
if (state.calls.get(callId) !== call) return;
|
|
327
|
+
state.calls.delete(callId);
|
|
328
|
+
if (!outcome) {
|
|
329
|
+
try {
|
|
330
|
+
const output = wireOutput(value);
|
|
331
|
+
outcome = utf8ByteLength(JSON.stringify(output)) > frame.output_byte_budget
|
|
332
|
+
? { status: "ambiguous", message: "tool attachment output exceeded the admitted byte budget after dispatch" }
|
|
333
|
+
: { status: "completed", output };
|
|
334
|
+
} catch {
|
|
335
|
+
outcome = { status: "ambiguous", message: "tool attachment result was not valid bounded wire output after dispatch" };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (state.socket !== socket) return;
|
|
339
|
+
retainAndSend(callId, identity, outcome, socket);
|
|
340
|
+
maybeFinishDrain(socket);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function handleCancel(frame, socket) {
|
|
344
|
+
const callId = frame.call_id;
|
|
345
|
+
const retained = state.receipts.get(callId);
|
|
346
|
+
if (retained) {
|
|
347
|
+
send(socket, retained.frame);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const call = state.calls.get(callId);
|
|
351
|
+
if (call) {
|
|
352
|
+
state.calls.delete(callId);
|
|
353
|
+
call.controller.abort(new Error("tool attachment call was cancelled"));
|
|
354
|
+
}
|
|
355
|
+
if (state.receipts.size >= 512) throw new Error("tool attachment retained receipt bound is exhausted");
|
|
356
|
+
retainAndSend(callId, call?.identity, call
|
|
357
|
+
? { status: "ambiguous", message: "tool execution was cancelled after dispatch" }
|
|
358
|
+
: { status: "cancelled", message: "tool attachment call was cancelled before dispatch" }, socket);
|
|
359
|
+
maybeFinishDrain(socket);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function handleAck(frame, socket) {
|
|
363
|
+
const receipt = state.receipts.get(frame.call_id);
|
|
364
|
+
if (!receipt) throw new Error("ack did not match a retained terminal result");
|
|
365
|
+
state.receipts.delete(frame.call_id);
|
|
366
|
+
maybeFinishDrain(socket);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function retainAndSend(callId, identity, outcome, socket) {
|
|
370
|
+
const result = { type: "result", call_id: callId, outcome };
|
|
371
|
+
state.receipts.set(callId, { identity, frame: result });
|
|
372
|
+
send(socket, result);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function startHeartbeat(socket) {
|
|
376
|
+
clearInterval(state.heartbeat);
|
|
377
|
+
state.heartbeat = setInterval(() => {
|
|
378
|
+
if (state.socket !== socket) return;
|
|
379
|
+
if (state.pendingNonce !== undefined) {
|
|
380
|
+
closeSocket(socket, 1012, "tool attachment heartbeat timed out");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const nonce = randomNonce();
|
|
384
|
+
state.pendingNonce = nonce;
|
|
385
|
+
try { send(socket, { type: "ping", nonce }); }
|
|
386
|
+
catch (error) { transportFailure(socket, error); }
|
|
387
|
+
}, positiveOption(options.heartbeatMs, DEFAULT_HEARTBEAT_MS, "heartbeatMs"));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function maybeFinishDrain(socket) {
|
|
391
|
+
if (!state.stopped || !state.drainAcknowledged) return;
|
|
392
|
+
if (state.calls.size || state.receipts.size) return;
|
|
393
|
+
clearTimeout(state.drainTimer);
|
|
394
|
+
state.drainTimer = undefined;
|
|
395
|
+
releaseAdmission();
|
|
396
|
+
closeSocket(socket, 1000, "tool attachment drained");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function rejectProtocol(socket, reason) {
|
|
400
|
+
if (state.socket !== socket) return;
|
|
401
|
+
state.stopped = true;
|
|
402
|
+
state.connected = false;
|
|
403
|
+
clearTimers(state);
|
|
404
|
+
abortGeneration(state, new Error(reason));
|
|
405
|
+
releaseAdmission();
|
|
406
|
+
closeSocket(socket, 1008, closeReason(reason));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function transportFailure(socket, error) {
|
|
410
|
+
if (state.socket !== socket) return;
|
|
411
|
+
state.connected = false;
|
|
412
|
+
closeSocket(socket, 1011, closeReason(`tool attachment transport failed: ${errorMessage(error)}`));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function closeSocket(socket, code, reason) {
|
|
416
|
+
try { socket.close(code, reason); }
|
|
417
|
+
catch {}
|
|
418
|
+
socketClosed(socket);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function socketClosed(socket, event) {
|
|
422
|
+
if (state.socket !== socket) return;
|
|
423
|
+
const policyRejected = event?.code === 1008;
|
|
424
|
+
if (policyRejected) state.stopped = true;
|
|
425
|
+
state.connected = false;
|
|
426
|
+
state.socket = undefined;
|
|
427
|
+
state.catalogSent = false;
|
|
428
|
+
state.readyReceived = false;
|
|
429
|
+
clearTimers(state);
|
|
430
|
+
abortGeneration(state, new Error("tool attachment disconnected"));
|
|
431
|
+
if (!state.stopped && state.readySettled && options.reconnect !== false) {
|
|
432
|
+
state.reconnectTimer = setTimeout(connectGeneration, options.reconnectDelayMs ?? 250);
|
|
433
|
+
} else if (!state.readySettled) {
|
|
434
|
+
state.readySettled = true;
|
|
435
|
+
releaseAdmission();
|
|
436
|
+
rejectReady(policyRejected
|
|
437
|
+
? new AttachmentRejectedError(event?.reason)
|
|
438
|
+
: new Error("tool attachment closed before ready"));
|
|
439
|
+
} else releaseAdmission();
|
|
440
|
+
if (state.stopped || options.reconnect === false) resolveClosed();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function releaseAdmission() {
|
|
444
|
+
if (state.admissionReleased) return;
|
|
445
|
+
state.admissionReleased = true;
|
|
446
|
+
admission.release();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function wireOutput(value) {
|
|
451
|
+
if (value?.[TOOL_RESULT]) return {
|
|
452
|
+
output: outputBody(value.output), success: value.success,
|
|
453
|
+
structured_result: snapshot(value.structuredResult),
|
|
454
|
+
metadata: value.metadata == null ? null : snapshot(value.metadata), process_trace: null,
|
|
455
|
+
};
|
|
456
|
+
return { output: outputBody(value), success: true, structured_result: snapshot(value), metadata: null, process_trace: null };
|
|
457
|
+
}
|
|
458
|
+
function failedOutput(error) { return { output: errorMessage(error), success: false, structured_result: null, metadata: null, process_trace: null }; }
|
|
459
|
+
function outputBody(value) {
|
|
460
|
+
if (Array.isArray(value) && value.every((item) => ["input_text", "input_image", "input_audio"].includes(item?.type))) return snapshot(value);
|
|
461
|
+
if (typeof value === "string") return value;
|
|
462
|
+
return value === undefined ? "undefined" : JSON.stringify(value);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function openSocket(endpoint, transport) {
|
|
466
|
+
let socket;
|
|
467
|
+
if (transport !== undefined) socket = await transport.connect(endpoint);
|
|
468
|
+
else {
|
|
469
|
+
if (typeof globalThis.window === "object" && !isSameOriginWebSocket(endpoint)) {
|
|
470
|
+
throw new Error("browser tool attachments require a same-origin URL or an injected authenticated transport");
|
|
471
|
+
}
|
|
472
|
+
const WebSocketImpl = globalThis.WebSocket;
|
|
473
|
+
if (typeof WebSocketImpl !== "function") throw new Error("WebSocket is unavailable; inject transport.connect() in this runtime");
|
|
474
|
+
socket = new WebSocketImpl(endpoint);
|
|
475
|
+
}
|
|
476
|
+
if (!socket || typeof socket.send !== "function" || typeof socket.close !== "function") {
|
|
477
|
+
throw new TypeError("attachment transport must return a WebSocket-compatible object");
|
|
478
|
+
}
|
|
479
|
+
return socket;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function validateAttachmentOptions(target, options) {
|
|
483
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) throw new TypeError("tool attachment options must be an object");
|
|
484
|
+
const allowed = new Set(["provider", "reconnect", "reconnectDelayMs", "heartbeatMs", "handshakeTimeoutMs", "drainTimeoutMs", "machines", "attachmentId"]);
|
|
485
|
+
for (const name of Object.keys(options)) if (!allowed.has(name)) throw new TypeError(`unsupported tool attachment option: ${name}`);
|
|
486
|
+
if (options.provider !== undefined && (typeof options.provider !== "string" || !options.provider.trim())) throw new TypeError("tool attachment provider must be a non-empty string");
|
|
487
|
+
if (options.reconnect !== undefined && typeof options.reconnect !== "boolean") throw new TypeError("tool attachment reconnect must be boolean");
|
|
488
|
+
if (options.attachmentId !== undefined && (typeof options.attachmentId !== "string" || !ATTACHMENT_ID.test(options.attachmentId))) {
|
|
489
|
+
throw new TypeError("tool attachment attachmentId must be a safe identifier of at most 123 bytes");
|
|
490
|
+
}
|
|
491
|
+
for (const name of ["reconnectDelayMs", "heartbeatMs", "handshakeTimeoutMs", "drainTimeoutMs"]) {
|
|
492
|
+
if (options[name] !== undefined) positiveInteger(options[name], name);
|
|
493
|
+
}
|
|
494
|
+
if (typeof target === "object" && !(target instanceof URL)) {
|
|
495
|
+
if (!target || Array.isArray(target)) throw new TypeError("invalid tool attachment target");
|
|
496
|
+
for (const name of Object.keys(target)) if (!new Set(["endpoint", "transport"]).has(name)) throw new TypeError(`unsupported tool attachment target field: ${name}`);
|
|
497
|
+
if (target.endpoint === undefined) throw new TypeError("tool attachment target requires endpoint");
|
|
498
|
+
if (!target.transport || typeof target.transport.connect !== "function") throw new TypeError("tool attachment target transport must implement connect(endpoint)");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function isSameOriginWebSocket(endpoint) {
|
|
503
|
+
if (!globalThis.location?.href) return false;
|
|
504
|
+
const websocket = new URL(endpoint); const page = new URL(globalThis.location.href);
|
|
505
|
+
const pageProtocol = page.protocol === "https:" ? "wss:" : page.protocol === "http:" ? "ws:" : "";
|
|
506
|
+
return websocket.protocol === pageProtocol && websocket.host === page.host;
|
|
507
|
+
}
|
|
508
|
+
function bindSocket(socket, handlers) {
|
|
509
|
+
if (typeof socket.addEventListener === "function") {
|
|
510
|
+
socket.addEventListener("open", handlers.open);
|
|
511
|
+
socket.addEventListener("message", (event) => handlers.message(event?.data));
|
|
512
|
+
socket.addEventListener("close", handlers.close);
|
|
513
|
+
socket.addEventListener("error", (event) => handlers.error(event?.error ?? new Error("tool attachment WebSocket failed")));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (typeof socket.on === "function") {
|
|
517
|
+
socket.on("open", handlers.open);
|
|
518
|
+
socket.on("message", (data, isBinary) => handlers.message(isBinary === true ? data : typeof data === "string" ? data : decode(data)));
|
|
519
|
+
socket.on("close", (code, reason) => handlers.close({ code, reason: decode(reason) }));
|
|
520
|
+
socket.on("error", (error) => handlers.error(error ?? new Error("tool attachment WebSocket failed")));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
throw new TypeError("attachment WebSocket must support addEventListener() or on()");
|
|
524
|
+
}
|
|
525
|
+
function send(socket, frame) {
|
|
526
|
+
try { socket.send(JSON.stringify(frame)); }
|
|
527
|
+
catch (error) { throw new AttachmentTransportError(error); }
|
|
528
|
+
}
|
|
529
|
+
function parseFrame(encoded) {
|
|
530
|
+
if (typeof encoded !== "string") throw new TypeError("tool attachments require text frames");
|
|
531
|
+
if (utf8ByteLength(encoded) > 256 * 1024) throw new Error("tool attachment frame exceeds 262144 bytes");
|
|
532
|
+
const frame = JSON.parse(encoded);
|
|
533
|
+
if (!frame || typeof frame !== "object" || Array.isArray(frame)) throw new TypeError("tool attachment frame must be an object");
|
|
534
|
+
const keys = DO_KEYS[frame.type];
|
|
535
|
+
if (!keys) throw new Error(`unsupported durable-object tool attachment frame: ${frame.type}`);
|
|
536
|
+
exactKeys(frame, keys);
|
|
537
|
+
if (frame.type === "call") {
|
|
538
|
+
requiredIdentifier(frame.session_id, "session_id"); requiredIdentifier(frame.call_id, "call_id");
|
|
539
|
+
requiredIdentifier(frame.model, "model"); requiredIdentifier(frame.name, "name");
|
|
540
|
+
if (typeof frame.input !== "string" && (!frame.input || typeof frame.input !== "object" || Array.isArray(frame.input))) throw new Error("call input must be an object or string");
|
|
541
|
+
if (utf8ByteLength(JSON.stringify(frame.input)) > 128 * 1024) throw new Error("call input exceeds 131072 bytes");
|
|
542
|
+
if (positiveInteger(frame.output_token_budget, "output_token_budget") > 1_000_000) throw new Error("output_token_budget exceeds protocol bound");
|
|
543
|
+
if (positiveInteger(frame.output_byte_budget, "output_byte_budget") > 128 * 1024) throw new Error("output_byte_budget exceeds protocol bound");
|
|
544
|
+
positiveInteger(frame.deadline_at, "deadline_at");
|
|
545
|
+
} else if (frame.type === "cancel" || frame.type === "ack") requiredIdentifier(frame.call_id, "call_id");
|
|
546
|
+
else if (frame.type === "pong") {
|
|
547
|
+
if (typeof frame.nonce !== "string" || !frame.nonce || utf8ByteLength(frame.nonce) > 128) throw new Error("invalid pong nonce");
|
|
548
|
+
}
|
|
549
|
+
return frame;
|
|
550
|
+
}
|
|
551
|
+
function clearTimers(state) {
|
|
552
|
+
clearInterval(state.heartbeat); clearTimeout(state.handshakeTimer);
|
|
553
|
+
clearTimeout(state.drainTimer); clearTimeout(state.reconnectTimer);
|
|
554
|
+
state.heartbeat = state.handshakeTimer = state.drainTimer = state.reconnectTimer = undefined;
|
|
555
|
+
state.pendingNonce = undefined;
|
|
556
|
+
}
|
|
557
|
+
function abortGeneration(state, reason) {
|
|
558
|
+
for (const call of state.calls.values()) call.controller.abort(reason);
|
|
559
|
+
state.calls.clear(); state.receipts.clear(); state.pendingNonce = undefined;
|
|
560
|
+
}
|
|
561
|
+
function positiveInteger(value, name) { if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${name} must be a positive safe integer`); return value; }
|
|
562
|
+
function positiveOption(value, fallback, name) { return value === undefined ? fallback : positiveInteger(value, name); }
|
|
563
|
+
function requiredIdentifier(value, name) { if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) throw new TypeError(`${name} must be a safe ASCII identifier`); return value; }
|
|
564
|
+
function exactKeys(value, allowed) { for (const key of Object.keys(value)) if (!allowed.includes(key)) throw new Error(`${value.type} contains unsupported field ${key}`); }
|
|
565
|
+
function snapshot(value) { return value === undefined ? null : JSON.parse(JSON.stringify(value)); }
|
|
566
|
+
function decode(value) { if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return new TextDecoder().decode(value); return String(value); }
|
|
567
|
+
function errorMessage(error) { return error && (error.stack || error.message) || String(error); }
|
|
568
|
+
function callIdentity(frame) { return JSON.stringify([frame.session_id, frame.call_id, frame.model, frame.name, frame.input, frame.output_token_budget, frame.output_byte_budget, frame.deadline_at]); }
|
|
569
|
+
function attachmentEndpoint(target) {
|
|
570
|
+
const raw = typeof target === "object" && !(target instanceof URL) ? target.endpoint : target;
|
|
571
|
+
let url; try { url = new URL(raw); } catch { throw new TypeError("tool attachment target must be a valid WebSocket URL"); }
|
|
572
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new TypeError("tool attachment target must use ws: or wss:");
|
|
573
|
+
if (url.username || url.password) throw new TypeError("tool attachment target must not contain credentials");
|
|
574
|
+
if (url.hash) throw new TypeError("tool attachment target must not contain a fragment");
|
|
575
|
+
if (url.protocol === "ws:" && !isLoopback(url.hostname)) throw new TypeError("plaintext ws: tool attachments are limited to loopback hosts");
|
|
576
|
+
return url.href;
|
|
577
|
+
}
|
|
578
|
+
function attachmentTransport(target) { return typeof target === "object" && !(target instanceof URL) ? target.transport : undefined; }
|
|
579
|
+
function isLoopback(hostname) {
|
|
580
|
+
return hostname === "localhost"
|
|
581
|
+
|| hostname === "[::1]"
|
|
582
|
+
|| /^127(?:\.[0-9]{1,3}){3}$/.test(hostname)
|
|
583
|
+
|| hostname === "nanocodex.localhost"
|
|
584
|
+
|| /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.nanocodex\.localhost$/.test(hostname);
|
|
585
|
+
}
|
|
586
|
+
function closeReason(reason) {
|
|
587
|
+
if (utf8ByteLength(reason) <= 123) return reason;
|
|
588
|
+
let bounded = ""; for (const scalar of reason) { if (utf8ByteLength(bounded + scalar) > 123) break; bounded += scalar; }
|
|
589
|
+
return bounded;
|
|
590
|
+
}
|
|
591
|
+
function randomNonce() {
|
|
592
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
593
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const DO_KEYS = Object.freeze({
|
|
597
|
+
ready: ["type"],
|
|
598
|
+
call: ["type", "session_id", "call_id", "model", "name", "input", "output_token_budget", "output_byte_budget", "deadline_at"],
|
|
599
|
+
cancel: ["type", "call_id"],
|
|
600
|
+
ack: ["type", "call_id"],
|
|
601
|
+
pong: ["type", "nonce"],
|
|
602
|
+
draining: ["type"],
|
|
603
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { NamedTool, Workspace } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
export type JustBashFetch = (
|
|
4
|
+
url: string,
|
|
5
|
+
options?: Readonly<{
|
|
6
|
+
method?: string | undefined;
|
|
7
|
+
headers?: Headers | Record<string, string> | undefined;
|
|
8
|
+
body?: string | undefined;
|
|
9
|
+
followRedirects?: boolean | undefined;
|
|
10
|
+
timeoutMs?: number | undefined;
|
|
11
|
+
maxRedirects?: number | undefined;
|
|
12
|
+
signal?: AbortSignal | undefined;
|
|
13
|
+
}>,
|
|
14
|
+
) => Promise<Readonly<{
|
|
15
|
+
status: number;
|
|
16
|
+
statusText: string;
|
|
17
|
+
headers: Record<string, string>;
|
|
18
|
+
body: Uint8Array;
|
|
19
|
+
url: string;
|
|
20
|
+
}>>;
|
|
21
|
+
|
|
22
|
+
export type JustBashCustomCommand = Readonly<{
|
|
23
|
+
name: string;
|
|
24
|
+
trusted?: boolean | undefined;
|
|
25
|
+
execute(
|
|
26
|
+
args: string[],
|
|
27
|
+
context: unknown,
|
|
28
|
+
): Promise<Readonly<{ stdout: string; stderr: string; exitCode: number }>>;
|
|
29
|
+
}> | Readonly<{
|
|
30
|
+
name: string;
|
|
31
|
+
load(): Promise<JustBashCustomCommand>;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
export type JustBashNetworkOptions = Readonly<{
|
|
35
|
+
/** Explicitly permit credential-free HTTP(S) requests to arbitrary origins. */
|
|
36
|
+
dangerouslyAllowFullInternetAccess?: boolean | undefined;
|
|
37
|
+
/** Otherwise, allow only these exact origin/path prefixes. */
|
|
38
|
+
allowedUrlPrefixes?: readonly string[] | undefined;
|
|
39
|
+
/** Defaults to GET and HEAD in Just Bash. */
|
|
40
|
+
allowedMethods?: readonly string[] | undefined;
|
|
41
|
+
}>;
|
|
42
|
+
|
|
43
|
+
export type JustBashRuntime = Readonly<{
|
|
44
|
+
/**
|
|
45
|
+
* The authoritative workspace handle while this runtime is mounted. All mutations must use
|
|
46
|
+
* this handle so Bash's bounded metadata view remains synchronized.
|
|
47
|
+
*/
|
|
48
|
+
filesystem: Workspace;
|
|
49
|
+
/** Model instructions derived from the actual shell descriptor. */
|
|
50
|
+
instructions: string;
|
|
51
|
+
/** Exact command, workspace, limit, network, and escalation boundary. */
|
|
52
|
+
descriptor: JustBashDescriptor;
|
|
53
|
+
/** One-shot, cancellable `exec_command` tool backed by Just Bash. */
|
|
54
|
+
tool: NamedTool;
|
|
55
|
+
}>;
|
|
56
|
+
|
|
57
|
+
export type JustBashDescriptor = Readonly<{
|
|
58
|
+
shell: "nanocodex-just-bash";
|
|
59
|
+
commands: readonly string[];
|
|
60
|
+
customCommands: readonly string[];
|
|
61
|
+
cwd: string;
|
|
62
|
+
limits: Readonly<Record<string, number>>;
|
|
63
|
+
network: Readonly<{ enabled: boolean; mode: string }>;
|
|
64
|
+
pty: false;
|
|
65
|
+
sessions: false;
|
|
66
|
+
sandboxEscalation: false;
|
|
67
|
+
}>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Mounts a caller-owned workspace into an in-isolate Just Bash runtime.
|
|
71
|
+
*
|
|
72
|
+
* Do not mutate the source `filesystem` handle while the runtime is mounted. Use the returned
|
|
73
|
+
* `filesystem` for every mutation so the shell's metadata view remains authoritative.
|
|
74
|
+
*/
|
|
75
|
+
export function justBash(options: {
|
|
76
|
+
/** Caller-owned durable workspace. See `JustBashRuntime.filesystem` for mutation ownership. */
|
|
77
|
+
filesystem: Workspace;
|
|
78
|
+
/** Refresh metadata before each serialized command when another writer shares the storage. */
|
|
79
|
+
refreshFilesystemBeforeExec?: boolean | undefined;
|
|
80
|
+
executionTimeoutMs?: number | undefined;
|
|
81
|
+
maxEntries?: number | undefined;
|
|
82
|
+
maxOutputTokens?: number | undefined;
|
|
83
|
+
network?: false | JustBashNetworkOptions | undefined;
|
|
84
|
+
/** Stable host-owned name for the actual network boundary exposed in the descriptor. */
|
|
85
|
+
networkMode?: string | undefined;
|
|
86
|
+
/** Host-owned fetch boundary used by curl and app-owned commands. */
|
|
87
|
+
fetch?: JustBashFetch | undefined;
|
|
88
|
+
/** Application commands registered directly in the embedded interpreter. */
|
|
89
|
+
customCommands?: readonly JustBashCustomCommand[] | undefined;
|
|
90
|
+
}): Promise<JustBashRuntime>;
|