@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.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/.dsh-plugin/bot-product.mjs +81 -5
- package/.dsh-plugin/client.js +17490 -1791
- package/README.i18n.yaml +4 -4
- package/README.md +409 -364
- package/README.zh.md +409 -364
- package/apps/integration-client/README.md +31 -7
- package/apps/integration-client/api.mjs +14 -3
- package/apps/integration-client/binding-editor.jsx +308 -109
- package/apps/integration-client/binding-model.mjs +262 -0
- package/apps/integration-client/binding.css +144 -0
- package/apps/integration-client/bot-model-status.mjs +18 -0
- package/apps/integration-client/bot-profile-identity.jsx +197 -0
- package/apps/integration-client/bot-profile.css +57 -0
- package/apps/integration-client/bot-profile.mjs +97 -0
- package/apps/integration-client/bot-settings-dialog.css +18 -0
- package/apps/integration-client/bot-settings-dialog.jsx +111 -0
- package/apps/integration-client/bot-thread-header-bridge.css +20 -0
- package/apps/integration-client/bot-thread-header.jsx +278 -0
- package/apps/integration-client/bot-threads-panel.jsx +378 -0
- package/apps/integration-client/bot-threads.css +169 -0
- package/apps/integration-client/chat-design.mjs +250 -0
- package/apps/integration-client/chat-ui.jsx +175 -0
- package/apps/integration-client/chat.css +477 -0
- package/apps/integration-client/composer-draft-binding.mjs +50 -0
- package/apps/integration-client/composer-draft-bound.jsx +14 -0
- package/apps/integration-client/composer-draft-host.jsx +142 -0
- package/apps/integration-client/composer-draft-session.jsx +287 -0
- package/apps/integration-client/composer-draft-witness.mjs +115 -0
- package/apps/integration-client/composer-draft.css +4 -0
- package/apps/integration-client/composer-draft.mjs +322 -0
- package/apps/integration-client/contact-attention.jsx +54 -0
- package/apps/integration-client/data-panel.jsx +439 -0
- package/apps/integration-client/data.css +69 -0
- package/apps/integration-client/desktop-bridge.mjs +1 -0
- package/apps/integration-client/desktop-http-socket.mjs +49 -0
- package/apps/integration-client/desktop-keyboard.mjs +32 -0
- package/apps/integration-client/desktop-pane.jsx +99 -30
- package/apps/integration-client/desktop-stream.mjs +13 -3
- package/apps/integration-client/desktop-tools.jsx +19 -0
- package/apps/integration-client/desktop-touch.mjs +27 -0
- package/apps/integration-client/desktop.css +41 -0
- package/apps/integration-client/independent-chat-controller.mjs +224 -0
- package/apps/integration-client/independent-chat-entry.jsx +61 -0
- package/apps/integration-client/independent-chat-navigation.mjs +21 -0
- package/apps/integration-client/independent-chat-page.jsx +148 -0
- package/apps/integration-client/independent-chat-rich.jsx +64 -0
- package/apps/integration-client/independent-chat-rich.mjs +30 -0
- package/apps/integration-client/independent-chat.css +73 -0
- package/apps/integration-client/index.jsx +24 -16
- package/apps/integration-client/library-panel.jsx +4 -1
- package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
- package/apps/integration-client/main-route-editor.jsx +94 -0
- package/apps/integration-client/main-route-model.mjs +100 -0
- package/apps/integration-client/model-picker.css +37 -0
- package/apps/integration-client/model-picker.jsx +83 -0
- package/apps/integration-client/native-activity.css +141 -0
- package/apps/integration-client/native-activity.jsx +276 -0
- package/apps/integration-client/native-activity.mjs +384 -0
- package/apps/integration-client/notifications-panel.jsx +186 -0
- package/apps/integration-client/notifications.css +126 -0
- package/apps/integration-client/pairing-qr.jsx +79 -0
- package/apps/integration-client/panels.jsx +126 -115
- package/apps/integration-client/product-slots.jsx +46 -0
- package/apps/integration-client/routine-recorder.mjs +23 -2
- package/apps/integration-client/secure-pairing.jsx +84 -0
- package/apps/integration-client/tasks-panel.jsx +32 -10
- package/apps/integration-client/ui-icons.jsx +17 -0
- package/apps/integration-client/usage-explorer.jsx +43 -0
- package/apps/integration-client/usage-report.mjs +71 -0
- package/apps/integration-client/usage.css +34 -0
- package/apps/integration-client/web-panel-model.mjs +140 -0
- package/apps/integration-client/web-panel-page.jsx +221 -0
- package/apps/integration-client/web-panel-pins-live.jsx +28 -0
- package/apps/integration-client/web-panel.css +139 -0
- package/apps/integration-client/workspace-model.mjs +67 -0
- package/apps/integration-client/workspace-navigation.mjs +56 -0
- package/apps/integration-client/workspace-ui.jsx +173 -0
- package/apps/integration-client/workspace.css +332 -0
- package/modules/bot-runtime/SOURCE.json +176 -50
- package/modules/bot-runtime/index.mjs +169 -13
- package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
- package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
- package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
- package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
- package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
- package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
- package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
- package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
- package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
- package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
- package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
- package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
- package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
- package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
- package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
- package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
- package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
- package/modules/bot-runtime/read-model.mjs +19 -4
- package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
- package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
- package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
- package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
- package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
- package/modules/independent-runtime/index.mjs +152 -62
- package/modules/independent-runtime/remote-tools.mjs +8 -5
- package/modules/openmaus-protocol/NOTICE +10 -0
- package/modules/openmaus-protocol/README.md +15 -0
- package/modules/openmaus-protocol/SOURCE.json +68 -0
- package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
- package/modules/openmaus-protocol/index.mjs +1 -0
- package/modules/openmaus-protocol/transport.mjs +77 -0
- package/modules/openmaus-protocol/vendor/LICENSE +202 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
- package/package.json +20 -6
- package/packages/bot-acp/README.md +156 -0
- package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-acp/licenses/NOTICE.md +27 -0
- package/packages/bot-acp/src/acp-adapter.mjs +795 -0
- package/packages/bot-acp/src/acp-host.mjs +148 -0
- package/packages/bot-acp/src/native-launch.mjs +121 -0
- package/packages/bot-acp/src/permission-policy.mjs +283 -0
- package/packages/bot-acp/src/rpc-codec.mjs +265 -0
- package/packages/bot-acp/src/session-core.mjs +581 -0
- package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
- package/packages/bot-engines/README.md +281 -0
- package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-engines/licenses/NOTICE.md +32 -0
- package/packages/bot-engines/src/claude-engine.mjs +772 -0
- package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
- package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
- package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
- package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
- package/packages/bot-engines/src/claude-protocol.mjs +177 -0
- package/packages/bot-engines/src/driver-catalog.mjs +334 -0
- package/packages/bot-engines/src/host-loop-request.mjs +95 -0
- package/packages/bot-engines/src/host-registrations.mjs +105 -0
- package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
- package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
- package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
- package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
- package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
- package/packages/cli-runtime/src/native-peer.ps1 +51 -3
- package/packages/cli-runtime/src/native-process.mjs +77 -14
- package/packages/model-router-plugin/README.md +3 -3
- package/packages/model-router-plugin/package.json +1 -1
- package/packages/orchestration/src/binding-product.mjs +19 -0
- package/packages/orchestration/src/bot-data-actions.mjs +45 -0
- package/packages/orchestration/src/bot-data-chart.mjs +109 -0
- package/packages/orchestration/src/bot-data-context.mjs +44 -0
- package/packages/orchestration/src/bot-data-engine.mjs +809 -0
- package/packages/orchestration/src/bot-data-io.mjs +186 -0
- package/packages/orchestration/src/bot-data-sql.mjs +195 -0
- package/packages/orchestration/src/bot-data-surface.mjs +144 -0
- package/packages/orchestration/src/bot-data-tools.mjs +68 -0
- package/packages/orchestration/src/bot-data-worker.mjs +228 -0
- package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
- package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
- package/packages/orchestration/src/bot-loop.mjs +152 -19
- package/packages/orchestration/src/bot-notifications.mjs +159 -0
- package/packages/orchestration/src/bot-policy.mjs +43 -4
- package/packages/orchestration/src/bot-profile.mjs +171 -0
- package/packages/orchestration/src/bot-routine.mjs +4 -4
- package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
- package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
- package/packages/orchestration/src/bot-serving.mjs +164 -0
- package/packages/orchestration/src/bot-session-store.mjs +105 -9
- package/packages/orchestration/src/bot-settings.mjs +50 -11
- package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
- package/packages/orchestration/src/bot-thread-host.mjs +187 -0
- package/packages/orchestration/src/bot-threads.mjs +790 -0
- package/packages/orchestration/src/data-product-access.mjs +37 -0
- package/packages/orchestration/src/data-tool-contract.mjs +291 -0
- package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
- package/packages/orchestration/src/desktop-proxy.mjs +311 -0
- package/packages/orchestration/src/desktop-sessions.mjs +43 -9
- package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
- package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
- package/packages/orchestration/src/independent-chat-page.mjs +39 -0
- package/packages/orchestration/src/independent-chat-service.mjs +308 -0
- package/packages/orchestration/src/independent-context-state.mjs +44 -0
- package/packages/orchestration/src/independent-message-wire.mjs +84 -0
- package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
- package/packages/orchestration/src/independent-tool-families.mjs +73 -0
- package/packages/orchestration/src/independent-tool-host.mjs +344 -0
- package/packages/orchestration/src/main-bot-grant.mjs +15 -0
- package/packages/orchestration/src/main-model-routes.mjs +208 -0
- package/packages/orchestration/src/model-accounting.mjs +99 -35
- package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
- package/packages/orchestration/src/model-loop-frame.mjs +179 -0
- package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
- package/packages/orchestration/src/model-request-bounds.mjs +111 -0
- package/packages/orchestration/src/native-activity-contract.mjs +491 -0
- package/packages/orchestration/src/native-activity-service.mjs +102 -0
- package/packages/orchestration/src/native-activity-store.mjs +249 -0
- package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
- package/packages/orchestration/src/native-data-tools.mjs +100 -0
- package/packages/orchestration/src/native-remote-actions.mjs +742 -0
- package/packages/orchestration/src/native-team-tools.mjs +73 -0
- package/packages/orchestration/src/pairing-invite.mjs +37 -0
- package/packages/orchestration/src/product-composition.mjs +1559 -1001
- package/packages/orchestration/src/remote-machines.mjs +82 -14
- package/packages/orchestration/src/routine-execution.mjs +5 -3
- package/packages/orchestration/src/routine-session.mjs +19 -13
- package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
- package/packages/orchestration/src/task-journal.mjs +29 -0
- package/packages/orchestration/src/task-product.mjs +17 -0
- package/packages/orchestration/src/team-assignment-store.mjs +120 -0
- package/packages/orchestration/src/team-bridge.mjs +282 -0
- package/packages/orchestration/src/team-scheduler.mjs +83 -0
- package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
- package/packages/orchestration/src/team-tool-contract.mjs +284 -0
- package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
- package/packages/orchestration/src/web-panel-actions.mjs +103 -0
- package/packages/orchestration/src/web-panel-framing.mjs +227 -0
- package/packages/orchestration/src/web-panel-store.mjs +206 -0
- package/remote-computer-agent/README.md +39 -29
- package/remote-computer-agent/build.mjs +3 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
- package/remote-computer-agent/dist/cli.mjs +1688 -168
- package/remote-computer-agent/dist/cli.mjs.map +4 -4
- package/remote-computer-agent/dist/index.mjs +1579 -122
- package/remote-computer-agent/dist/index.mjs.map +4 -4
- package/remote-computer-agent/dist/native-peer.ps1 +51 -3
- package/remote-computer-agent/package-lock.json +308 -2
- package/remote-computer-agent/package.json +2 -1
- package/remote-computer-agent/pair-device.ps1 +20 -0
- package/remote-computer-agent/setup.ps1 +38 -16
- package/remote-computer-agent/src/cli.ts +24 -3
- package/remote-computer-agent/src/desktop-session.ts +11 -3
- package/remote-computer-agent/src/e2e-server.ts +81 -0
- package/remote-computer-agent/src/pairing-page.ts +35 -0
- package/remote-computer-agent/src/protocol.ts +25 -25
- package/remote-computer-agent/src/secure-pairing.ts +105 -0
- package/remote-computer-agent/src/store.ts +20 -5
- package/remote-computer-agent/src/transport.ts +39 -15
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFile as
|
|
5
|
-
import { resolve as
|
|
4
|
+
import { readFile as readFile10, writeFile as writeFile7, mkdir as mkdir8 } from "node:fs/promises";
|
|
5
|
+
import { resolve as resolve5, dirname as dirname4, join as join9 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/agent.ts
|
|
8
|
-
import { readFile as
|
|
9
|
-
import { join as join8, dirname, resolve as resolve3 } from "node:path";
|
|
10
|
-
import { randomUUID as
|
|
8
|
+
import { readFile as readFile9, writeFile as writeFile5, mkdir as mkdir6, rename, unlink as unlink5, stat as stat7, realpath as realpath7 } from "node:fs/promises";
|
|
9
|
+
import { join as join8, dirname as dirname2, resolve as resolve3 } from "node:path";
|
|
10
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
11
11
|
import { spawn as spawn4, execFile as execFile2 } from "node:child_process";
|
|
12
12
|
import { promisify as promisify2 } from "node:util";
|
|
13
13
|
|
|
14
14
|
// src/protocol.ts
|
|
15
15
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
16
|
-
var AGENT_VERSION = "0.1.
|
|
16
|
+
var AGENT_VERSION = "0.1.2";
|
|
17
17
|
var AGENT_API_MAJOR = 1;
|
|
18
18
|
var AgentError = class extends Error {
|
|
19
19
|
constructor(code, status = 400) {
|
|
@@ -86,7 +86,1042 @@ var isMutating = (name) => CATALOG.find((x) => x.name === name)?.mutating ?? fal
|
|
|
86
86
|
import { mkdirSync, openSync, readFileSync, writeFileSync, closeSync, unlinkSync, chmodSync, fstatSync, lstatSync } from "node:fs";
|
|
87
87
|
import { join } from "node:path";
|
|
88
88
|
import { createRequire } from "node:module";
|
|
89
|
-
import { randomUUID } from "node:crypto";
|
|
89
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
90
|
+
|
|
91
|
+
// src/secure-pairing.ts
|
|
92
|
+
import { randomInt, randomUUID, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
93
|
+
|
|
94
|
+
// ../modules/openmaus-protocol/dist/protocol.mjs
|
|
95
|
+
import { createCipheriv, createDecipheriv, createECDH, hkdfSync, randomBytes as randomBytes2 } from "node:crypto";
|
|
96
|
+
import { createHash as createHash2, createHmac, createPrivateKey, createPublicKey, sign, timingSafeEqual as timingSafeEqual2, verify } from "node:crypto";
|
|
97
|
+
import { Duplex } from "node:stream";
|
|
98
|
+
var B64U = /^[A-Za-z0-9_-]*$/;
|
|
99
|
+
var b64u = (bytes) => Buffer.from(bytes).toString("base64url");
|
|
100
|
+
function fromB64u(text) {
|
|
101
|
+
if (!B64U.test(text) || text.length % 4 === 1) return null;
|
|
102
|
+
const bytes = Buffer.from(text, "base64url");
|
|
103
|
+
return bytes.toString("base64url") === text ? bytes : null;
|
|
104
|
+
}
|
|
105
|
+
var sha256 = (bytes) => createHash2("sha256").update(bytes).digest();
|
|
106
|
+
var hmacSha256 = (key2, message) => createHmac("sha256", key2).update(message).digest();
|
|
107
|
+
var KEY_ID_BYTES = 16;
|
|
108
|
+
var PSK_LABEL = Buffer.from("omb-e2e psk v1", "utf8");
|
|
109
|
+
var ADMISSION_LABEL = Buffer.from("omb-relay admission v1", "utf8");
|
|
110
|
+
function credentialHashFromHex(hex) {
|
|
111
|
+
return /^[0-9a-f]{64}$/.test(hex) ? Buffer.from(hex, "hex") : null;
|
|
112
|
+
}
|
|
113
|
+
var keyId = (transportPublicKey) => sha256(transportPublicKey).subarray(0, KEY_ID_BYTES);
|
|
114
|
+
var psk = (credHash, transportKeyId) => hmacSha256(credHash, Buffer.concat([PSK_LABEL, transportKeyId]));
|
|
115
|
+
var RELAY_TOKEN_TTL_S = 12 * 60 * 60;
|
|
116
|
+
var PROTOCOL_NAME = "Noise_NKpsk0_P256_AESGCM_SHA256";
|
|
117
|
+
var PUBLIC_KEY_BYTES = 65;
|
|
118
|
+
var TAG_BYTES = 16;
|
|
119
|
+
var MAX_RECORD_BYTES = 65535;
|
|
120
|
+
var MAX_RECORD_PLAINTEXT = MAX_RECORD_BYTES - TAG_BYTES;
|
|
121
|
+
var MAX_HANDSHAKE_PAYLOAD = 512;
|
|
122
|
+
var MAX_HANDSHAKE_MESSAGE = PUBLIC_KEY_BYTES + MAX_HANDSHAKE_PAYLOAD + TAG_BYTES;
|
|
123
|
+
var CONN_BYTES = 16;
|
|
124
|
+
var MAX_APP_CHARS = 40;
|
|
125
|
+
var NONCE_LIMIT = 2 ** 32;
|
|
126
|
+
var HANDSHAKE_DEADLINE_MS = 1e4;
|
|
127
|
+
var TRANSPORT_DIRECT = 2;
|
|
128
|
+
var CLOSE = {
|
|
129
|
+
normal: 1e3,
|
|
130
|
+
goingAway: 1001,
|
|
131
|
+
protocol: 4e3,
|
|
132
|
+
rejected: 4001,
|
|
133
|
+
deadline: 4002
|
|
134
|
+
};
|
|
135
|
+
var NoiseError = class extends Error {
|
|
136
|
+
reason;
|
|
137
|
+
constructor(reason) {
|
|
138
|
+
super(`e2e: ${reason}`);
|
|
139
|
+
this.reason = reason;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
function prologue(transport) {
|
|
143
|
+
return Buffer.concat([Buffer.from("omb-e2e/1", "utf8"), Buffer.from([0, transport])]);
|
|
144
|
+
}
|
|
145
|
+
var ecdh = (privateKey) => {
|
|
146
|
+
const curve = createECDH("prime256v1");
|
|
147
|
+
if (privateKey) curve.setPrivateKey(privateKey);
|
|
148
|
+
else curve.generateKeys();
|
|
149
|
+
return curve;
|
|
150
|
+
};
|
|
151
|
+
function keyPairFromPrivate(privateKey) {
|
|
152
|
+
if (privateKey.length !== 32) throw new NoiseError("bad_private_key");
|
|
153
|
+
return { privateKey: Buffer.from(privateKey), publicKey: ecdh(privateKey).getPublicKey(null, "uncompressed") };
|
|
154
|
+
}
|
|
155
|
+
function generateKeyPair() {
|
|
156
|
+
const curve = ecdh();
|
|
157
|
+
const raw = curve.getPrivateKey();
|
|
158
|
+
return { privateKey: Buffer.concat([Buffer.alloc(32 - raw.length), raw]), publicKey: curve.getPublicKey(null, "uncompressed") };
|
|
159
|
+
}
|
|
160
|
+
function dh(privateKey, publicKey) {
|
|
161
|
+
if (publicKey.length !== PUBLIC_KEY_BYTES || publicKey[0] !== 4) throw new NoiseError("bad_point");
|
|
162
|
+
try {
|
|
163
|
+
return ecdh(privateKey).computeSecret(publicKey);
|
|
164
|
+
} catch {
|
|
165
|
+
throw new NoiseError("bad_point");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
var CHECK_SCALAR = Buffer.alloc(32, 1);
|
|
169
|
+
function isPublicKey(bytes) {
|
|
170
|
+
try {
|
|
171
|
+
dh(CHECK_SCALAR, bytes);
|
|
172
|
+
return true;
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function hkdf(chainingKey, ikm, outputs) {
|
|
178
|
+
const out = Buffer.from(hkdfSync("sha256", ikm, chainingKey, Buffer.alloc(0), 32 * outputs));
|
|
179
|
+
return Array.from({ length: outputs }, (_, index) => out.subarray(index * 32, index * 32 + 32));
|
|
180
|
+
}
|
|
181
|
+
function nonceBytes(n) {
|
|
182
|
+
const nonce = Buffer.alloc(12);
|
|
183
|
+
nonce.writeBigUInt64BE(BigInt(n), 4);
|
|
184
|
+
return nonce;
|
|
185
|
+
}
|
|
186
|
+
function aeadSeal(key2, n, ad, plaintext) {
|
|
187
|
+
const cipher = createCipheriv("aes-256-gcm", key2, nonceBytes(n), { authTagLength: TAG_BYTES });
|
|
188
|
+
cipher.setAAD(ad);
|
|
189
|
+
return Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
|
|
190
|
+
}
|
|
191
|
+
function aeadOpen(key2, n, ad, ciphertext) {
|
|
192
|
+
if (ciphertext.length < TAG_BYTES) throw new NoiseError("short_ciphertext");
|
|
193
|
+
const decipher = createDecipheriv("aes-256-gcm", key2, nonceBytes(n), { authTagLength: TAG_BYTES });
|
|
194
|
+
decipher.setAAD(ad);
|
|
195
|
+
decipher.setAuthTag(ciphertext.subarray(ciphertext.length - TAG_BYTES));
|
|
196
|
+
try {
|
|
197
|
+
return Buffer.concat([decipher.update(ciphertext.subarray(0, ciphertext.length - TAG_BYTES)), decipher.final()]);
|
|
198
|
+
} catch {
|
|
199
|
+
throw new NoiseError("aead");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
var CipherState = class {
|
|
203
|
+
#key;
|
|
204
|
+
#n;
|
|
205
|
+
/** `n` other than 0 is for tests of the counter limit. */
|
|
206
|
+
constructor(key2 = null, n = 0) {
|
|
207
|
+
this.#key = key2;
|
|
208
|
+
this.#n = n;
|
|
209
|
+
}
|
|
210
|
+
get hasKey() {
|
|
211
|
+
return this.#key !== null;
|
|
212
|
+
}
|
|
213
|
+
get nonce() {
|
|
214
|
+
return this.#n;
|
|
215
|
+
}
|
|
216
|
+
/** Test-only view of the key, for the vectors. */
|
|
217
|
+
get keyBytes() {
|
|
218
|
+
return this.#key;
|
|
219
|
+
}
|
|
220
|
+
encryptWithAd(ad, plaintext) {
|
|
221
|
+
if (!this.#key) return Buffer.from(plaintext);
|
|
222
|
+
if (this.#n >= NONCE_LIMIT) throw new NoiseError("nonce_exhausted");
|
|
223
|
+
const out = aeadSeal(this.#key, this.#n, ad, plaintext);
|
|
224
|
+
this.#n += 1;
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
/** The counter moves only on success; any failure ends the session anyway. */
|
|
228
|
+
decryptWithAd(ad, ciphertext) {
|
|
229
|
+
if (!this.#key) return Buffer.from(ciphertext);
|
|
230
|
+
if (this.#n >= NONCE_LIMIT) throw new NoiseError("nonce_exhausted");
|
|
231
|
+
const out = aeadOpen(this.#key, this.#n, ad, ciphertext);
|
|
232
|
+
this.#n += 1;
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
var SymmetricState = class _SymmetricState {
|
|
237
|
+
ck;
|
|
238
|
+
h;
|
|
239
|
+
cipher = new CipherState();
|
|
240
|
+
trace;
|
|
241
|
+
constructor(h, ck, trace) {
|
|
242
|
+
this.h = h;
|
|
243
|
+
this.ck = ck;
|
|
244
|
+
this.trace = trace;
|
|
245
|
+
}
|
|
246
|
+
static initialize(trace = null) {
|
|
247
|
+
const name = Buffer.from(PROTOCOL_NAME, "utf8");
|
|
248
|
+
const h = name.length <= 32 ? Buffer.concat([name, Buffer.alloc(32 - name.length)]) : sha256(name);
|
|
249
|
+
return new _SymmetricState(h, Buffer.from(h), trace);
|
|
250
|
+
}
|
|
251
|
+
clone() {
|
|
252
|
+
const copy = new _SymmetricState(Buffer.from(this.h), Buffer.from(this.ck), this.trace ? [...this.trace] : null);
|
|
253
|
+
copy.cipher = this.cipher;
|
|
254
|
+
return copy;
|
|
255
|
+
}
|
|
256
|
+
mark(step) {
|
|
257
|
+
this.trace?.push({ step, h: this.h.toString("hex"), ck: this.ck.toString("hex"), k: this.cipher.keyBytes?.toString("hex") ?? null });
|
|
258
|
+
}
|
|
259
|
+
mixHash(data) {
|
|
260
|
+
this.h = sha256(Buffer.concat([this.h, data]));
|
|
261
|
+
}
|
|
262
|
+
mixKey(ikm) {
|
|
263
|
+
const [ck, key2] = hkdf(this.ck, ikm, 2);
|
|
264
|
+
this.ck = ck;
|
|
265
|
+
this.cipher = new CipherState(key2);
|
|
266
|
+
}
|
|
267
|
+
mixKeyAndHash(ikm) {
|
|
268
|
+
const [ck, tempH, key2] = hkdf(this.ck, ikm, 3);
|
|
269
|
+
this.ck = ck;
|
|
270
|
+
this.mixHash(tempH);
|
|
271
|
+
this.cipher = new CipherState(key2);
|
|
272
|
+
}
|
|
273
|
+
encryptAndHash(plaintext) {
|
|
274
|
+
const out = this.cipher.encryptWithAd(this.h, plaintext);
|
|
275
|
+
this.mixHash(out);
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
decryptAndHash(ciphertext) {
|
|
279
|
+
const out = this.cipher.decryptWithAd(this.h, ciphertext);
|
|
280
|
+
this.mixHash(ciphertext);
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
/** k_pc then k_cp. */
|
|
284
|
+
split() {
|
|
285
|
+
const [first, second] = hkdf(this.ck, Buffer.alloc(0), 2);
|
|
286
|
+
return [new CipherState(first), new CipherState(second)];
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
function initialized(transport, responderStatic, trace) {
|
|
290
|
+
const state = SymmetricState.initialize(trace);
|
|
291
|
+
state.mark("init");
|
|
292
|
+
state.mixHash(prologue(transport));
|
|
293
|
+
state.mark("prologue");
|
|
294
|
+
state.mixHash(responderStatic);
|
|
295
|
+
state.mark("s");
|
|
296
|
+
return state;
|
|
297
|
+
}
|
|
298
|
+
var RecordCipher = class {
|
|
299
|
+
#state;
|
|
300
|
+
constructor(state) {
|
|
301
|
+
this.#state = state;
|
|
302
|
+
}
|
|
303
|
+
get nonce() {
|
|
304
|
+
return this.#state.nonce;
|
|
305
|
+
}
|
|
306
|
+
seal(plaintext) {
|
|
307
|
+
if (plaintext.length > MAX_RECORD_PLAINTEXT) throw new NoiseError("record_too_large");
|
|
308
|
+
return this.#state.encryptWithAd(Buffer.alloc(0), plaintext);
|
|
309
|
+
}
|
|
310
|
+
open(record) {
|
|
311
|
+
if (record.length > MAX_RECORD_BYTES) throw new NoiseError("record_too_large");
|
|
312
|
+
return this.#state.decryptWithAd(Buffer.alloc(0), record);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
function sessionKeys(state, side) {
|
|
316
|
+
const [pc, cp] = state.split();
|
|
317
|
+
const kPc = pc.keyBytes;
|
|
318
|
+
const kCp = cp.keyBytes;
|
|
319
|
+
return side === "phone" ? { send: new RecordCipher(pc), receive: new RecordCipher(cp), handshakeHash: state.h, kPc, kCp } : { send: new RecordCipher(cp), receive: new RecordCipher(pc), handshakeHash: state.h, kPc, kCp };
|
|
320
|
+
}
|
|
321
|
+
function encodePayload1(conn, app) {
|
|
322
|
+
return Buffer.from(JSON.stringify({ v: 1, conn: b64u(conn), app }), "utf8");
|
|
323
|
+
}
|
|
324
|
+
function encodePayload2(conn) {
|
|
325
|
+
return Buffer.from(JSON.stringify({ v: 1, conn }), "utf8");
|
|
326
|
+
}
|
|
327
|
+
function jsonObject(bytes) {
|
|
328
|
+
try {
|
|
329
|
+
const value = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
330
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
331
|
+
} catch {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
var isConn = (value) => typeof value === "string" && fromB64u(value)?.length === CONN_BYTES;
|
|
336
|
+
function parsePayload1(bytes) {
|
|
337
|
+
if (bytes.length > MAX_HANDSHAKE_PAYLOAD) return null;
|
|
338
|
+
const value = jsonObject(bytes);
|
|
339
|
+
if (!value || value.v !== 1 || !isConn(value.conn)) return null;
|
|
340
|
+
if (value.app !== void 0 && (typeof value.app !== "string" || value.app.length > MAX_APP_CHARS)) return null;
|
|
341
|
+
return { v: 1, conn: value.conn, ...typeof value.app === "string" ? { app: value.app } : {} };
|
|
342
|
+
}
|
|
343
|
+
function checkPayload2(bytes, conn) {
|
|
344
|
+
const value = bytes.length <= MAX_HANDSHAKE_PAYLOAD ? jsonObject(bytes) : null;
|
|
345
|
+
if (!value || value.v !== 1) return "version";
|
|
346
|
+
return value.conn === conn ? "ok" : "conn";
|
|
347
|
+
}
|
|
348
|
+
function phoneHello(options, payload1, conn) {
|
|
349
|
+
if (!isPublicKey(options.remoteStatic)) throw new NoiseError("bad_point");
|
|
350
|
+
if (options.psk.length !== 32) throw new NoiseError("bad_psk");
|
|
351
|
+
if (payload1.length > MAX_HANDSHAKE_PAYLOAD) throw new NoiseError("payload_too_large");
|
|
352
|
+
const state = initialized(options.transport, options.remoteStatic, options.trace ?? null);
|
|
353
|
+
state.mixKeyAndHash(options.psk);
|
|
354
|
+
state.mark("msg1.psk");
|
|
355
|
+
const e = options.ephemeral ?? generateKeyPair();
|
|
356
|
+
state.mixHash(e.publicKey);
|
|
357
|
+
state.mixKey(e.publicKey);
|
|
358
|
+
state.mark("msg1.e");
|
|
359
|
+
state.mixKey(dh(e.privateKey, options.remoteStatic));
|
|
360
|
+
state.mark("msg1.es");
|
|
361
|
+
const message1 = Buffer.concat([e.publicKey, state.encryptAndHash(payload1)]);
|
|
362
|
+
state.mark("msg1.payload");
|
|
363
|
+
let done = false;
|
|
364
|
+
return {
|
|
365
|
+
message1,
|
|
366
|
+
finish(message2) {
|
|
367
|
+
if (done) throw new NoiseError("finished");
|
|
368
|
+
done = true;
|
|
369
|
+
if (message2.length < PUBLIC_KEY_BYTES + TAG_BYTES || message2.length > MAX_HANDSHAKE_MESSAGE) throw new NoiseError("message2_size");
|
|
370
|
+
const re = message2.subarray(0, PUBLIC_KEY_BYTES);
|
|
371
|
+
const ee = dh(e.privateKey, re);
|
|
372
|
+
state.mixHash(re);
|
|
373
|
+
state.mixKey(re);
|
|
374
|
+
state.mark("msg2.e");
|
|
375
|
+
state.mixKey(ee);
|
|
376
|
+
state.mark("msg2.ee");
|
|
377
|
+
const payload2 = state.decryptAndHash(message2.subarray(PUBLIC_KEY_BYTES));
|
|
378
|
+
state.mark("msg2.payload");
|
|
379
|
+
const check = checkPayload2(payload2, conn);
|
|
380
|
+
if (check !== "ok") throw new NoiseError(`payload2_${check}`);
|
|
381
|
+
return sessionKeys(state, "phone");
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function computerAccept(options, message1) {
|
|
386
|
+
if (message1.length < PUBLIC_KEY_BYTES + TAG_BYTES || message1.length > MAX_HANDSHAKE_MESSAGE) return null;
|
|
387
|
+
const re = message1.subarray(0, PUBLIC_KEY_BYTES);
|
|
388
|
+
let es;
|
|
389
|
+
try {
|
|
390
|
+
es = dh(options.staticKey.privateKey, re);
|
|
391
|
+
} catch {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
const base = initialized(options.transport, options.staticKey.publicKey, options.trace ?? null);
|
|
395
|
+
let match = null;
|
|
396
|
+
for (const candidate of options.candidates) {
|
|
397
|
+
if (candidate.psk.length !== 32) continue;
|
|
398
|
+
const state2 = base.clone();
|
|
399
|
+
state2.mixKeyAndHash(candidate.psk);
|
|
400
|
+
state2.mark("msg1.psk");
|
|
401
|
+
state2.mixHash(re);
|
|
402
|
+
state2.mixKey(re);
|
|
403
|
+
state2.mark("msg1.e");
|
|
404
|
+
state2.mixKey(es);
|
|
405
|
+
state2.mark("msg1.es");
|
|
406
|
+
try {
|
|
407
|
+
const payload = state2.decryptAndHash(message1.subarray(PUBLIC_KEY_BYTES));
|
|
408
|
+
state2.mark("msg1.payload");
|
|
409
|
+
match ??= { id: candidate.id, state: state2, payload };
|
|
410
|
+
} catch {
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (!match) return null;
|
|
414
|
+
const payload1 = parsePayload1(match.payload);
|
|
415
|
+
if (!payload1) return null;
|
|
416
|
+
const { state } = match;
|
|
417
|
+
const e = options.ephemeral ?? generateKeyPair();
|
|
418
|
+
state.mixHash(e.publicKey);
|
|
419
|
+
state.mixKey(e.publicKey);
|
|
420
|
+
state.mark("msg2.e");
|
|
421
|
+
state.mixKey(dh(e.privateKey, re));
|
|
422
|
+
state.mark("msg2.ee");
|
|
423
|
+
const payload2 = options.payload2 ? options.payload2(payload1) : encodePayload2(payload1.conn);
|
|
424
|
+
const message2 = Buffer.concat([e.publicKey, state.encryptAndHash(payload2)]);
|
|
425
|
+
state.mark("msg2.payload");
|
|
426
|
+
if (options.trace && state.trace) options.trace.splice(0, options.trace.length, ...state.trace);
|
|
427
|
+
return { id: match.id, payload1, message2, keys: sessionKeys(state, "computer") };
|
|
428
|
+
}
|
|
429
|
+
var newConn = () => randomBytes2(CONN_BYTES);
|
|
430
|
+
var VERSION = 0;
|
|
431
|
+
var TYPE = { data: 0, windowUpdate: 1, ping: 2, goAway: 3 };
|
|
432
|
+
var FLAG = { syn: 1, ack: 2, fin: 4, rst: 8 };
|
|
433
|
+
var KNOWN_FLAGS = 15;
|
|
434
|
+
var GO_AWAY = { normal: 0, protocol: 1, internal: 2, removed: 3 };
|
|
435
|
+
var HEADER_BYTES = 12;
|
|
436
|
+
var MAX_DATA_PAYLOAD = 65507;
|
|
437
|
+
var INITIAL_WINDOW = 256 * 1024;
|
|
438
|
+
var STREAM_WINDOW = 2 * 1024 * 1024;
|
|
439
|
+
var OPEN_WINDOW_DELTA = STREAM_WINDOW - INITIAL_WINDOW;
|
|
440
|
+
var WINDOW_UPDATE_AFTER = 1024 * 1024;
|
|
441
|
+
var MAX_STREAMS = 64;
|
|
442
|
+
var MAX_STREAM_ID = 2 ** 31 - 1;
|
|
443
|
+
var PING_INTERVAL_MS = 2e4;
|
|
444
|
+
var PING_TIMEOUT_MS = 1e4;
|
|
445
|
+
var YamuxError = class extends Error {
|
|
446
|
+
reason;
|
|
447
|
+
constructor(reason) {
|
|
448
|
+
super(`yamux: ${reason}`);
|
|
449
|
+
this.reason = reason;
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
function encodeFrame(frame) {
|
|
453
|
+
const payload = frame.type === TYPE.data ? frame.data ?? new Uint8Array(0) : new Uint8Array(0);
|
|
454
|
+
if (frame.type === TYPE.data && payload.length !== frame.length) throw new YamuxError("data_length");
|
|
455
|
+
if (payload.length > MAX_DATA_PAYLOAD) throw new YamuxError("data_too_large");
|
|
456
|
+
const out = Buffer.alloc(HEADER_BYTES + payload.length);
|
|
457
|
+
out.writeUInt8(VERSION, 0);
|
|
458
|
+
out.writeUInt8(frame.type, 1);
|
|
459
|
+
out.writeUInt16BE(frame.flags, 2);
|
|
460
|
+
out.writeUInt32BE(frame.streamId, 4);
|
|
461
|
+
out.writeUInt32BE(frame.length, 8);
|
|
462
|
+
out.set(payload, HEADER_BYTES);
|
|
463
|
+
return out;
|
|
464
|
+
}
|
|
465
|
+
function decodeFrame(bytes) {
|
|
466
|
+
if (bytes.length < HEADER_BYTES) throw new YamuxError("short_frame");
|
|
467
|
+
const view = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
468
|
+
if (view.readUInt8(0) !== VERSION) throw new YamuxError("version");
|
|
469
|
+
const type = view.readUInt8(1);
|
|
470
|
+
if (type > TYPE.goAway) throw new YamuxError("type");
|
|
471
|
+
const flags = view.readUInt16BE(2);
|
|
472
|
+
if ((flags & ~KNOWN_FLAGS) !== 0) throw new YamuxError("flags");
|
|
473
|
+
const streamId = view.readUInt32BE(4);
|
|
474
|
+
const length = view.readUInt32BE(8);
|
|
475
|
+
if (type === TYPE.data) {
|
|
476
|
+
if (length > MAX_DATA_PAYLOAD) throw new YamuxError("data_too_large");
|
|
477
|
+
if (bytes.length !== HEADER_BYTES + length) throw new YamuxError("data_length");
|
|
478
|
+
if (streamId === 0) throw new YamuxError("stream_zero");
|
|
479
|
+
return { type, flags, streamId, length, data: view.subarray(HEADER_BYTES) };
|
|
480
|
+
}
|
|
481
|
+
if (bytes.length !== HEADER_BYTES) throw new YamuxError("trailing_bytes");
|
|
482
|
+
if (type === TYPE.windowUpdate) {
|
|
483
|
+
if (streamId === 0) throw new YamuxError("stream_zero");
|
|
484
|
+
} else {
|
|
485
|
+
if (streamId !== 0) throw new YamuxError("session_frame_stream");
|
|
486
|
+
if (type === TYPE.ping && flags !== FLAG.syn && flags !== FLAG.ack) throw new YamuxError("ping_flags");
|
|
487
|
+
if (type === TYPE.goAway && flags !== 0) throw new YamuxError("goaway_flags");
|
|
488
|
+
}
|
|
489
|
+
return { type, flags, streamId, length };
|
|
490
|
+
}
|
|
491
|
+
var openFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.syn, streamId, length: OPEN_WINDOW_DELTA });
|
|
492
|
+
var acceptFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.ack, streamId, length: OPEN_WINDOW_DELTA });
|
|
493
|
+
var resetFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.rst, streamId, length: 0 });
|
|
494
|
+
var finFrame = (streamId) => ({ type: TYPE.windowUpdate, flags: FLAG.fin, streamId, length: 0 });
|
|
495
|
+
var windowFrame = (streamId, delta) => ({ type: TYPE.windowUpdate, flags: 0, streamId, length: delta });
|
|
496
|
+
var dataFrame = (streamId, data, flags = 0) => ({ type: TYPE.data, flags, streamId, length: data.length, data });
|
|
497
|
+
var pingFrame = (value, flags = FLAG.syn) => ({ type: TYPE.ping, flags, streamId: 0, length: value >>> 0 });
|
|
498
|
+
var goAwayFrame = (code) => ({ type: TYPE.goAway, flags: 0, streamId: 0, length: code });
|
|
499
|
+
var Mux = class {
|
|
500
|
+
role;
|
|
501
|
+
#events;
|
|
502
|
+
#streams = /* @__PURE__ */ new Map();
|
|
503
|
+
/** The highest ID the phone has opened. */
|
|
504
|
+
#highestId = 0;
|
|
505
|
+
#peerGoneAway = false;
|
|
506
|
+
#localGoneAway = false;
|
|
507
|
+
constructor(role, events) {
|
|
508
|
+
this.role = role;
|
|
509
|
+
this.#events = events;
|
|
510
|
+
}
|
|
511
|
+
get activeStreams() {
|
|
512
|
+
return this.#streams.size;
|
|
513
|
+
}
|
|
514
|
+
get peerGoneAway() {
|
|
515
|
+
return this.#peerGoneAway;
|
|
516
|
+
}
|
|
517
|
+
has(streamId) {
|
|
518
|
+
return this.#streams.has(streamId);
|
|
519
|
+
}
|
|
520
|
+
sendWindow(streamId) {
|
|
521
|
+
return this.#streams.get(streamId)?.sendWindow ?? 0;
|
|
522
|
+
}
|
|
523
|
+
/** The phone side. Data may follow at once, within the first 256 KiB. */
|
|
524
|
+
open() {
|
|
525
|
+
if (this.role !== "phone") throw new YamuxError("computer_open");
|
|
526
|
+
if (this.#localGoneAway || this.#peerGoneAway) throw new YamuxError("gone_away");
|
|
527
|
+
if (this.#streams.size >= MAX_STREAMS) throw new YamuxError("too_many_streams");
|
|
528
|
+
const streamId = this.#highestId === 0 ? 1 : this.#highestId + 2;
|
|
529
|
+
if (streamId > MAX_STREAM_ID) throw new YamuxError("ids_exhausted");
|
|
530
|
+
this.#highestId = streamId;
|
|
531
|
+
this.#streams.set(streamId, {
|
|
532
|
+
established: false,
|
|
533
|
+
receiveWindow: INITIAL_WINDOW + OPEN_WINDOW_DELTA,
|
|
534
|
+
sendWindow: INITIAL_WINDOW,
|
|
535
|
+
uncredited: 0,
|
|
536
|
+
localFin: false,
|
|
537
|
+
remoteFin: false
|
|
538
|
+
});
|
|
539
|
+
this.#events.send(openFrame(streamId));
|
|
540
|
+
return streamId;
|
|
541
|
+
}
|
|
542
|
+
/** The computer side, once the origin socket is connected. */
|
|
543
|
+
accept(streamId) {
|
|
544
|
+
const stream = this.#streams.get(streamId);
|
|
545
|
+
if (!stream || stream.established || this.role !== "computer") return;
|
|
546
|
+
stream.established = true;
|
|
547
|
+
stream.receiveWindow += OPEN_WINDOW_DELTA;
|
|
548
|
+
this.#events.send(acceptFrame(streamId));
|
|
549
|
+
}
|
|
550
|
+
/** Sends up to the window and the frame cap; returns the bytes taken. */
|
|
551
|
+
write(streamId, data) {
|
|
552
|
+
const stream = this.#streams.get(streamId);
|
|
553
|
+
if (!stream || stream.localFin || this.role === "computer" && !stream.established) return 0;
|
|
554
|
+
const size = Math.min(data.length, stream.sendWindow, MAX_DATA_PAYLOAD);
|
|
555
|
+
if (size === 0) return 0;
|
|
556
|
+
stream.sendWindow -= size;
|
|
557
|
+
this.#events.send(dataFrame(streamId, data.subarray(0, size)));
|
|
558
|
+
return size;
|
|
559
|
+
}
|
|
560
|
+
finish(streamId) {
|
|
561
|
+
const stream = this.#streams.get(streamId);
|
|
562
|
+
if (!stream || stream.localFin) return;
|
|
563
|
+
stream.localFin = true;
|
|
564
|
+
this.#events.send(finFrame(streamId));
|
|
565
|
+
this.#closeIfDone(streamId, stream);
|
|
566
|
+
}
|
|
567
|
+
reset(streamId) {
|
|
568
|
+
if (!this.#streams.delete(streamId)) return;
|
|
569
|
+
this.#events.send(resetFrame(streamId));
|
|
570
|
+
this.#events.reset(streamId);
|
|
571
|
+
}
|
|
572
|
+
/** The receiver handed `bytes` on (to the origin socket, or the HTTP stack). Grants them back in 1 MiB steps. */
|
|
573
|
+
consumed(streamId, bytes) {
|
|
574
|
+
const stream = this.#streams.get(streamId);
|
|
575
|
+
if (!stream || stream.remoteFin) return;
|
|
576
|
+
stream.uncredited += bytes;
|
|
577
|
+
if (stream.uncredited < WINDOW_UPDATE_AFTER) return;
|
|
578
|
+
const delta = stream.uncredited;
|
|
579
|
+
stream.uncredited = 0;
|
|
580
|
+
stream.receiveWindow += delta;
|
|
581
|
+
this.#events.send(windowFrame(streamId, delta));
|
|
582
|
+
}
|
|
583
|
+
ping(value) {
|
|
584
|
+
this.#events.send(pingFrame(value));
|
|
585
|
+
}
|
|
586
|
+
goAway(code = GO_AWAY.normal) {
|
|
587
|
+
this.#localGoneAway = true;
|
|
588
|
+
this.#events.send(goAwayFrame(code));
|
|
589
|
+
}
|
|
590
|
+
/** Sends GoAway(1); the caller closes with 4000. */
|
|
591
|
+
protocolError() {
|
|
592
|
+
this.goAway(GO_AWAY.protocol);
|
|
593
|
+
this.closeAll();
|
|
594
|
+
}
|
|
595
|
+
closeAll() {
|
|
596
|
+
const ids = [...this.#streams.keys()];
|
|
597
|
+
this.#streams.clear();
|
|
598
|
+
for (const id2 of ids) this.#events.reset(id2);
|
|
599
|
+
}
|
|
600
|
+
receive(frame) {
|
|
601
|
+
if (frame.type === TYPE.ping) {
|
|
602
|
+
if (frame.flags === FLAG.syn) this.#events.send(pingFrame(frame.length, FLAG.ack));
|
|
603
|
+
else this.#events.pong?.(frame.length);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (frame.type === TYPE.goAway) {
|
|
607
|
+
this.#peerGoneAway = true;
|
|
608
|
+
this.#events.goAway?.(frame.length);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (frame.flags & FLAG.syn) {
|
|
612
|
+
this.#receiveOpen(frame);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (frame.flags & FLAG.ack) {
|
|
616
|
+
this.#receiveAck(frame);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const stream = this.#known(frame.streamId);
|
|
620
|
+
if (!stream) return;
|
|
621
|
+
this.#receiveOnStream(frame.streamId, stream, frame);
|
|
622
|
+
}
|
|
623
|
+
/** A stream that ended is ignored: an RST and the peer's last frames cross. An ID never opened is an error. */
|
|
624
|
+
#known(streamId) {
|
|
625
|
+
if (streamId % 2 === 0 || streamId > this.#highestId) throw new YamuxError("unknown_stream");
|
|
626
|
+
return this.#streams.get(streamId) ?? null;
|
|
627
|
+
}
|
|
628
|
+
#receiveOpen(frame) {
|
|
629
|
+
if (this.role === "phone") throw new YamuxError("computer_opened_stream");
|
|
630
|
+
if (frame.type !== TYPE.windowUpdate || frame.flags !== FLAG.syn) throw new YamuxError("open_flags");
|
|
631
|
+
if (frame.streamId % 2 === 0) throw new YamuxError("even_stream");
|
|
632
|
+
if (frame.streamId <= this.#highestId) throw new YamuxError("stream_reused");
|
|
633
|
+
this.#highestId = frame.streamId;
|
|
634
|
+
if (this.#localGoneAway || this.#streams.size >= MAX_STREAMS) {
|
|
635
|
+
this.#events.send(resetFrame(frame.streamId));
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
this.#streams.set(frame.streamId, {
|
|
639
|
+
established: false,
|
|
640
|
+
receiveWindow: INITIAL_WINDOW,
|
|
641
|
+
sendWindow: INITIAL_WINDOW + frame.length,
|
|
642
|
+
uncredited: 0,
|
|
643
|
+
localFin: false,
|
|
644
|
+
remoteFin: false
|
|
645
|
+
});
|
|
646
|
+
this.#events.opened?.(frame.streamId);
|
|
647
|
+
}
|
|
648
|
+
#receiveAck(frame) {
|
|
649
|
+
if (this.role === "computer") throw new YamuxError("phone_ack");
|
|
650
|
+
if (frame.type !== TYPE.windowUpdate || (frame.flags & ~(FLAG.ack | FLAG.fin | FLAG.rst)) !== 0) throw new YamuxError("ack_flags");
|
|
651
|
+
const stream = this.#known(frame.streamId);
|
|
652
|
+
if (!stream) return;
|
|
653
|
+
if (stream.established) throw new YamuxError("duplicate_ack");
|
|
654
|
+
stream.established = true;
|
|
655
|
+
this.#events.accepted?.(frame.streamId);
|
|
656
|
+
this.#receiveOnStream(frame.streamId, stream, { ...frame, flags: frame.flags & ~FLAG.ack });
|
|
657
|
+
}
|
|
658
|
+
#receiveOnStream(streamId, stream, frame) {
|
|
659
|
+
if (frame.flags & FLAG.rst) {
|
|
660
|
+
this.#streams.delete(streamId);
|
|
661
|
+
this.#events.reset(streamId);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (frame.type === TYPE.data) {
|
|
665
|
+
if (stream.remoteFin && (frame.length > 0 || frame.flags & FLAG.fin)) throw new YamuxError("data_after_fin");
|
|
666
|
+
if (frame.length > stream.receiveWindow) throw new YamuxError("window_exceeded");
|
|
667
|
+
stream.receiveWindow -= frame.length;
|
|
668
|
+
if (frame.length > 0) this.#events.data(streamId, frame.data);
|
|
669
|
+
} else if (frame.length > 0) {
|
|
670
|
+
stream.sendWindow += frame.length;
|
|
671
|
+
this.#events.writable(streamId);
|
|
672
|
+
}
|
|
673
|
+
if (frame.flags & FLAG.fin) {
|
|
674
|
+
if (stream.remoteFin) throw new YamuxError("data_after_fin");
|
|
675
|
+
stream.remoteFin = true;
|
|
676
|
+
this.#events.ended(streamId);
|
|
677
|
+
this.#closeIfDone(streamId, stream);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
#closeIfDone(streamId, stream) {
|
|
681
|
+
if (stream.localFin && stream.remoteFin) this.#streams.delete(streamId);
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
var SEND_BUFFER_HIGH = 1024 * 1024;
|
|
685
|
+
var SessionClosed = class extends Error {
|
|
686
|
+
code;
|
|
687
|
+
constructor(code) {
|
|
688
|
+
super(`e2e session closed (${code})`);
|
|
689
|
+
this.code = code;
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
var noop = () => {
|
|
693
|
+
};
|
|
694
|
+
var MuxStream = class extends Duplex {
|
|
695
|
+
streamId;
|
|
696
|
+
#session;
|
|
697
|
+
#pending = null;
|
|
698
|
+
#pendingDone = null;
|
|
699
|
+
#ungranted = 0;
|
|
700
|
+
constructor(session, streamId) {
|
|
701
|
+
super({ allowHalfOpen: true, readableHighWaterMark: 256 * 1024 });
|
|
702
|
+
this.#session = session;
|
|
703
|
+
this.streamId = streamId;
|
|
704
|
+
this.on("error", noop);
|
|
705
|
+
}
|
|
706
|
+
_write(chunk, _encoding, done) {
|
|
707
|
+
this.#pending = chunk;
|
|
708
|
+
this.#pendingDone = done;
|
|
709
|
+
this.flush();
|
|
710
|
+
}
|
|
711
|
+
_final(done) {
|
|
712
|
+
this.#session.mux?.finish(this.streamId);
|
|
713
|
+
done();
|
|
714
|
+
}
|
|
715
|
+
_read() {
|
|
716
|
+
if (this.#ungranted === 0) return;
|
|
717
|
+
const bytes = this.#ungranted;
|
|
718
|
+
this.#ungranted = 0;
|
|
719
|
+
this.#session.mux?.consumed(this.streamId, bytes);
|
|
720
|
+
}
|
|
721
|
+
_destroy(error2, done) {
|
|
722
|
+
const mux = this.#session.mux;
|
|
723
|
+
if (mux?.has(this.streamId)) mux.reset(this.streamId);
|
|
724
|
+
this.#session.forget(this.streamId);
|
|
725
|
+
const pendingDone = this.#pendingDone;
|
|
726
|
+
this.#pending = null;
|
|
727
|
+
this.#pendingDone = null;
|
|
728
|
+
pendingDone?.(error2 ?? new Error("stream reset"));
|
|
729
|
+
done(error2);
|
|
730
|
+
}
|
|
731
|
+
/** Granted back as the reader takes it, so a slow reader slows the sender. */
|
|
732
|
+
deliver(data) {
|
|
733
|
+
if (this.push(data)) this.#session.mux?.consumed(this.streamId, data.length);
|
|
734
|
+
else this.#ungranted += data.length;
|
|
735
|
+
}
|
|
736
|
+
remoteEnded() {
|
|
737
|
+
this.push(null);
|
|
738
|
+
}
|
|
739
|
+
remoteReset() {
|
|
740
|
+
if (!this.destroyed) this.destroy(new Error("stream reset"));
|
|
741
|
+
}
|
|
742
|
+
flush() {
|
|
743
|
+
const mux = this.#session.mux;
|
|
744
|
+
while (this.#pending && this.#pending.length > 0) {
|
|
745
|
+
if (!mux || !mux.has(this.streamId)) return void this.destroy(new Error("stream reset"));
|
|
746
|
+
if (this.#session.congested) return this.#session.whenDrained(() => this.flush());
|
|
747
|
+
const taken = mux.write(this.streamId, this.#pending);
|
|
748
|
+
if (taken === 0) return;
|
|
749
|
+
this.#pending = this.#pending.subarray(taken);
|
|
750
|
+
}
|
|
751
|
+
const done = this.#pendingDone;
|
|
752
|
+
this.#pending = null;
|
|
753
|
+
this.#pendingDone = null;
|
|
754
|
+
done?.();
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
var E2ESession = class _E2ESession {
|
|
758
|
+
role;
|
|
759
|
+
#transport;
|
|
760
|
+
#streams = /* @__PURE__ */ new Map();
|
|
761
|
+
#timers;
|
|
762
|
+
#state = "handshake";
|
|
763
|
+
#keys = null;
|
|
764
|
+
#mux = null;
|
|
765
|
+
#accept = null;
|
|
766
|
+
#deadline = null;
|
|
767
|
+
#pingTimer = null;
|
|
768
|
+
#pongTimer = null;
|
|
769
|
+
#awaitingPong = null;
|
|
770
|
+
#pingCounter = 0;
|
|
771
|
+
#drainWaiters = [];
|
|
772
|
+
#settleReady;
|
|
773
|
+
#settleClosed;
|
|
774
|
+
#connectOrigin = null;
|
|
775
|
+
#removed = false;
|
|
776
|
+
/** The computer side: which device (or pairing window) this is. */
|
|
777
|
+
peer = null;
|
|
778
|
+
ready;
|
|
779
|
+
closed;
|
|
780
|
+
constructor(role, transport, timers) {
|
|
781
|
+
this.role = role;
|
|
782
|
+
this.#transport = transport;
|
|
783
|
+
this.#timers = {
|
|
784
|
+
pingIntervalMs: timers.pingIntervalMs ?? PING_INTERVAL_MS,
|
|
785
|
+
pingTimeoutMs: timers.pingTimeoutMs ?? PING_TIMEOUT_MS,
|
|
786
|
+
handshakeDeadlineMs: timers.handshakeDeadlineMs ?? HANDSHAKE_DEADLINE_MS
|
|
787
|
+
};
|
|
788
|
+
this.ready = new Promise((resolve6, reject) => {
|
|
789
|
+
this.#settleReady = (error2) => error2 ? reject(error2) : resolve6();
|
|
790
|
+
});
|
|
791
|
+
this.ready.catch(noop);
|
|
792
|
+
this.closed = new Promise((resolve6) => {
|
|
793
|
+
this.#settleClosed = resolve6;
|
|
794
|
+
});
|
|
795
|
+
this.#deadline = setTimeout(() => this.close(CLOSE.deadline), this.#timers.handshakeDeadlineMs);
|
|
796
|
+
this.#deadline.unref?.();
|
|
797
|
+
}
|
|
798
|
+
/** Waits for message 1. Anything wrong with it closes 4001 and nothing else is sent. */
|
|
799
|
+
static computer(options) {
|
|
800
|
+
const session = new _E2ESession("computer", options.transport, options);
|
|
801
|
+
session.#connectOrigin = options.connectOrigin;
|
|
802
|
+
session.#accept = (message) => {
|
|
803
|
+
const accepted = computerAccept({ staticKey: options.staticKey, transport: options.transportByte, candidates: options.candidates() }, message);
|
|
804
|
+
if (!accepted) return session.close(CLOSE.rejected);
|
|
805
|
+
options.transport.send(accepted.message2);
|
|
806
|
+
session.peer = accepted.id;
|
|
807
|
+
session.#start(accepted.keys);
|
|
808
|
+
options.opened?.(accepted.id, accepted.payload1);
|
|
809
|
+
};
|
|
810
|
+
return session;
|
|
811
|
+
}
|
|
812
|
+
/** Sends message 1 now. `ready` rejects with a NoiseError when the computer can't be verified: a hard stop. */
|
|
813
|
+
static phone(options) {
|
|
814
|
+
const session = new _E2ESession("phone", options.transport, options);
|
|
815
|
+
const conn = newConn();
|
|
816
|
+
const hello = phoneHello(
|
|
817
|
+
{ remoteStatic: options.remoteStatic, psk: options.psk, transport: options.transportByte, ephemeral: options.ephemeral },
|
|
818
|
+
encodePayload1(conn, options.app),
|
|
819
|
+
b64u(conn)
|
|
820
|
+
);
|
|
821
|
+
session.#accept = (message) => {
|
|
822
|
+
let keys;
|
|
823
|
+
try {
|
|
824
|
+
keys = hello.finish(message);
|
|
825
|
+
} catch (error2) {
|
|
826
|
+
session.#settleReady(error2);
|
|
827
|
+
return session.close(CLOSE.protocol);
|
|
828
|
+
}
|
|
829
|
+
session.#start(keys);
|
|
830
|
+
};
|
|
831
|
+
options.transport.send(hello.message1);
|
|
832
|
+
return session;
|
|
833
|
+
}
|
|
834
|
+
get mux() {
|
|
835
|
+
return this.#mux;
|
|
836
|
+
}
|
|
837
|
+
/** The phone side: the computer said, inside the encryption, that this device was removed. Only this, or a 4001 on
|
|
838
|
+
* a direct route, means "pair again"; a bare 4001 through the relay may be the relay's own and means "keep trying". */
|
|
839
|
+
get removed() {
|
|
840
|
+
return this.#removed;
|
|
841
|
+
}
|
|
842
|
+
get state() {
|
|
843
|
+
return this.#state;
|
|
844
|
+
}
|
|
845
|
+
/** Records each way so far, for diagnostics. */
|
|
846
|
+
get records() {
|
|
847
|
+
return { sent: this.#keys?.send.nonce ?? 0, received: this.#keys?.receive.nonce ?? 0 };
|
|
848
|
+
}
|
|
849
|
+
get congested() {
|
|
850
|
+
return this.#transport.bufferedAmount > SEND_BUFFER_HIGH;
|
|
851
|
+
}
|
|
852
|
+
get activeStreams() {
|
|
853
|
+
return this.#streams.size;
|
|
854
|
+
}
|
|
855
|
+
whenDrained(listener) {
|
|
856
|
+
this.#drainWaiters.push(listener);
|
|
857
|
+
if (this.#drainWaiters.length > 1) return;
|
|
858
|
+
this.#transport.onceDrained(() => {
|
|
859
|
+
const waiters = this.#drainWaiters;
|
|
860
|
+
this.#drainWaiters = [];
|
|
861
|
+
for (const waiter of waiters) waiter();
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
forget(streamId) {
|
|
865
|
+
this.#streams.delete(streamId);
|
|
866
|
+
}
|
|
867
|
+
/** Every message the transport receives, in order. */
|
|
868
|
+
receive(message, binary) {
|
|
869
|
+
if (this.#state === "closed") return;
|
|
870
|
+
if (this.#state === "handshake") {
|
|
871
|
+
const accept = this.#accept;
|
|
872
|
+
this.#accept = null;
|
|
873
|
+
if (!binary || !accept) return this.close(this.role === "computer" ? CLOSE.rejected : CLOSE.protocol);
|
|
874
|
+
return accept(Buffer.from(message));
|
|
875
|
+
}
|
|
876
|
+
if (!binary) return this.close(CLOSE.protocol);
|
|
877
|
+
let plaintext;
|
|
878
|
+
try {
|
|
879
|
+
plaintext = this.#keys.receive.open(message);
|
|
880
|
+
} catch {
|
|
881
|
+
return this.close(CLOSE.protocol);
|
|
882
|
+
}
|
|
883
|
+
try {
|
|
884
|
+
this.#mux.receive(decodeFrame(plaintext));
|
|
885
|
+
} catch (error2) {
|
|
886
|
+
if (!(error2 instanceof YamuxError)) throw error2;
|
|
887
|
+
this.#mux?.protocolError();
|
|
888
|
+
this.close(CLOSE.protocol);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
/** The phone side. */
|
|
892
|
+
openStream() {
|
|
893
|
+
if (this.#state !== "open" || !this.#mux) throw new SessionClosed(CLOSE.normal);
|
|
894
|
+
const streamId = this.#mux.open();
|
|
895
|
+
const stream = new MuxStream(this, streamId);
|
|
896
|
+
this.#streams.set(streamId, stream);
|
|
897
|
+
return stream;
|
|
898
|
+
}
|
|
899
|
+
/** The computer side: the device was removed. The phone hears it authenticated, then the 4001 close. */
|
|
900
|
+
remove() {
|
|
901
|
+
if (this.#state === "open") this.#mux?.goAway(GO_AWAY.removed);
|
|
902
|
+
this.close(CLOSE.rejected);
|
|
903
|
+
}
|
|
904
|
+
/** A planned close (quit, sleep, restart): GoAway first, so the peer knows it isn't an error. */
|
|
905
|
+
shutdown() {
|
|
906
|
+
if (this.#state === "open") this.#mux?.goAway(GO_AWAY.normal);
|
|
907
|
+
this.close(CLOSE.normal);
|
|
908
|
+
}
|
|
909
|
+
close(code, reason) {
|
|
910
|
+
if (this.#state === "closed") return;
|
|
911
|
+
this.#transport.close(code, reason);
|
|
912
|
+
this.#end(code);
|
|
913
|
+
}
|
|
914
|
+
/** The transport closed under us. */
|
|
915
|
+
transportClosed(code) {
|
|
916
|
+
this.#end(code);
|
|
917
|
+
}
|
|
918
|
+
#end(code) {
|
|
919
|
+
if (this.#state === "closed") return;
|
|
920
|
+
const wasHandshake = this.#state === "handshake";
|
|
921
|
+
this.#state = "closed";
|
|
922
|
+
for (const timer of [this.#deadline, this.#pongTimer]) if (timer) clearTimeout(timer);
|
|
923
|
+
if (this.#pingTimer) clearInterval(this.#pingTimer);
|
|
924
|
+
this.#mux?.closeAll();
|
|
925
|
+
for (const stream of this.#streams.values()) stream.remoteReset();
|
|
926
|
+
this.#streams.clear();
|
|
927
|
+
const waiters = this.#drainWaiters;
|
|
928
|
+
this.#drainWaiters = [];
|
|
929
|
+
for (const waiter of waiters) waiter();
|
|
930
|
+
if (wasHandshake) this.#settleReady(new SessionClosed(code));
|
|
931
|
+
this.#settleClosed(code);
|
|
932
|
+
}
|
|
933
|
+
#start(keys) {
|
|
934
|
+
if (this.#deadline) clearTimeout(this.#deadline);
|
|
935
|
+
this.#keys = keys;
|
|
936
|
+
this.#state = "open";
|
|
937
|
+
this.#mux = new Mux(this.role, {
|
|
938
|
+
send: (frame) => this.#send(frame),
|
|
939
|
+
opened: (streamId) => this.#openOrigin(streamId),
|
|
940
|
+
data: (streamId, data) => this.#streams.get(streamId)?.deliver(Buffer.from(data.buffer, data.byteOffset, data.byteLength)),
|
|
941
|
+
ended: (streamId) => this.#streams.get(streamId)?.remoteEnded(),
|
|
942
|
+
reset: (streamId) => {
|
|
943
|
+
const stream = this.#streams.get(streamId);
|
|
944
|
+
this.#streams.delete(streamId);
|
|
945
|
+
stream?.remoteReset();
|
|
946
|
+
},
|
|
947
|
+
writable: (streamId) => this.#streams.get(streamId)?.flush(),
|
|
948
|
+
goAway: (code) => {
|
|
949
|
+
if (code === GO_AWAY.removed && this.role === "phone") this.#removed = true;
|
|
950
|
+
else if (code !== GO_AWAY.normal) this.close(CLOSE.protocol);
|
|
951
|
+
},
|
|
952
|
+
pong: (value) => {
|
|
953
|
+
if (value !== this.#awaitingPong) return;
|
|
954
|
+
this.#awaitingPong = null;
|
|
955
|
+
if (this.#pongTimer) clearTimeout(this.#pongTimer);
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
this.#pingTimer = setInterval(() => this.#ping(), this.#timers.pingIntervalMs);
|
|
959
|
+
this.#pingTimer.unref?.();
|
|
960
|
+
this.#settleReady();
|
|
961
|
+
}
|
|
962
|
+
#ping() {
|
|
963
|
+
if (this.#awaitingPong !== null || !this.#mux) return;
|
|
964
|
+
this.#pingCounter = this.#pingCounter + 1 >>> 0;
|
|
965
|
+
this.#awaitingPong = this.#pingCounter;
|
|
966
|
+
this.#mux.ping(this.#pingCounter);
|
|
967
|
+
this.#pongTimer = setTimeout(() => this.close(CLOSE.goingAway), this.#timers.pingTimeoutMs);
|
|
968
|
+
this.#pongTimer.unref?.();
|
|
969
|
+
}
|
|
970
|
+
#send(frame) {
|
|
971
|
+
if (this.#state !== "open" || !this.#keys) return;
|
|
972
|
+
let record;
|
|
973
|
+
try {
|
|
974
|
+
record = this.#keys.send.seal(encodeFrame(frame));
|
|
975
|
+
} catch (error2) {
|
|
976
|
+
if (error2 instanceof NoiseError) return this.close(CLOSE.normal);
|
|
977
|
+
throw error2;
|
|
978
|
+
}
|
|
979
|
+
this.#transport.send(record);
|
|
980
|
+
if (this.#keys.send.nonce >= NONCE_LIMIT - 1024) queueMicrotask(() => this.close(CLOSE.normal));
|
|
981
|
+
}
|
|
982
|
+
#openOrigin(streamId) {
|
|
983
|
+
const stream = new MuxStream(this, streamId);
|
|
984
|
+
this.#streams.set(streamId, stream);
|
|
985
|
+
const connect = this.#connectOrigin;
|
|
986
|
+
if (!connect) return void stream.destroy();
|
|
987
|
+
connect().then(
|
|
988
|
+
(socket) => {
|
|
989
|
+
if (stream.destroyed || this.#state !== "open") return void socket.destroy();
|
|
990
|
+
this.#mux?.accept(streamId);
|
|
991
|
+
socket.on("error", () => stream.destroy());
|
|
992
|
+
socket.on("close", () => {
|
|
993
|
+
if (!socket.readableEnded) stream.destroy();
|
|
994
|
+
});
|
|
995
|
+
stream.on("close", () => socket.destroy());
|
|
996
|
+
stream.pipe(socket);
|
|
997
|
+
socket.pipe(stream);
|
|
998
|
+
},
|
|
999
|
+
() => stream.destroy()
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
var DIRECT_PROTOCOL = "omb-e2e.v1";
|
|
1004
|
+
|
|
1005
|
+
// src/secure-pairing.ts
|
|
1006
|
+
var PAIRING_TTL_MS = 12e4;
|
|
1007
|
+
var PAIRING_ATTEMPTS = 5;
|
|
1008
|
+
var MAX_PAIRED_CLIENTS = 128;
|
|
1009
|
+
var MAX_INVITES = 20;
|
|
1010
|
+
var digestEqual = (a, b) => /^[0-9a-f]{64}$/.test(a) && /^[0-9a-f]{64}$/.test(b) && timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
1011
|
+
var nameOf = (value) => String(value ?? "").replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 60) || "DSH Host";
|
|
1012
|
+
var SecurePairing = class {
|
|
1013
|
+
constructor(db, machineId, commitClient, now = Date.now) {
|
|
1014
|
+
this.db = db;
|
|
1015
|
+
this.machineId = machineId;
|
|
1016
|
+
this.commitClient = commitClient;
|
|
1017
|
+
this.now = now;
|
|
1018
|
+
db.exec(`CREATE TABLE IF NOT EXISTS pairing_invites (
|
|
1019
|
+
id TEXT PRIMARY KEY, client_id TEXT NOT NULL, token_digest TEXT NOT NULL, code_digest TEXT NOT NULL,
|
|
1020
|
+
expires_at INTEGER NOT NULL, attempts_left INTEGER NOT NULL, ceiling TEXT NOT NULL, name TEXT NOT NULL,
|
|
1021
|
+
consumed_request TEXT, consumed_at INTEGER
|
|
1022
|
+
); CREATE INDEX IF NOT EXISTS pairing_invite_expiry ON pairing_invites(expires_at);`);
|
|
1023
|
+
const generated = generateKeyPair();
|
|
1024
|
+
db.prepare("INSERT OR IGNORE INTO meta VALUES (?,?)").run("e2eTransportPrivateKey", b64u(generated.privateKey));
|
|
1025
|
+
const stored = db.prepare("SELECT value FROM meta WHERE key=?").get("e2eTransportPrivateKey")?.value;
|
|
1026
|
+
if (typeof stored !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(stored)) fail("PAIRING_IDENTITY_INVALID");
|
|
1027
|
+
try {
|
|
1028
|
+
this.transportKey = keyPairFromPrivate(Buffer.from(stored, "base64url"));
|
|
1029
|
+
} catch {
|
|
1030
|
+
fail("PAIRING_IDENTITY_INVALID");
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
db;
|
|
1034
|
+
machineId;
|
|
1035
|
+
commitClient;
|
|
1036
|
+
now;
|
|
1037
|
+
replay = /* @__PURE__ */ new Map();
|
|
1038
|
+
transportKey;
|
|
1039
|
+
identity() {
|
|
1040
|
+
return { kind: "noise-v1", publicKey: b64u(this.transportKey.publicKey), fingerprint: sha(this.transportKey.publicKey) };
|
|
1041
|
+
}
|
|
1042
|
+
create(clientId, ceiling, options) {
|
|
1043
|
+
identifier(clientId);
|
|
1044
|
+
let endpoint;
|
|
1045
|
+
try {
|
|
1046
|
+
endpoint = new URL(options.endpoint);
|
|
1047
|
+
} catch {
|
|
1048
|
+
fail("PAIRING_ENDPOINT_INVALID");
|
|
1049
|
+
}
|
|
1050
|
+
if (!["http:", "https:"].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.pathname !== "/" || endpoint.search || endpoint.hash || endpoint.href.length > 1024) fail("PAIRING_ENDPOINT_INVALID");
|
|
1051
|
+
const now = this.now(), token = `dsh_pair_${secret()}`, code = String(randomInt(1e6)).padStart(6, "0"), id2 = `invite-${randomUUID()}`;
|
|
1052
|
+
return this.db.transaction(() => {
|
|
1053
|
+
this.db.prepare("DELETE FROM pairing_invites WHERE expires_at<=?").run(now - PAIRING_TTL_MS);
|
|
1054
|
+
if (this.db.prepare("SELECT 1 FROM clients WHERE id=? AND revoked=0").get(clientId)) fail("CLIENT_ALREADY_PAIRED", 409);
|
|
1055
|
+
if (this.db.prepare("SELECT count(*) AS n FROM clients WHERE revoked=0").get().n >= MAX_PAIRED_CLIENTS) fail("PAIRING_DEVICE_LIMIT", 429);
|
|
1056
|
+
this.db.prepare("UPDATE pairing_invites SET attempts_left=0 WHERE client_id=? AND consumed_request IS NULL").run(clientId);
|
|
1057
|
+
if (this.db.prepare("SELECT count(*) AS n FROM pairing_invites WHERE expires_at>? AND attempts_left>0 AND consumed_request IS NULL").get(now).n >= MAX_INVITES) fail("PAIRING_WINDOW_LIMIT", 429);
|
|
1058
|
+
this.db.prepare("INSERT INTO pairing_invites VALUES (?,?,?,?,?,?,?,?,NULL,NULL)").run(id2, clientId, sha(token), sha(code), now + PAIRING_TTL_MS, PAIRING_ATTEMPTS, canonical(ceiling), nameOf(options.name));
|
|
1059
|
+
return {
|
|
1060
|
+
format: "dsh-pairing-invite",
|
|
1061
|
+
version: 2,
|
|
1062
|
+
inviteId: id2,
|
|
1063
|
+
machineId: this.machineId,
|
|
1064
|
+
clientId,
|
|
1065
|
+
name: nameOf(options.name),
|
|
1066
|
+
endpoint: endpoint.origin,
|
|
1067
|
+
expiresAt: now + PAIRING_TTL_MS,
|
|
1068
|
+
token,
|
|
1069
|
+
code,
|
|
1070
|
+
transport: this.identity(),
|
|
1071
|
+
ceiling: structuredClone(ceiling)
|
|
1072
|
+
};
|
|
1073
|
+
}).immediate();
|
|
1074
|
+
}
|
|
1075
|
+
candidates() {
|
|
1076
|
+
const now = this.now(), transportId = keyId(this.transportKey.publicKey);
|
|
1077
|
+
for (const [id2, row] of this.replay) if (row.expiresAt <= now) this.replay.delete(id2);
|
|
1078
|
+
const invites = this.db.prepare("SELECT id,token_digest FROM pairing_invites WHERE expires_at>? AND attempts_left>0").all(now).map((row) => ({ id: { kind: "invite", id: row.id, digest: row.token_digest }, hash: row.token_digest }));
|
|
1079
|
+
const clients = this.db.prepare("SELECT id,digest,revision FROM clients WHERE revoked=0").all().map((row) => ({ id: { kind: "client", id: row.id, digest: row.digest, revision: row.revision }, hash: row.digest }));
|
|
1080
|
+
if (clients.length > MAX_PAIRED_CLIENTS) fail("PAIRING_DEVICE_LIMIT", 429);
|
|
1081
|
+
return [...invites, ...clients].map((row) => ({ id: row.id, psk: psk(credentialHashFromHex(row.hash), transportId) }));
|
|
1082
|
+
}
|
|
1083
|
+
validPeer(peer) {
|
|
1084
|
+
if (peer.kind === "client") {
|
|
1085
|
+
const client = this.db.prepare("SELECT digest,revision,revoked FROM clients WHERE id=?").get(peer.id);
|
|
1086
|
+
return Boolean(client && !client.revoked && client.revision === peer.revision && digestEqual(client.digest, peer.digest));
|
|
1087
|
+
}
|
|
1088
|
+
const invite = this.db.prepare("SELECT token_digest,expires_at,attempts_left FROM pairing_invites WHERE id=?").get(peer.id);
|
|
1089
|
+
return Boolean(invite && invite.expires_at > this.now() && invite.attempts_left > 0 && digestEqual(invite.token_digest, peer.digest));
|
|
1090
|
+
}
|
|
1091
|
+
redeem(peer, input) {
|
|
1092
|
+
if (peer.kind !== "invite" || !input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key2) => !["requestId", "code"].includes(key2)) || typeof input.requestId !== "string" || !/^[A-Za-z0-9_-]{16,128}$/.test(input.requestId) || typeof input.code !== "string" || !/^\d{6}$/.test(input.code)) fail("PAIRING_REQUEST_INVALID");
|
|
1093
|
+
const now = this.now();
|
|
1094
|
+
const decision = this.db.transaction(() => {
|
|
1095
|
+
const row = this.db.prepare("SELECT * FROM pairing_invites WHERE id=?").get(peer.id);
|
|
1096
|
+
if (!row || !digestEqual(row.token_digest, peer.digest) || row.expires_at <= now || row.attempts_left <= 0) return { error: "PAIRING_WINDOW_CLOSED" };
|
|
1097
|
+
if (!digestEqual(row.code_digest, sha(input.code))) {
|
|
1098
|
+
this.db.prepare("UPDATE pairing_invites SET attempts_left=attempts_left-1 WHERE id=?").run(peer.id);
|
|
1099
|
+
return { error: "PAIRING_CODE_INVALID" };
|
|
1100
|
+
}
|
|
1101
|
+
if (row.consumed_request !== null) {
|
|
1102
|
+
const cached = this.replay.get(row.id), client = this.db.prepare("SELECT digest,revoked FROM clients WHERE id=?").get(row.client_id);
|
|
1103
|
+
if (row.consumed_request !== input.requestId || !cached || cached.expiresAt <= now || cached.proof !== peer.digest || cached.requestId !== input.requestId) return { error: "PAIRING_ALREADY_REDEEMED" };
|
|
1104
|
+
if (!client || client.revoked || !digestEqual(client.digest, sha(cached.result.token))) return { error: "PAIRING_DEVICE_CHANGED" };
|
|
1105
|
+
return { result: structuredClone(cached.result), ceiling: JSON.parse(row.ceiling), recovered: true };
|
|
1106
|
+
}
|
|
1107
|
+
const result = this.commitClient(row.client_id, JSON.parse(row.ceiling));
|
|
1108
|
+
this.db.prepare("UPDATE pairing_invites SET consumed_request=?,consumed_at=? WHERE id=?").run(input.requestId, now, row.id);
|
|
1109
|
+
this.replay.set(row.id, { proof: peer.digest, requestId: input.requestId, expiresAt: row.expires_at, result: structuredClone(result) });
|
|
1110
|
+
return { result, ceiling: JSON.parse(row.ceiling), recovered: false };
|
|
1111
|
+
}).immediate();
|
|
1112
|
+
if (decision.error) fail(decision.error, 403);
|
|
1113
|
+
return { ...decision.result, transport: this.identity(), ceiling: decision.ceiling, recovered: decision.recovered };
|
|
1114
|
+
}
|
|
1115
|
+
invalidate(clientId) {
|
|
1116
|
+
this.db.prepare("UPDATE pairing_invites SET attempts_left=0 WHERE client_id=?").run(clientId);
|
|
1117
|
+
for (const [id2, row] of this.replay) if (row.result.clientId === clientId) this.replay.delete(id2);
|
|
1118
|
+
}
|
|
1119
|
+
close() {
|
|
1120
|
+
this.replay.clear();
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
// src/store.ts
|
|
90
1125
|
var Store = class {
|
|
91
1126
|
constructor(dataDir, Database, options = {}) {
|
|
92
1127
|
this.dataDir = dataDir;
|
|
@@ -131,7 +1166,7 @@ var Store = class {
|
|
|
131
1166
|
unlinkSync(this.lockPath);
|
|
132
1167
|
this.lockFd = openSync(this.lockPath, "wx", 384);
|
|
133
1168
|
}
|
|
134
|
-
writeFileSync(this.lockFd, JSON.stringify({ pid: process.pid, instanceId:
|
|
1169
|
+
writeFileSync(this.lockFd, JSON.stringify({ pid: process.pid, instanceId: randomUUID2() }));
|
|
135
1170
|
}
|
|
136
1171
|
this.db = new Driver(join(dataDir, "agent.sqlite"));
|
|
137
1172
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -145,8 +1180,9 @@ var Store = class {
|
|
|
145
1180
|
CREATE TABLE IF NOT EXISTS artifacts(id TEXT PRIMARY KEY,context TEXT NOT NULL,hash TEXT NOT NULL,size INTEGER NOT NULL,media TEXT NOT NULL);
|
|
146
1181
|
CREATE TABLE IF NOT EXISTS worktrees(id TEXT PRIMARY KEY,context TEXT NOT NULL,root TEXT NOT NULL,path TEXT NOT NULL,head TEXT NOT NULL,source TEXT NOT NULL);
|
|
147
1182
|
CREATE TABLE IF NOT EXISTS audit(id INTEGER PRIMARY KEY,time INTEGER NOT NULL,code TEXT NOT NULL,actor TEXT,operation TEXT);`);
|
|
148
|
-
this.db.prepare("INSERT OR IGNORE INTO meta VALUES (?,?)").run("machineId", `machine-${
|
|
1183
|
+
this.db.prepare("INSERT OR IGNORE INTO meta VALUES (?,?)").run("machineId", `machine-${randomUUID2()}`);
|
|
149
1184
|
this.machineId = this.db.prepare("SELECT value FROM meta WHERE key=?").get("machineId").value;
|
|
1185
|
+
this.pairing = new SecurePairing(this.db, this.machineId, (id2, ceiling) => this.commitClient(id2, ceiling));
|
|
150
1186
|
if (!options.admin) this.db.prepare("UPDATE operations SET status='unknown',error='OUTCOME_UNKNOWN' WHERE status IN ('admitted','running','cancel_pending')").run();
|
|
151
1187
|
if (!options.admin) this.db.prepare("DELETE FROM grants").run();
|
|
152
1188
|
} catch (e) {
|
|
@@ -160,6 +1196,7 @@ var Store = class {
|
|
|
160
1196
|
}
|
|
161
1197
|
}
|
|
162
1198
|
dataDir;
|
|
1199
|
+
pairing;
|
|
163
1200
|
db;
|
|
164
1201
|
machineId;
|
|
165
1202
|
lockFd;
|
|
@@ -196,7 +1233,7 @@ var Store = class {
|
|
|
196
1233
|
}
|
|
197
1234
|
}
|
|
198
1235
|
}
|
|
199
|
-
|
|
1236
|
+
approvedCeiling(id2, ceiling) {
|
|
200
1237
|
identifier(id2);
|
|
201
1238
|
if (!ceiling || ceiling.hostManagedBots !== void 0 && typeof ceiling.hostManagedBots !== "boolean" || !Array.isArray(ceiling.botIds ?? []) || !Array.isArray(ceiling.roots) || !ceiling.roots.length || !Array.isArray(ceiling.tools) || !ceiling.tools.length || ceiling.tools.some((x) => !TOOL_NAMES.includes(x))) fail("INVALID_CEILING");
|
|
202
1239
|
const managed = ceiling.hostManagedBots === true, botIds = ceiling.botIds ?? [];
|
|
@@ -205,9 +1242,25 @@ var Store = class {
|
|
|
205
1242
|
botIds.forEach(identifier);
|
|
206
1243
|
ceiling.roots.forEach(identifier);
|
|
207
1244
|
const saved = { botIds, roots: ceiling.roots, tools: ceiling.tools, ...managed ? { hostManagedBots: true } : {} };
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
1245
|
+
return saved;
|
|
1246
|
+
}
|
|
1247
|
+
commitClient(id2, ceiling) {
|
|
1248
|
+
return this.db.transaction(() => {
|
|
1249
|
+
const previous = this.db.prepare("SELECT revoked FROM clients WHERE id=?").get(id2);
|
|
1250
|
+
if (previous && !previous.revoked) fail("CLIENT_ALREADY_PAIRED", 409);
|
|
1251
|
+
if (this.db.prepare("SELECT count(*) AS n FROM clients WHERE revoked=0").get().n >= MAX_PAIRED_CLIENTS) fail("PAIRING_DEVICE_LIMIT", 429);
|
|
1252
|
+
const token = secret();
|
|
1253
|
+
if (previous) this.db.prepare("UPDATE clients SET digest=?,revision=revision+1,revoked=0,ceiling=? WHERE id=? AND revoked=1").run(sha(token), canonical(ceiling), id2);
|
|
1254
|
+
else this.db.prepare("INSERT INTO clients VALUES (?,?,1,0,?)").run(id2, sha(token), canonical(ceiling));
|
|
1255
|
+
return { clientId: id2, token, machineId: this.machineId, secretRef: `remote-agent/${this.machineId}/${id2}`, hostManagedBots: ceiling.hostManagedBots === true };
|
|
1256
|
+
}).immediate();
|
|
1257
|
+
}
|
|
1258
|
+
pair(id2, ceiling) {
|
|
1259
|
+
return this.commitClient(id2, this.approvedCeiling(id2, ceiling));
|
|
1260
|
+
}
|
|
1261
|
+
invite(id2, ceiling, options) {
|
|
1262
|
+
if (!this.admin) fail("ADMIN_REQUIRED", 403);
|
|
1263
|
+
return this.pairing.create(id2, this.approvedCeiling(id2, ceiling), options);
|
|
211
1264
|
}
|
|
212
1265
|
client(token) {
|
|
213
1266
|
if (typeof token !== "string" || token.length > 256) fail("AUTH_INVALID", 401);
|
|
@@ -217,12 +1270,20 @@ var Store = class {
|
|
|
217
1270
|
return row;
|
|
218
1271
|
}
|
|
219
1272
|
rotate(id2) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
1273
|
+
return this.db.transaction(() => {
|
|
1274
|
+
identifier(id2);
|
|
1275
|
+
const row = this.db.prepare("SELECT revoked FROM clients WHERE id=?").get(id2);
|
|
1276
|
+
if (!row) fail("AUTH_INVALID", 401);
|
|
1277
|
+
if (row.revoked && this.db.prepare("SELECT count(*) AS n FROM clients WHERE revoked=0").get().n >= MAX_PAIRED_CLIENTS) fail("PAIRING_DEVICE_LIMIT", 429);
|
|
1278
|
+
const token = secret();
|
|
1279
|
+
this.db.prepare("UPDATE clients SET digest=?,revision=revision+1,revoked=0 WHERE id=?").run(sha(token), id2);
|
|
1280
|
+
this.pairing.invalidate(id2);
|
|
1281
|
+
return { clientId: id2, token };
|
|
1282
|
+
}).immediate();
|
|
223
1283
|
}
|
|
224
1284
|
revoke(id2) {
|
|
225
1285
|
this.db.prepare("UPDATE clients SET revoked=1,revision=revision+1 WHERE id=?").run(identifier(id2));
|
|
1286
|
+
this.pairing.invalidate(id2);
|
|
226
1287
|
}
|
|
227
1288
|
currentClient(client) {
|
|
228
1289
|
const row = this.db.prepare("SELECT * FROM clients WHERE id=?").get(client?.id);
|
|
@@ -238,7 +1299,7 @@ var Store = class {
|
|
|
238
1299
|
if (!Number.isSafeInteger(ttl) || ttl < 1e3 || ttl > 3e5) fail("INVALID_ARGUMENT");
|
|
239
1300
|
this.db.prepare("DELETE FROM grants WHERE json_extract(context,'$.expiresAt')<=?").run(Date.now());
|
|
240
1301
|
if (this.db.prepare("SELECT count(*) AS count FROM grants").get().count >= 4096) fail("GRANT_LIMIT", 429);
|
|
241
|
-
const context = { clientId: client.id, botId: request.botId, taskId: request.taskId, attemptId: request.attemptId, roots: request.roots, tools: request.tools, expiresAt: Date.now() + ttl, revision: client.revision, grantId:
|
|
1302
|
+
const context = { clientId: client.id, botId: request.botId, taskId: request.taskId, attemptId: request.attemptId, roots: request.roots, tools: request.tools, expiresAt: Date.now() + ttl, revision: client.revision, grantId: randomUUID2() };
|
|
242
1303
|
const token = secret();
|
|
243
1304
|
this.db.prepare("INSERT INTO grants VALUES (?,?)").run(sha(token), canonical(context));
|
|
244
1305
|
return { grantToken: token, machineId: this.machineId, agentApiMajor: 1, ...context };
|
|
@@ -303,6 +1364,7 @@ var Store = class {
|
|
|
303
1364
|
close() {
|
|
304
1365
|
if (this.closed) return;
|
|
305
1366
|
this.closed = true;
|
|
1367
|
+
this.pairing?.close();
|
|
306
1368
|
try {
|
|
307
1369
|
this.db.close();
|
|
308
1370
|
} finally {
|
|
@@ -466,7 +1528,7 @@ var Roots = class {
|
|
|
466
1528
|
};
|
|
467
1529
|
|
|
468
1530
|
// src/browser.ts
|
|
469
|
-
import { randomUUID as
|
|
1531
|
+
import { randomUUID as randomUUID3, createHash as createHash3 } from "node:crypto";
|
|
470
1532
|
import { access, mkdir, readFile, stat as stat2, unlink } from "node:fs/promises";
|
|
471
1533
|
import { join as join2 } from "node:path";
|
|
472
1534
|
var BrowserTools = class {
|
|
@@ -515,7 +1577,7 @@ var BrowserTools = class {
|
|
|
515
1577
|
if (a.profileId !== void 0) {
|
|
516
1578
|
const root = this.profileRoots[a.profileId];
|
|
517
1579
|
if (!root) fail("BROWSER_PROFILE_DENIED", 403);
|
|
518
|
-
const owner = JSON.stringify([c.clientId, c.botId, a.profileId]), key2 =
|
|
1580
|
+
const owner = JSON.stringify([c.clientId, c.botId, a.profileId]), key2 = createHash3("sha256").update(owner).digest("hex");
|
|
519
1581
|
try {
|
|
520
1582
|
context = this.profiles.get(key2);
|
|
521
1583
|
if (!context) {
|
|
@@ -547,9 +1609,9 @@ var BrowserTools = class {
|
|
|
547
1609
|
});
|
|
548
1610
|
const page = await context.newPage();
|
|
549
1611
|
page.on("popup", (p) => void p.close());
|
|
550
|
-
const id2 = `browser-${
|
|
1612
|
+
const id2 = `browser-${randomUUID3()}`, downloads = /* @__PURE__ */ new Map();
|
|
551
1613
|
page.on("download", (download2) => {
|
|
552
|
-
const downloadId2 = `download-${
|
|
1614
|
+
const downloadId2 = `download-${randomUUID3()}`;
|
|
553
1615
|
downloads.set(downloadId2, { download: download2, path: download2.path(), suggestedFilename: download2.suggestedFilename() });
|
|
554
1616
|
});
|
|
555
1617
|
this.sessions.set(id2, { context, page, owner: JSON.stringify([c.clientId, c.botId, c.taskId, c.attemptId]), revision: null, persistent, profileId, downloads });
|
|
@@ -598,7 +1660,7 @@ var BrowserTools = class {
|
|
|
598
1660
|
return { completed: true, url: session.page.url(), result: value ?? null };
|
|
599
1661
|
}
|
|
600
1662
|
if (name === "browser_snapshot") {
|
|
601
|
-
session.revision = `snapshot-${
|
|
1663
|
+
session.revision = `snapshot-${randomUUID3()}`;
|
|
602
1664
|
const nodes = await session.page.locator('a,button,input,textarea,select,[role="button"]').evaluateAll((elements) => elements.slice(0, 300).map((el, i) => {
|
|
603
1665
|
el.setAttribute("data-dsh-agent-ref", String(i));
|
|
604
1666
|
return { ref: String(i), tag: el.tagName.toLowerCase(), text: (el.innerText || el.getAttribute("aria-label") || "").slice(0, 300) };
|
|
@@ -642,7 +1704,7 @@ var BrowserTools = class {
|
|
|
642
1704
|
// src/desktop.ts
|
|
643
1705
|
import net from "node:net";
|
|
644
1706
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
645
|
-
import { randomUUID as
|
|
1707
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
646
1708
|
var error = (code, possiblyApplied = false, stopConfirmed = false) => Object.assign(new AgentError(code, 409), { possiblyApplied, stopConfirmed });
|
|
647
1709
|
var DesktopTools = class {
|
|
648
1710
|
constructor(config, options = {}) {
|
|
@@ -651,7 +1713,7 @@ var DesktopTools = class {
|
|
|
651
1713
|
}
|
|
652
1714
|
config;
|
|
653
1715
|
options;
|
|
654
|
-
nonce =
|
|
1716
|
+
nonce = randomUUID4();
|
|
655
1717
|
owners = /* @__PURE__ */ new Map();
|
|
656
1718
|
closing = new AbortController();
|
|
657
1719
|
active = /* @__PURE__ */ new Set();
|
|
@@ -674,7 +1736,7 @@ var DesktopTools = class {
|
|
|
674
1736
|
async rpc(control2, packet, beforeWrite = () => {
|
|
675
1737
|
}) {
|
|
676
1738
|
const name = this.config.pipeName + (control2 ? "-control" : "");
|
|
677
|
-
return new Promise((
|
|
1739
|
+
return new Promise((resolve6, reject) => {
|
|
678
1740
|
const socket = (this.options.connect ?? net.createConnection)(`\\\\.\\pipe\\${name}`), chunks = [];
|
|
679
1741
|
let bytes = 0, sent = false, settled = false;
|
|
680
1742
|
const finish = (value, e) => {
|
|
@@ -683,7 +1745,7 @@ var DesktopTools = class {
|
|
|
683
1745
|
clearTimeout(timer);
|
|
684
1746
|
socket.destroy();
|
|
685
1747
|
if (e) reject(e);
|
|
686
|
-
else
|
|
1748
|
+
else resolve6(value);
|
|
687
1749
|
};
|
|
688
1750
|
const timer = setTimeout(() => finish(null, error(sent ? "OUTCOME_UNKNOWN" : "DESKTOP_UNAVAILABLE", !control2 && sent)), this.options.timeoutMs ?? 1e4);
|
|
689
1751
|
socket.once("connect", () => {
|
|
@@ -826,7 +1888,7 @@ var DesktopTools = class {
|
|
|
826
1888
|
import net2 from "node:net";
|
|
827
1889
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
828
1890
|
import { EventEmitter } from "node:events";
|
|
829
|
-
import { randomUUID as
|
|
1891
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
830
1892
|
var inputTools = ["desktop_click", "desktop_type", "desktop_key", "desktop_scroll"];
|
|
831
1893
|
var unknown = () => Object.assign(new AgentError("OUTCOME_UNKNOWN", 409), { possiblyApplied: true, stopConfirmed: false });
|
|
832
1894
|
var safeOrigin = (value) => {
|
|
@@ -942,13 +2004,13 @@ var NativeDesktopPipe = class _NativeDesktopPipe extends EventEmitter {
|
|
|
942
2004
|
if (this.ended) return Promise.reject(unknown());
|
|
943
2005
|
if (this.pending.size >= 64) return Promise.reject(new AgentError("DESKTOP_INPUT_LIMIT", 429));
|
|
944
2006
|
const id2 = ++this.nextId;
|
|
945
|
-
return new Promise((
|
|
2007
|
+
return new Promise((resolve6, reject) => {
|
|
946
2008
|
const timer = setTimeout(() => {
|
|
947
2009
|
this.pending.delete(id2);
|
|
948
2010
|
reject(unknown());
|
|
949
2011
|
this.finish(unknown());
|
|
950
2012
|
}, 6500);
|
|
951
|
-
this.pending.set(id2, { resolve:
|
|
2013
|
+
this.pending.set(id2, { resolve: resolve6, reject, timer });
|
|
952
2014
|
try {
|
|
953
2015
|
this.write({ ...message, id: id2 });
|
|
954
2016
|
} catch (e) {
|
|
@@ -996,7 +2058,7 @@ var DesktopSessions = class {
|
|
|
996
2058
|
forceKeyframes = /* @__PURE__ */ new Set();
|
|
997
2059
|
flights = /* @__PURE__ */ new WeakMap();
|
|
998
2060
|
sessions = /* @__PURE__ */ new Map();
|
|
999
|
-
nonce =
|
|
2061
|
+
nonce = randomUUID5();
|
|
1000
2062
|
closing = false;
|
|
1001
2063
|
timer;
|
|
1002
2064
|
sockets = /* @__PURE__ */ new Set();
|
|
@@ -1034,7 +2096,7 @@ var DesktopSessions = class {
|
|
|
1034
2096
|
if (mode === "control" && !controlAllowed) fail("SCOPE_DENIED", 403);
|
|
1035
2097
|
await this.sweep();
|
|
1036
2098
|
if (this.opening + [...this.sessions.values()].filter((s) => !s.closed).length >= 4 || this.sessions.size >= 256) fail("DESKTOP_SESSION_LIMIT", 429);
|
|
1037
|
-
const id2 = `desktop-${
|
|
2099
|
+
const id2 = `desktop-${randomUUID5()}`, token = secret();
|
|
1038
2100
|
this.opening++;
|
|
1039
2101
|
let native;
|
|
1040
2102
|
try {
|
|
@@ -1097,6 +2159,13 @@ var DesktopSessions = class {
|
|
|
1097
2159
|
const row = this.own(c, request?.sessionId);
|
|
1098
2160
|
return this.stop(row, "SESSION_CLOSED");
|
|
1099
2161
|
}
|
|
2162
|
+
async closeClientSession(clientId, request) {
|
|
2163
|
+
if (!request || typeof request !== "object" || Array.isArray(request) || Object.keys(request).some((key2) => !["sessionId", "botId", "taskId", "attemptId"].includes(key2))) fail("INVALID_ARGUMENT");
|
|
2164
|
+
for (const key2 of ["sessionId", "botId", "taskId", "attemptId"]) identifier(request[key2]);
|
|
2165
|
+
const row = this.sessions.get(request.sessionId);
|
|
2166
|
+
if (!row || row.context.clientId !== clientId || row.context.botId !== request.botId || row.context.taskId !== request.taskId || row.context.attemptId !== request.attemptId) fail("SCOPE_DENIED", 403);
|
|
2167
|
+
return this.stop(row, "SESSION_CLOSED");
|
|
2168
|
+
}
|
|
1100
2169
|
hasOrigin(origin) {
|
|
1101
2170
|
return [...this.sessions.values()].some((row) => !row.closed && row.origin === origin && row.context.expiresAt > Date.now());
|
|
1102
2171
|
}
|
|
@@ -1212,7 +2281,7 @@ var DesktopSessions = class {
|
|
|
1212
2281
|
fail("INVALID_ARGUMENT");
|
|
1213
2282
|
}
|
|
1214
2283
|
/** Called only after the HTTP server validates Host/network/TLS on upgrade. */
|
|
1215
|
-
accept(socket, origin, allowedOrigins = []) {
|
|
2284
|
+
accept(socket, origin, allowedOrigins = [], peerClientId) {
|
|
1216
2285
|
this.sockets.add(socket);
|
|
1217
2286
|
let row, authenticating = false, alive2 = true;
|
|
1218
2287
|
const authTimer = setTimeout(() => socket.close(1008, "Desktop authentication required"), this.options.authTimeoutMs ?? 5e3);
|
|
@@ -1278,6 +2347,7 @@ var DesktopSessions = class {
|
|
|
1278
2347
|
try {
|
|
1279
2348
|
const candidate = this.sessions.get(message.sessionId);
|
|
1280
2349
|
if (message.type !== "auth" || typeof message.token !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(message.token) || !candidate || !matches(message.token, candidate.digest)) fail("AUTH_INVALID", 401);
|
|
2350
|
+
if (peerClientId !== void 0 && candidate.context.clientId !== peerClientId) fail("AUTH_INVALID", 401);
|
|
1281
2351
|
if (candidate.closed || candidate.used) fail("DESKTOP_SESSION_CLOSED", 409);
|
|
1282
2352
|
this.check(candidate.context);
|
|
1283
2353
|
if (origin !== void 0 && (candidate.origin ? origin !== candidate.origin : !allowedOrigins.includes(origin))) fail("ORIGIN_DENIED", 403);
|
|
@@ -1381,7 +2451,7 @@ var DesktopSessions = class {
|
|
|
1381
2451
|
}
|
|
1382
2452
|
stop(row, reason) {
|
|
1383
2453
|
this.forceKeyframes.delete(row);
|
|
1384
|
-
if (row.stop) return row.stop;
|
|
2454
|
+
if (row.stop) return row.stopped ? Promise.resolve({ stopConfirmed: true }) : row.stop;
|
|
1385
2455
|
row.closed = true;
|
|
1386
2456
|
row.closedAt = Date.now();
|
|
1387
2457
|
clearTimeout(row.frameTimer);
|
|
@@ -1432,7 +2502,7 @@ import { execFile, spawn } from "node:child_process";
|
|
|
1432
2502
|
import { promisify } from "node:util";
|
|
1433
2503
|
import { mkdir as mkdir2, open as open2, readFile as readFile4, lstat as lstat2 } from "node:fs/promises";
|
|
1434
2504
|
import { isAbsolute, join as join3 } from "node:path";
|
|
1435
|
-
import { randomUUID as
|
|
2505
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
1436
2506
|
|
|
1437
2507
|
// src/container-supervisor.mjs
|
|
1438
2508
|
async function runContainerSupervisor() {
|
|
@@ -1596,9 +2666,9 @@ var ContainerCommands = class {
|
|
|
1596
2666
|
child.kill("SIGKILL");
|
|
1597
2667
|
}, 15e3);
|
|
1598
2668
|
try {
|
|
1599
|
-
const code = await new Promise((
|
|
2669
|
+
const code = await new Promise((resolve6, reject) => {
|
|
1600
2670
|
child.once("error", reject);
|
|
1601
|
-
child.once("close",
|
|
2671
|
+
child.once("close", resolve6);
|
|
1602
2672
|
});
|
|
1603
2673
|
if (failed || code !== 0) fail("CONTAINER_OUTPUT_UNAVAILABLE", 503);
|
|
1604
2674
|
return { stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr) };
|
|
@@ -1727,7 +2797,7 @@ var ContainerCommands = class {
|
|
|
1727
2797
|
}
|
|
1728
2798
|
}
|
|
1729
2799
|
async run({ id: id2, c, rootId, workspace, workspaceIdentity, executable, argv, timeout, validate, validateWorkspace, shouldCancel, artifact }) {
|
|
1730
|
-
const name = "dsh-worker-" + sha(canonical({ machine: this.store.machineId, id: id2 })).slice(0, 40), nonce =
|
|
2800
|
+
const name = "dsh-worker-" + sha(canonical({ machine: this.store.machineId, id: id2 })).slice(0, 40), nonce = randomUUID6(), digest2 = sha(canonical({ rootId, workspace, workspaceIdentity, executable, argv, image: this.config.image }));
|
|
1731
2801
|
const deadlineAt = Math.min(c.expiresAt, Date.now() + timeout), supervisorFile = await this.supervisor();
|
|
1732
2802
|
const args2 = containerArguments({ name, nonce, machineId: this.store.machineId, workspace, workspaceIdentity, config: this.config, executable, argv, deadlineAt, supervisorFile });
|
|
1733
2803
|
this.store.db.prepare("INSERT INTO container_workers(operation_id,name,nonce,context,root_id,workspace,input_digest,phase,cid,reason,workspace_identity) VALUES (?,?,?,?,?,?,?,'prepared',NULL,NULL,?)").run(id2, name, nonce, canonical(c), rootId, workspace, digest2, workspaceIdentity);
|
|
@@ -1789,11 +2859,11 @@ var ContainerCommands = class {
|
|
|
1789
2859
|
}
|
|
1790
2860
|
this.store.db.prepare("UPDATE container_workers SET phase='starting' WHERE operation_id=?").run(id2);
|
|
1791
2861
|
child = spawn(this.config.dockerExecutable, ["container", "start", "-a", cid], { env: this.dockerEnv, detached: true, stdio: ["ignore", "ignore", "ignore"], windowsHide: true });
|
|
1792
|
-
await new Promise((
|
|
2862
|
+
await new Promise((resolve6, reject) => {
|
|
1793
2863
|
child.once("error", reject);
|
|
1794
2864
|
child.once("close", () => {
|
|
1795
2865
|
closed2 = true;
|
|
1796
|
-
|
|
2866
|
+
resolve6();
|
|
1797
2867
|
});
|
|
1798
2868
|
});
|
|
1799
2869
|
await cancelling;
|
|
@@ -2185,7 +3255,7 @@ function reconcileRootsGeneration(db, generation) {
|
|
|
2185
3255
|
import { constants as constants3 } from "node:fs";
|
|
2186
3256
|
import { open as open4, lstat as lstat4, stat as stat4, realpath as realpath3, link, unlink as unlink3 } from "node:fs/promises";
|
|
2187
3257
|
import path3 from "node:path";
|
|
2188
|
-
import { randomBytes as
|
|
3258
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
2189
3259
|
var PINNED_WRITE_MAX_BYTES = 1048576;
|
|
2190
3260
|
var PINNED_WRITE_MAX_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
2191
3261
|
var PINNED_WRITE_MAX_PATH_BYTES = 4096;
|
|
@@ -2424,7 +3494,7 @@ async function pinnedCreateNewFile(request) {
|
|
|
2424
3494
|
try {
|
|
2425
3495
|
await pinned.verifyHostPath(directoryPath);
|
|
2426
3496
|
if (await pinned.lstatChild(targetName)) failWrite("TARGET_EXISTS", 409, { target: targetPath });
|
|
2427
|
-
tempName = `${PINNED_WRITE_TEMP_PREFIX}${
|
|
3497
|
+
tempName = `${PINNED_WRITE_TEMP_PREFIX}${randomBytes3(16).toString("hex")}`;
|
|
2428
3498
|
const temp = await open4(pinned.childPath(tempName), TEMP_FLAGS, 384).catch((e) => {
|
|
2429
3499
|
const code = fsCode(e);
|
|
2430
3500
|
if (code === "EEXIST") failWrite("PINNED_WRITE_FAILED", 500, { reason: "temp-name-collision" });
|
|
@@ -2513,15 +3583,15 @@ async function pinnedCreateNewFile(request) {
|
|
|
2513
3583
|
}
|
|
2514
3584
|
|
|
2515
3585
|
// src/cli-task.ts
|
|
2516
|
-
import { readFile as
|
|
3586
|
+
import { readFile as readFile8, stat as stat6, writeFile as writeFile4 } from "node:fs/promises";
|
|
2517
3587
|
import { isAbsolute as isAbsolute6, join as join7, relative, resolve as resolve2 } from "node:path";
|
|
2518
3588
|
|
|
2519
3589
|
// ../packages/cli-runtime/src/agent-codex-runtime.mjs
|
|
2520
|
-
import { randomBytes as
|
|
3590
|
+
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
2521
3591
|
|
|
2522
3592
|
// ../packages/cli-runtime/src/codex-session-core.mjs
|
|
2523
|
-
import { createHash as
|
|
2524
|
-
import { readFile as
|
|
3593
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
3594
|
+
import { readFile as readFile7, realpath as realpath6, stat as stat5, mkdir as mkdir5, writeFile as writeFile3 } from "node:fs/promises";
|
|
2525
3595
|
import { spawn as spawn3 } from "node:child_process";
|
|
2526
3596
|
import { isAbsolute as isAbsolute5, join as join6 } from "node:path";
|
|
2527
3597
|
|
|
@@ -2529,11 +3599,128 @@ import { isAbsolute as isAbsolute5, join as join6 } from "node:path";
|
|
|
2529
3599
|
import { spawn as spawn2 } from "node:child_process";
|
|
2530
3600
|
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
2531
3601
|
import { StringDecoder } from "node:string_decoder";
|
|
2532
|
-
import { access as access2, realpath as realpath4 } from "node:fs/promises";
|
|
2533
|
-
import {
|
|
3602
|
+
import { access as access2, lstat as lstat5, readFile as readFile6, readdir as readdir2, realpath as realpath4, rmdir, unlink as unlink4 } from "node:fs/promises";
|
|
3603
|
+
import { lstatSync as lstatSync2, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync2, realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3604
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
3605
|
+
import { tmpdir } from "node:os";
|
|
3606
|
+
import { basename, dirname, isAbsolute as isAbsolute3, join as join4 } from "node:path";
|
|
2534
3607
|
import { fileURLToPath } from "node:url";
|
|
3608
|
+
|
|
3609
|
+
// ../packages/cli-runtime/src/native-cli-output.mjs
|
|
3610
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3611
|
+
var FRAME = 262144;
|
|
3612
|
+
var RAW_FRAME = 18 * 1024 * 1024;
|
|
3613
|
+
var IMAGE = 6 * 1024 * 1024;
|
|
3614
|
+
var fail2 = () => {
|
|
3615
|
+
throw new Error("CLI_FRAME_LIMIT");
|
|
3616
|
+
};
|
|
3617
|
+
function receipt(frame, rawBytes) {
|
|
3618
|
+
const results = frame?.type === "user" && frame.message?.content;
|
|
3619
|
+
const toolResult = Array.isArray(results) && results.length > 0 && results.every((block) => block?.type === "tool_result" && typeof block.tool_use_id === "string");
|
|
3620
|
+
if (!toolResult) {
|
|
3621
|
+
if (rawBytes > FRAME) fail2();
|
|
3622
|
+
return frame;
|
|
3623
|
+
}
|
|
3624
|
+
let imageBytes = 0, images = 0, nodes = 0;
|
|
3625
|
+
const visit = (value, depth = 0) => {
|
|
3626
|
+
if (++nodes > 3e4 || depth > 24) fail2();
|
|
3627
|
+
if (!value || typeof value !== "object") return value;
|
|
3628
|
+
if (value.type === "image") {
|
|
3629
|
+
const source = value.source;
|
|
3630
|
+
const data = source?.type === "base64" ? source.data : value.data;
|
|
3631
|
+
const mediaType = source?.type === "base64" ? source.media_type : value.mimeType;
|
|
3632
|
+
if (typeof data !== "string" || data.length > IMAGE / 3 * 4 || !["image/png", "image/jpeg", "image/webp"].includes(mediaType)) fail2();
|
|
3633
|
+
const bytes = Buffer.from(data, "base64");
|
|
3634
|
+
if (!bytes.length || bytes.length > IMAGE || bytes.toString("base64") !== data || ++images > 32 || (imageBytes += bytes.length) > IMAGE * 2) fail2();
|
|
3635
|
+
return { type: "image", mediaType, bytes: bytes.length, sha256: createHash4("sha256").update(bytes).digest("hex") };
|
|
3636
|
+
}
|
|
3637
|
+
if (Array.isArray(value)) return value.map((item) => visit(item, depth + 1));
|
|
3638
|
+
return Object.fromEntries(Object.entries(value).map(([key2, item]) => [key2, visit(item, depth + 1)]));
|
|
3639
|
+
};
|
|
3640
|
+
const result = visit(frame);
|
|
3641
|
+
if (rawBytes > FRAME && !images || Buffer.byteLength(JSON.stringify(result)) > FRAME) fail2();
|
|
3642
|
+
return result;
|
|
3643
|
+
}
|
|
3644
|
+
var NativeCliOutput = class {
|
|
3645
|
+
#buffer = "";
|
|
3646
|
+
#bytes = 0;
|
|
3647
|
+
push(text) {
|
|
3648
|
+
this.#buffer += text;
|
|
3649
|
+
const frames = [];
|
|
3650
|
+
let end;
|
|
3651
|
+
while ((end = this.#buffer.indexOf("\n")) >= 0) {
|
|
3652
|
+
const line = this.#buffer.slice(0, end);
|
|
3653
|
+
this.#buffer = this.#buffer.slice(end + 1);
|
|
3654
|
+
const rawBytes = Buffer.byteLength(line);
|
|
3655
|
+
if (rawBytes > RAW_FRAME) fail2();
|
|
3656
|
+
if (!line.trim()) continue;
|
|
3657
|
+
const frame = receipt(JSON.parse(line), rawBytes);
|
|
3658
|
+
this.#bytes += Buffer.byteLength(JSON.stringify(frame));
|
|
3659
|
+
if (this.#bytes > 8 * 1024 * 1024) fail2();
|
|
3660
|
+
frames.push(frame);
|
|
3661
|
+
}
|
|
3662
|
+
if (Buffer.byteLength(this.#buffer) > RAW_FRAME) fail2();
|
|
3663
|
+
return frames;
|
|
3664
|
+
}
|
|
3665
|
+
};
|
|
3666
|
+
|
|
3667
|
+
// ../packages/cli-runtime/src/native-process.mjs
|
|
2535
3668
|
var HERE = fileURLToPath(new URL(".", import.meta.url));
|
|
2536
3669
|
var MAX_FRAME = 262144;
|
|
3670
|
+
var COMPILER_ROOT = "DSH_NATIVE_COMPILER_ROOT";
|
|
3671
|
+
var COMPILER_SCRATCH = "DSH_NATIVE_COMPILER_SCRATCH";
|
|
3672
|
+
var inheritedCompilerRoot = process.platform === "win32" ? process.env[COMPILER_ROOT] : void 0;
|
|
3673
|
+
var hostTemp = process.platform === "win32" ? tmpdir() : null;
|
|
3674
|
+
var compilerRoot = null;
|
|
3675
|
+
var compilerFailure = () => Object.assign(new Error("NATIVE_COMPILER_SCRATCH_UNAVAILABLE"), { code: "NATIVE_COMPILER_SCRATCH_UNAVAILABLE" });
|
|
3676
|
+
var sameDirectory = (info, expected) => info.isDirectory() && !info.isSymbolicLink() && info.dev === expected.dev && info.ino === expected.ino;
|
|
3677
|
+
var rootStamp = (path4) => `dsh-native-compiler-v1:${basename(path4)}
|
|
3678
|
+
`;
|
|
3679
|
+
function compilerDirectory() {
|
|
3680
|
+
try {
|
|
3681
|
+
if (!compilerRoot) {
|
|
3682
|
+
if (inheritedCompilerRoot !== void 0) {
|
|
3683
|
+
if (!isAbsolute3(inheritedCompilerRoot) || inheritedCompilerRoot.length > 140 || !/^dshc-[a-f0-9]{16}$/.test(basename(inheritedCompilerRoot))) throw compilerFailure();
|
|
3684
|
+
const info2 = lstatSync2(inheritedCompilerRoot, { bigint: true });
|
|
3685
|
+
if (!info2.isDirectory() || info2.isSymbolicLink() || realpathSync(inheritedCompilerRoot) !== inheritedCompilerRoot || readFileSync2(join4(inheritedCompilerRoot, ".dsh-root"), "utf8") !== rootStamp(inheritedCompilerRoot)) throw compilerFailure();
|
|
3686
|
+
compilerRoot = { path: inheritedCompilerRoot, info: info2 };
|
|
3687
|
+
} else {
|
|
3688
|
+
const parent = realpathSync(hostTemp);
|
|
3689
|
+
const path5 = join4(parent, `dshc-${randomBytes4(8).toString("hex")}`);
|
|
3690
|
+
if (path5.length > 140) throw compilerFailure();
|
|
3691
|
+
mkdirSync2(path5);
|
|
3692
|
+
writeFileSync2(join4(path5, ".dsh-root"), rootStamp(path5), { flag: "wx" });
|
|
3693
|
+
compilerRoot = { path: path5, info: lstatSync2(path5, { bigint: true }) };
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
if (!sameDirectory(lstatSync2(compilerRoot.path, { bigint: true }), compilerRoot.info) || realpathSync(compilerRoot.path) !== compilerRoot.path) throw compilerFailure();
|
|
3697
|
+
const path4 = mkdtempSync(join4(compilerRoot.path, "c-"));
|
|
3698
|
+
const info = lstatSync2(path4, { bigint: true }), owner = randomBytes4(16).toString("hex");
|
|
3699
|
+
writeFileSync2(join4(path4, ".dsh-owner"), owner, { flag: "wx" });
|
|
3700
|
+
const root = compilerRoot;
|
|
3701
|
+
const cleanup = async () => {
|
|
3702
|
+
try {
|
|
3703
|
+
if (dirname(path4) !== root.path || !/^c-[A-Za-z0-9]{6}$/.test(basename(path4))) return false;
|
|
3704
|
+
if (!sameDirectory(await lstat5(root.path, { bigint: true }), root.info) || await realpath4(root.path) !== root.path) return false;
|
|
3705
|
+
if (!sameDirectory(await lstat5(path4, { bigint: true }), info) || await realpath4(path4) !== path4 || await readFile6(join4(path4, ".dsh-owner"), "utf8") !== owner) return false;
|
|
3706
|
+
const names = await readdir2(path4);
|
|
3707
|
+
if (names.length > 128) return false;
|
|
3708
|
+
for (const name of names) {
|
|
3709
|
+
const entry = await lstat5(join4(path4, name));
|
|
3710
|
+
if (!entry.isFile() || entry.isSymbolicLink()) return false;
|
|
3711
|
+
}
|
|
3712
|
+
for (const name of names) await unlink4(join4(path4, name));
|
|
3713
|
+
await rmdir(path4);
|
|
3714
|
+
return true;
|
|
3715
|
+
} catch {
|
|
3716
|
+
return false;
|
|
3717
|
+
}
|
|
3718
|
+
};
|
|
3719
|
+
return { root: root.path, path: path4, cleanup };
|
|
3720
|
+
} catch {
|
|
3721
|
+
throw compilerFailure();
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
2537
3724
|
function privateEnvironment(home, source = process.env) {
|
|
2538
3725
|
const env2 = {
|
|
2539
3726
|
HOME: home,
|
|
@@ -2553,6 +3740,7 @@ function privateEnvironment(home, source = process.env) {
|
|
|
2553
3740
|
};
|
|
2554
3741
|
for (const key2 of ["SystemRoot", "windir", "ComSpec", "LANG", "LC_ALL"]) if (source[key2]) env2[key2] = source[key2];
|
|
2555
3742
|
env2.PATH = process.platform === "win32" ? join4(source.SystemRoot ?? "C:\\Windows", "System32") : "/usr/bin:/bin";
|
|
3743
|
+
if (process.platform === "win32" && (compilerRoot?.path ?? inheritedCompilerRoot)) env2[COMPILER_ROOT] = compilerRoot?.path ?? inheritedCompilerRoot;
|
|
2556
3744
|
return env2;
|
|
2557
3745
|
}
|
|
2558
3746
|
async function resolveNativeHelper({ python = "/usr/bin/python3" } = {}) {
|
|
@@ -2568,27 +3756,28 @@ async function resolveNativeHelper({ python = "/usr/bin/python3" } = {}) {
|
|
|
2568
3756
|
function nativeProcess(helper, mode, spec, { home, env: env2 = privateEnvironment(home) } = {}) {
|
|
2569
3757
|
const events = new EventEmitter2();
|
|
2570
3758
|
const args2 = helper.platform === "win32" ? [...helper.prefix, "-Mode", mode] : [...helper.prefix, mode];
|
|
2571
|
-
const
|
|
2572
|
-
|
|
3759
|
+
const compiler = helper.platform === "win32" ? compilerDirectory() : null;
|
|
3760
|
+
const helperEnv = compiler ? Object.fromEntries(Object.entries(env2).filter(([key2]) => ![COMPILER_ROOT, COMPILER_SCRATCH].includes(key2.toUpperCase()))) : env2;
|
|
3761
|
+
if (compiler) {
|
|
3762
|
+
helperEnv[COMPILER_ROOT] = compiler.root;
|
|
3763
|
+
helperEnv[COMPILER_SCRATCH] = compiler.path;
|
|
3764
|
+
}
|
|
3765
|
+
const child = spawn2(helper.executable, args2, { cwd: home, env: helperEnv, windowsHide: true, shell: false, stdio: ["pipe", "pipe", "pipe"] });
|
|
3766
|
+
let tail = "", buffer = "", bytes = 0, closed2 = false, treeStopped = false;
|
|
2573
3767
|
const decoder = new StringDecoder("utf8"), cliDecoder = new StringDecoder("utf8");
|
|
2574
|
-
|
|
3768
|
+
const cliOutput = new NativeCliOutput();
|
|
2575
3769
|
const frame = (value) => {
|
|
3770
|
+
if (mode === "supervise" && value.control === "stopped" && value.treeStopped === true) treeStopped = true;
|
|
2576
3771
|
if (mode === "supervise" && value.type === "cli-output") {
|
|
2577
3772
|
if (typeof value.data !== "string" || value.data.length > MAX_FRAME * 2) throw new Error("INVALID_CLI_FRAME");
|
|
2578
|
-
|
|
2579
|
-
if (Buffer.byteLength(cliBuffer) > MAX_FRAME) throw new Error("CLI_FRAME_LIMIT");
|
|
2580
|
-
let end;
|
|
2581
|
-
while ((end = cliBuffer.indexOf("\n")) >= 0) {
|
|
2582
|
-
const line = cliBuffer.slice(0, end);
|
|
2583
|
-
cliBuffer = cliBuffer.slice(end + 1);
|
|
2584
|
-
if (line.trim()) events.emit("message", JSON.parse(line));
|
|
2585
|
-
}
|
|
3773
|
+
for (const message of cliOutput.push(cliDecoder.write(Buffer.from(value.data, "base64")))) events.emit("message", message);
|
|
2586
3774
|
} else events.emit(value.control ? "control" : "message", value);
|
|
2587
3775
|
};
|
|
2588
|
-
const exit = new Promise((
|
|
3776
|
+
const exit = new Promise((resolve6) => child.once("close", async (code, signal) => {
|
|
2589
3777
|
closed2 = true;
|
|
3778
|
+
if (compiler && code === 0 && signal === null && (mode !== "supervise" || treeStopped)) await compiler.cleanup();
|
|
2590
3779
|
events.emit("closed", { code, signal });
|
|
2591
|
-
|
|
3780
|
+
resolve6({ code, signal });
|
|
2592
3781
|
}));
|
|
2593
3782
|
child.on("error", (error2) => events.emit("failure", error2));
|
|
2594
3783
|
child.stdin.on("error", () => {
|
|
@@ -2599,7 +3788,8 @@ function nativeProcess(helper, mode, spec, { home, env: env2 = privateEnvironmen
|
|
|
2599
3788
|
child.stdout.on("data", (chunk) => {
|
|
2600
3789
|
bytes += chunk.length;
|
|
2601
3790
|
buffer += decoder.write(chunk);
|
|
2602
|
-
|
|
3791
|
+
const boundedStream = mode === "client" || mode === "server" || mode === "supervise";
|
|
3792
|
+
if (!boundedStream && bytes > 8 * 1024 * 1024 || Buffer.byteLength(buffer) > MAX_FRAME) {
|
|
2603
3793
|
events.emit("failure", new Error("OUTPUT_LIMIT"));
|
|
2604
3794
|
child.stdin.end();
|
|
2605
3795
|
return;
|
|
@@ -2617,17 +3807,17 @@ function nativeProcess(helper, mode, spec, { home, env: env2 = privateEnvironmen
|
|
|
2617
3807
|
}
|
|
2618
3808
|
}
|
|
2619
3809
|
});
|
|
2620
|
-
const send = (value) => new Promise((
|
|
3810
|
+
const send = (value) => new Promise((resolve6, reject) => {
|
|
2621
3811
|
const wire = JSON.stringify(value) + "\n";
|
|
2622
3812
|
if (closed2 || Buffer.byteLength(wire) > MAX_FRAME) {
|
|
2623
3813
|
reject(new Error(closed2 ? "NATIVE_CLOSED" : "INPUT_LIMIT"));
|
|
2624
3814
|
return;
|
|
2625
3815
|
}
|
|
2626
|
-
child.stdin.write(wire, (error2) => error2 ? reject(error2) :
|
|
3816
|
+
child.stdin.write(wire, (error2) => error2 ? reject(error2) : resolve6());
|
|
2627
3817
|
});
|
|
2628
3818
|
if (spec) void send(spec).catch((error2) => events.emit("failure", error2));
|
|
2629
3819
|
function waitControl(name, timeoutMs = 15e3) {
|
|
2630
|
-
return new Promise((
|
|
3820
|
+
return new Promise((resolve6, reject) => {
|
|
2631
3821
|
const clean = () => {
|
|
2632
3822
|
clearTimeout(timer);
|
|
2633
3823
|
events.off("control", control2);
|
|
@@ -2637,7 +3827,7 @@ function nativeProcess(helper, mode, spec, { home, env: env2 = privateEnvironmen
|
|
|
2637
3827
|
const control2 = (value) => {
|
|
2638
3828
|
if (value.control === name) {
|
|
2639
3829
|
clean();
|
|
2640
|
-
|
|
3830
|
+
resolve6(value);
|
|
2641
3831
|
}
|
|
2642
3832
|
};
|
|
2643
3833
|
const ended = () => {
|
|
@@ -2669,8 +3859,8 @@ async function capabilityProbe({ home, python } = {}) {
|
|
|
2669
3859
|
try {
|
|
2670
3860
|
const helper = await resolveNativeHelper({ python });
|
|
2671
3861
|
proc = nativeProcess(helper, "probe", null, { home });
|
|
2672
|
-
const value = await new Promise((
|
|
2673
|
-
proc.events.once("message",
|
|
3862
|
+
const value = await new Promise((resolve6, reject) => {
|
|
3863
|
+
proc.events.once("message", resolve6);
|
|
2674
3864
|
proc.events.once("failure", reject);
|
|
2675
3865
|
proc.events.once("closed", () => reject(new Error("NATIVE_PROBE_FAILED")));
|
|
2676
3866
|
});
|
|
@@ -2685,8 +3875,8 @@ async function capabilityProbe({ home, python } = {}) {
|
|
|
2685
3875
|
async function protectProfile(helper, home) {
|
|
2686
3876
|
if (helper.platform !== "win32") return;
|
|
2687
3877
|
const proc = nativeProcess(helper, "protect", { path: home }, { home });
|
|
2688
|
-
const protectedValue = await new Promise((
|
|
2689
|
-
proc.events.once("message",
|
|
3878
|
+
const protectedValue = await new Promise((resolve6, reject) => {
|
|
3879
|
+
proc.events.once("message", resolve6);
|
|
2690
3880
|
proc.events.once("failure", reject);
|
|
2691
3881
|
proc.events.once("closed", () => reject(new Error("PROFILE_ACL_FAILED")));
|
|
2692
3882
|
});
|
|
@@ -2696,9 +3886,9 @@ async function protectProfile(helper, home) {
|
|
|
2696
3886
|
}
|
|
2697
3887
|
|
|
2698
3888
|
// ../packages/capability-broker/src/cli-bootstrap.mjs
|
|
2699
|
-
import { generateKeyPairSync, sign, randomBytes as
|
|
2700
|
-
import { mkdir as mkdir4, writeFile as writeFile2, mkdtemp, chmod, lstat as
|
|
2701
|
-
import { tmpdir } from "node:os";
|
|
3889
|
+
import { generateKeyPairSync, sign as sign2, randomBytes as randomBytes5 } from "node:crypto";
|
|
3890
|
+
import { mkdir as mkdir4, writeFile as writeFile2, mkdtemp, chmod, lstat as lstat6, realpath as realpath5, rmdir as rmdir2 } from "node:fs/promises";
|
|
3891
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
2702
3892
|
import { isAbsolute as isAbsolute4, join as join5 } from "node:path";
|
|
2703
3893
|
import { performance as performance2 } from "node:perf_hooks";
|
|
2704
3894
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
@@ -2722,14 +3912,65 @@ var PolicyError = class extends Error {
|
|
|
2722
3912
|
}
|
|
2723
3913
|
};
|
|
2724
3914
|
|
|
3915
|
+
// ../packages/capability-broker/src/cli-bridge-response.mjs
|
|
3916
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
3917
|
+
var CHUNK_BYTES = 128 * 1024;
|
|
3918
|
+
var MAX_RESPONSE = 9 * 1024 * 1024;
|
|
3919
|
+
var MAX_BUFFERED = 16 * 1024 * 1024;
|
|
3920
|
+
var fail3 = () => {
|
|
3921
|
+
throw new Error("INVALID_BRIDGE_RESPONSE");
|
|
3922
|
+
};
|
|
3923
|
+
var sha2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
|
|
3924
|
+
function* bridgeResponseFrames(id2, value) {
|
|
3925
|
+
let bytes = Buffer.from(JSON.stringify({ type: "response", id: id2, ...value }));
|
|
3926
|
+
if (bytes.length > MAX_RESPONSE) bytes = Buffer.from(JSON.stringify({ type: "response", id: id2, ok: false, code: "OUTPUT_LIMIT" }));
|
|
3927
|
+
if (bytes.length <= CHUNK_BYTES) {
|
|
3928
|
+
yield JSON.parse(bytes.toString("utf8"));
|
|
3929
|
+
return;
|
|
3930
|
+
}
|
|
3931
|
+
yield { type: "response-start", id: id2, bytes: bytes.length };
|
|
3932
|
+
for (let offset = 0, index = 0; offset < bytes.length; offset += CHUNK_BYTES, index++) {
|
|
3933
|
+
yield { type: "response-chunk", id: id2, index, data: bytes.subarray(offset, offset + CHUNK_BYTES).toString("base64") };
|
|
3934
|
+
}
|
|
3935
|
+
yield { type: "response-end", id: id2, sha256: sha2(bytes) };
|
|
3936
|
+
}
|
|
3937
|
+
function normalizeBridgeMcpResult(value) {
|
|
3938
|
+
if (!value || !Array.isArray(value.content) || value.content.length > 32 || Object.keys(value).some((key2) => !["content", "isError"].includes(key2)) || value.isError !== void 0 && typeof value.isError !== "boolean") fail3();
|
|
3939
|
+
for (const block of value.content) {
|
|
3940
|
+
if (block?.type === "text") {
|
|
3941
|
+
if (Object.keys(block).some((key2) => !["type", "text"].includes(key2)) || typeof block.text !== "string") fail3();
|
|
3942
|
+
} else if (block?.type === "image") {
|
|
3943
|
+
if (Object.keys(block).some((key2) => !["type", "data", "mimeType"].includes(key2)) || !["image/png", "image/jpeg", "image/webp"].includes(block.mimeType) || typeof block.data !== "string" || block.data.length > 8 * 1024 * 1024) fail3();
|
|
3944
|
+
const bytes = Buffer.from(block.data, "base64");
|
|
3945
|
+
if (!bytes.length || bytes.toString("base64") !== block.data || bytes.length > 6 * 1024 * 1024) fail3();
|
|
3946
|
+
} else fail3();
|
|
3947
|
+
}
|
|
3948
|
+
return value;
|
|
3949
|
+
}
|
|
3950
|
+
|
|
2725
3951
|
// ../packages/capability-broker/src/cli-bootstrap.mjs
|
|
2726
3952
|
var BRIDGE = fileURLToPath2(new URL("./cli-stdio-bridge.mjs", import.meta.url));
|
|
2727
3953
|
var MAX_BYTES = 262144;
|
|
2728
|
-
|
|
3954
|
+
var REQUEST_KEYS = ["type", "id", "op", "name", "args"];
|
|
3955
|
+
var capabilityOf = (handler) => {
|
|
3956
|
+
const value = handler.capability ?? handler.definition.name;
|
|
3957
|
+
if (typeof value !== "string" || !value || Buffer.byteLength(value) > 256) throw new TypeError("INVALID_HANDLER_CAPABILITY");
|
|
3958
|
+
return value;
|
|
3959
|
+
};
|
|
3960
|
+
async function createCliBootstrap({ authority, subject, binding, tools, handlers, home, ttlMs = 3e4, helper, contextFor = null }) {
|
|
2729
3961
|
if (!isAbsolute4(home)) throw new TypeError("An isolated absolute home is required");
|
|
3962
|
+
if (contextFor !== null && typeof contextFor !== "function") throw new TypeError("INVALID_HOST_CONTEXT_CALLBACK");
|
|
2730
3963
|
const ownedBinding = Object.freeze({ ...binding });
|
|
2731
3964
|
const registry = new Map(handlers.map((handler) => [handler.definition.name, handler]));
|
|
2732
3965
|
if (registry.size !== handlers.length || tools.some((name) => !registry.has(name))) throw new TypeError("Unknown or duplicate bridge handler");
|
|
3966
|
+
const selected = new Map(tools.map((name) => [name, { handler: registry.get(name), capability: capabilityOf(registry.get(name)) }]));
|
|
3967
|
+
const leaseTools = [...new Set([...selected.values()].map((entry) => entry.capability))];
|
|
3968
|
+
const byCapability = /* @__PURE__ */ new Map();
|
|
3969
|
+
for (const entry of selected.values()) {
|
|
3970
|
+
const sharing = byCapability.get(entry.capability);
|
|
3971
|
+
if (sharing) sharing.push(entry.handler);
|
|
3972
|
+
else byCapability.set(entry.capability, [entry.handler]);
|
|
3973
|
+
}
|
|
2733
3974
|
if (!Number.isSafeInteger(ttlMs) || ttlMs < 1e3 || ttlMs > 72e5 || typeof authority.renew !== "function") throw new TypeError("BOUNDED_RENEWABLE_LEASE_REQUIRED");
|
|
2734
3975
|
const leaseTtlMs = Math.min(3e4, ttlMs);
|
|
2735
3976
|
const startedAt = performance2.now();
|
|
@@ -2738,45 +3979,62 @@ async function createCliBootstrap({ authority, subject, binding, tools, handlers
|
|
|
2738
3979
|
const publicSpki = publicKey.export({ type: "spki", format: "der" }).toString("base64url");
|
|
2739
3980
|
await mkdir4(home, { recursive: true, mode: 448 });
|
|
2740
3981
|
await mkdir4(join5(home, "tmp"), { recursive: true, mode: 448 });
|
|
2741
|
-
const descriptorPath2 = join5(home, `bridge-${
|
|
3982
|
+
const descriptorPath2 = join5(home, `bridge-${randomBytes5(12).toString("hex")}.json`);
|
|
2742
3983
|
let socketDirectory;
|
|
2743
|
-
let endpoint = process.platform === "win32" ? `dsh-cli-${
|
|
3984
|
+
let endpoint = process.platform === "win32" ? `dsh-cli-${randomBytes5(18).toString("hex")}` : join5(home, `p-${randomBytes5(6).toString("hex")}.sock`);
|
|
2744
3985
|
if (process.platform === "linux" && Buffer.byteLength(endpoint) > 103) {
|
|
2745
|
-
let parent = await realpath5(
|
|
3986
|
+
let parent = await realpath5(tmpdir2());
|
|
2746
3987
|
if (Buffer.byteLength(join5(parent, "dsh-cli-XXXXXX", "p.sock")) > 103) parent = await realpath5("/tmp");
|
|
2747
3988
|
socketDirectory = await mkdtemp(join5(parent, "dsh-cli-"));
|
|
2748
3989
|
await chmod(socketDirectory, 448);
|
|
2749
|
-
const stat8 = await
|
|
3990
|
+
const stat8 = await lstat6(socketDirectory);
|
|
2750
3991
|
if (!stat8.isDirectory() || stat8.isSymbolicLink() || stat8.uid !== process.getuid() || (stat8.mode & 63) !== 0) {
|
|
2751
|
-
await
|
|
3992
|
+
await rmdir2(socketDirectory);
|
|
2752
3993
|
throw new Error("PRIVATE_SOCKET_DIRECTORY_REQUIRED");
|
|
2753
3994
|
}
|
|
2754
3995
|
endpoint = join5(socketDirectory, "p.sock");
|
|
2755
3996
|
if (Buffer.byteLength(endpoint) > 103) {
|
|
2756
|
-
await
|
|
3997
|
+
await rmdir2(socketDirectory);
|
|
2757
3998
|
throw new Error("PRIVATE_SOCKET_PATH_TOO_LONG");
|
|
2758
3999
|
}
|
|
2759
4000
|
}
|
|
2760
4001
|
let lease;
|
|
2761
4002
|
try {
|
|
2762
|
-
lease = authority.issue(subject, { binding: ownedBinding, tools, ttlMs: leaseTtlMs });
|
|
4003
|
+
lease = authority.issue(subject, { binding: ownedBinding, tools: leaseTools, ttlMs: leaseTtlMs });
|
|
2763
4004
|
} catch (error2) {
|
|
2764
|
-
if (socketDirectory) await
|
|
4005
|
+
if (socketDirectory) await rmdir2(socketDirectory);
|
|
2765
4006
|
throw error2;
|
|
2766
4007
|
}
|
|
2767
4008
|
let relay, closed2 = false, bound = false, connected = false, closing;
|
|
2768
|
-
let accepted, rejected = 0, requests = 0, replies = 0;
|
|
4009
|
+
let accepted, rejected = 0, requests = 0, replies = 0, highestRequestId = 0;
|
|
2769
4010
|
const active = /* @__PURE__ */ new Map();
|
|
2770
4011
|
const release = () => {
|
|
2771
4012
|
authority.release(lease.token);
|
|
2772
4013
|
for (const controller of active.values()) controller.abort(new Error("BRIDGE_CLOSED"));
|
|
2773
4014
|
};
|
|
2774
|
-
const reply = (id2, value) => {
|
|
4015
|
+
const reply = async (id2, value, signal) => {
|
|
2775
4016
|
if (!relay || relay.closed) return;
|
|
2776
4017
|
replies++;
|
|
2777
|
-
const
|
|
2778
|
-
|
|
2779
|
-
|
|
4018
|
+
for (const frame of bridgeResponseFrames(id2, value)) {
|
|
4019
|
+
signal?.throwIfAborted();
|
|
4020
|
+
await relay.send(frame);
|
|
4021
|
+
}
|
|
4022
|
+
};
|
|
4023
|
+
const hostContext = (op, name, id2) => {
|
|
4024
|
+
if (!contextFor) return null;
|
|
4025
|
+
let value;
|
|
4026
|
+
try {
|
|
4027
|
+
value = contextFor({ op, name, id: id2 });
|
|
4028
|
+
} catch (error2) {
|
|
4029
|
+
throw error2 instanceof PolicyError ? error2 : new PolicyError("CONTEXT_UNAVAILABLE", { gate: "host-context" });
|
|
4030
|
+
}
|
|
4031
|
+
if (!value || typeof value !== "object" || typeof value.then === "function") throw new PolicyError("CONTEXT_UNAVAILABLE", { gate: "host-context" });
|
|
4032
|
+
return value;
|
|
4033
|
+
};
|
|
4034
|
+
const requestSignal = (controller, context) => {
|
|
4035
|
+
const signal = context?.signal ?? null;
|
|
4036
|
+
if (signal !== null && !(signal instanceof AbortSignal)) throw new PolicyError("CONTEXT_UNAVAILABLE", { gate: "host-signal" });
|
|
4037
|
+
return signal ? AbortSignal.any([controller.signal, signal]) : controller.signal;
|
|
2780
4038
|
};
|
|
2781
4039
|
async function request(message) {
|
|
2782
4040
|
requests++;
|
|
@@ -2787,28 +4045,45 @@ async function createCliBootstrap({ authority, subject, binding, tools, handlers
|
|
|
2787
4045
|
}
|
|
2788
4046
|
if (message.type !== "request") return;
|
|
2789
4047
|
const id2 = message.id;
|
|
4048
|
+
if (id2 <= highestRequestId) {
|
|
4049
|
+
void reply(id2, { ok: false, code: "DUPLICATE_REQUEST" }).catch(() => {
|
|
4050
|
+
});
|
|
4051
|
+
return;
|
|
4052
|
+
}
|
|
4053
|
+
highestRequestId = id2;
|
|
2790
4054
|
try {
|
|
2791
4055
|
if (closed2) throw new PolicyError("BRIDGE_CLOSED");
|
|
2792
|
-
if (Buffer.byteLength(JSON.stringify(message)) > MAX_BYTES || Object.keys(message).some((key2) => !
|
|
2793
|
-
if (active.
|
|
4056
|
+
if (Buffer.byteLength(JSON.stringify(message)) > MAX_BYTES || Object.keys(message).some((key2) => !REQUEST_KEYS.includes(key2))) throw new PolicyError("INVALID_REQUEST");
|
|
4057
|
+
if (active.size >= 16) throw new PolicyError("BUSY");
|
|
2794
4058
|
if (message.op === "list") {
|
|
2795
|
-
|
|
4059
|
+
const context2 = hostContext("list", null, id2);
|
|
4060
|
+
requestSignal(new AbortController(), context2).throwIfAborted();
|
|
4061
|
+
await reply(id2, { ok: true, value: authority.list(lease.token, ownedBinding).flatMap((capability) => byCapability.get(capability) ?? []).map((handler) => handler.definition) });
|
|
2796
4062
|
return;
|
|
2797
4063
|
}
|
|
2798
4064
|
if (message.op !== "call") throw new PolicyError("INVALID_REQUEST");
|
|
4065
|
+
const entry = selected.get(message.name);
|
|
4066
|
+
if (!entry) throw new PolicyError("TOOL_DENIED");
|
|
4067
|
+
const context = hostContext("call", message.name, id2);
|
|
2799
4068
|
const controller = new AbortController();
|
|
4069
|
+
const signal = requestSignal(controller, context);
|
|
2800
4070
|
active.set(id2, controller);
|
|
2801
4071
|
try {
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
4072
|
+
signal.throwIfAborted();
|
|
4073
|
+
const value = await authority.run(lease.token, ownedBinding, entry.capability, ({ signal: authoritySignal }) => {
|
|
4074
|
+
authoritySignal.throwIfAborted();
|
|
4075
|
+
return entry.handler.execute(entry.handler.parse(message.args), { signal: authoritySignal, binding: ownedBinding, context });
|
|
4076
|
+
}, { signal });
|
|
4077
|
+
signal.throwIfAborted();
|
|
4078
|
+
const mcpResult = entry.handler.render ? normalizeBridgeMcpResult(await entry.handler.render(value)) : null;
|
|
4079
|
+
signal.throwIfAborted();
|
|
4080
|
+
await reply(id2, { ok: true, ...mcpResult ? { mcpResult } : { value } }, signal);
|
|
2807
4081
|
} finally {
|
|
2808
4082
|
active.delete(id2);
|
|
2809
4083
|
}
|
|
2810
4084
|
} catch (error2) {
|
|
2811
|
-
reply(id2, { ok: false, code: error2 instanceof PolicyError ? error2.code : "HANDLER_REFUSED" })
|
|
4085
|
+
await reply(id2, { ok: false, code: error2 instanceof PolicyError ? error2.code : "HANDLER_REFUSED" }).catch(() => {
|
|
4086
|
+
});
|
|
2812
4087
|
}
|
|
2813
4088
|
}
|
|
2814
4089
|
const close = () => {
|
|
@@ -2820,8 +4095,8 @@ async function createCliBootstrap({ authority, subject, binding, tools, handlers
|
|
|
2820
4095
|
closing = (async () => {
|
|
2821
4096
|
if (relay) {
|
|
2822
4097
|
relay.end();
|
|
2823
|
-
const stopped = await Promise.race([relay.exit.then(() => true), new Promise((
|
|
2824
|
-
const t = setTimeout(() =>
|
|
4098
|
+
const stopped = await Promise.race([relay.exit.then(() => true), new Promise((resolve6) => {
|
|
4099
|
+
const t = setTimeout(() => resolve6(false), 3e3);
|
|
2825
4100
|
t.unref();
|
|
2826
4101
|
})]);
|
|
2827
4102
|
if (!stopped) {
|
|
@@ -2829,7 +4104,7 @@ async function createCliBootstrap({ authority, subject, binding, tools, handlers
|
|
|
2829
4104
|
await relay.exit;
|
|
2830
4105
|
}
|
|
2831
4106
|
}
|
|
2832
|
-
if (socketDirectory) await
|
|
4107
|
+
if (socketDirectory) await rmdir2(socketDirectory).catch(() => {
|
|
2833
4108
|
});
|
|
2834
4109
|
})();
|
|
2835
4110
|
return closing;
|
|
@@ -2890,7 +4165,7 @@ async function createCliBootstrap({ authority, subject, binding, tools, handlers
|
|
|
2890
4165
|
hostPid: process.pid,
|
|
2891
4166
|
binding: ownedBinding
|
|
2892
4167
|
});
|
|
2893
|
-
const descriptor = JSON.stringify({ payload, signature:
|
|
4168
|
+
const descriptor = JSON.stringify({ payload, signature: sign2(null, Buffer.from(payload), privateKey).toString("base64url") });
|
|
2894
4169
|
await writeFile2(descriptorPath2, descriptor, { flag: "wx", mode: 384 });
|
|
2895
4170
|
};
|
|
2896
4171
|
return Object.freeze({
|
|
@@ -3026,15 +4301,15 @@ function assertEffectiveProfile(result, { requestedModel, bridge, home, tools, s
|
|
|
3026
4301
|
}
|
|
3027
4302
|
|
|
3028
4303
|
// ../packages/cli-runtime/src/codex-session-core.mjs
|
|
3029
|
-
var digest = (value) =>
|
|
4304
|
+
var digest = (value) => createHash6("sha256").update(value).digest("hex");
|
|
3030
4305
|
var id = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(value);
|
|
3031
4306
|
var validText = (value) => typeof value === "string" && value.trim() && !value.includes("\0") && Buffer.byteLength(value) <= 64e3;
|
|
3032
|
-
async function pinCodexExecutable({ executable, sha256, version }, home) {
|
|
3033
|
-
if (!isAbsolute5(executable) || !/^[a-f0-9]{64}$/.test(
|
|
4307
|
+
async function pinCodexExecutable({ executable, sha256: sha2562, version }, home) {
|
|
4308
|
+
if (!isAbsolute5(executable) || !/^[a-f0-9]{64}$/.test(sha2562) || !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version)) throw new TypeError("PINNED_NATIVE_ENTRY_REQUIRED");
|
|
3034
4309
|
const path4 = await realpath6(executable);
|
|
3035
4310
|
if (!(await stat5(path4)).isFile() || process.platform === "win32" && !path4.toLowerCase().endsWith(".exe")) throw new Error("NATIVE_ENTRY_REQUIRED");
|
|
3036
|
-
if (digest(await
|
|
3037
|
-
const reported = await new Promise((
|
|
4311
|
+
if (digest(await readFile7(path4)) !== sha2562) throw new Error("CLI_ENTRY_CHANGED");
|
|
4312
|
+
const reported = await new Promise((resolve6, reject) => {
|
|
3038
4313
|
const child = spawn3(path4, ["--version"], { cwd: home, env: privateEnvironment(home), windowsHide: true, shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
3039
4314
|
let output = "", error2 = "";
|
|
3040
4315
|
const timer = setTimeout(() => {
|
|
@@ -3051,12 +4326,12 @@ async function pinCodexExecutable({ executable, sha256, version }, home) {
|
|
|
3051
4326
|
child.once("error", reject);
|
|
3052
4327
|
child.once("close", (code) => {
|
|
3053
4328
|
clearTimeout(timer);
|
|
3054
|
-
if (code === 0)
|
|
4329
|
+
if (code === 0) resolve6(output.trim());
|
|
3055
4330
|
else reject(new Error(`CLI_VERSION_FAILED: ${error2.slice(0, 128)}`));
|
|
3056
4331
|
});
|
|
3057
4332
|
});
|
|
3058
4333
|
if (reported !== `codex-cli ${version}`) throw new Error("CLI_VERSION_MISMATCH");
|
|
3059
|
-
return Object.freeze({ path: path4, sha256, version });
|
|
4334
|
+
return Object.freeze({ path: path4, sha256: sha2562, version });
|
|
3060
4335
|
}
|
|
3061
4336
|
async function createCodexSessionCore({
|
|
3062
4337
|
authority,
|
|
@@ -3101,6 +4376,7 @@ async function createCodexSessionCore({
|
|
|
3101
4376
|
await protectProfile(probe.helper, home);
|
|
3102
4377
|
const entry = await pinCodexExecutable(suppliedEntry, home);
|
|
3103
4378
|
const bridge = await createCliBootstrap({ authority, subject, binding, tools, handlers, home: join6(home, "b"), ttlMs: deadlineMs, helper: probe.helper });
|
|
4379
|
+
const compilerRoot2 = privateEnvironment(home).DSH_NATIVE_COMPILER_ROOT;
|
|
3104
4380
|
const config = [
|
|
3105
4381
|
`model = ${JSON.stringify(requestedModel)}`,
|
|
3106
4382
|
'model_provider = "openai"',
|
|
@@ -3130,15 +4406,16 @@ async function createCodexSessionCore({
|
|
|
3130
4406
|
`HOME = ${JSON.stringify(home)}`,
|
|
3131
4407
|
`USERPROFILE = ${JSON.stringify(home)}`,
|
|
3132
4408
|
`TEMP = ${JSON.stringify(join6(home, "tmp"))}`,
|
|
3133
|
-
`TMP = ${JSON.stringify(join6(home, "tmp"))}
|
|
4409
|
+
`TMP = ${JSON.stringify(join6(home, "tmp"))}`,
|
|
4410
|
+
...compilerRoot2 ? [`DSH_NATIVE_COMPILER_ROOT = ${JSON.stringify(compilerRoot2)}`] : []
|
|
3134
4411
|
].join("\n") + "\n";
|
|
3135
4412
|
await writeFile3(join6(home, "config.toml"), config, { flag: "wx", mode: 384 });
|
|
3136
4413
|
if (provisionCredentials) await provisionCredentials(home);
|
|
3137
|
-
if (digest(await
|
|
4414
|
+
if (digest(await readFile7(join6(home, "config.toml"))) !== digest(Buffer.from(config))) {
|
|
3138
4415
|
await bridge.close();
|
|
3139
4416
|
throw new Error("CLI_PROFILE_CHANGED_DURING_PROVISIONING");
|
|
3140
4417
|
}
|
|
3141
|
-
if (digest(await
|
|
4418
|
+
if (digest(await readFile7(entry.path)) !== entry.sha256) {
|
|
3142
4419
|
await bridge.close();
|
|
3143
4420
|
throw new Error("CLI_ENTRY_CHANGED");
|
|
3144
4421
|
}
|
|
@@ -3152,7 +4429,7 @@ async function createCodexSessionCore({
|
|
|
3152
4429
|
for (const value of pending.values()) value.reject(error2);
|
|
3153
4430
|
pending.clear();
|
|
3154
4431
|
};
|
|
3155
|
-
const rpc = (method, params, timeoutMs = 3e4) => new Promise((
|
|
4432
|
+
const rpc = (method, params, timeoutMs = 3e4) => new Promise((resolve6, reject) => {
|
|
3156
4433
|
if (closed2 || fatal) {
|
|
3157
4434
|
reject(fatal ?? new Error("CLI_CLOSED"));
|
|
3158
4435
|
return;
|
|
@@ -3166,7 +4443,7 @@ async function createCodexSessionCore({
|
|
|
3166
4443
|
clearTimeout(timer2);
|
|
3167
4444
|
fn(value);
|
|
3168
4445
|
};
|
|
3169
|
-
pending.set(id2, { resolve: settle(
|
|
4446
|
+
pending.set(id2, { resolve: settle(resolve6), reject: settle(reject) });
|
|
3170
4447
|
void supervisor.send({ id: id2, method, params }).catch((error2) => {
|
|
3171
4448
|
const value = pending.get(id2);
|
|
3172
4449
|
pending.delete(id2);
|
|
@@ -3182,11 +4459,11 @@ async function createCodexSessionCore({
|
|
|
3182
4459
|
stopPromise = (async () => {
|
|
3183
4460
|
await bridge.close();
|
|
3184
4461
|
if (!fatal && !signal?.aborted && ["completed", "failed"].includes(evidence.snapshot().terminal) && !stopEvidence?.treeStopped) {
|
|
3185
|
-
await new Promise((
|
|
4462
|
+
await new Promise((resolve6) => {
|
|
3186
4463
|
const finish = () => {
|
|
3187
4464
|
clearTimeout(grace);
|
|
3188
4465
|
signal?.removeEventListener("abort", finish);
|
|
3189
|
-
|
|
4466
|
+
resolve6();
|
|
3190
4467
|
};
|
|
3191
4468
|
const grace = setTimeout(finish, 1e3);
|
|
3192
4469
|
supervisor.exit.then(finish);
|
|
@@ -3195,8 +4472,8 @@ async function createCodexSessionCore({
|
|
|
3195
4472
|
});
|
|
3196
4473
|
}
|
|
3197
4474
|
supervisor.end();
|
|
3198
|
-
const result = await Promise.race([supervisor.exit, new Promise((
|
|
3199
|
-
const t = setTimeout(() =>
|
|
4475
|
+
const result = await Promise.race([supervisor.exit, new Promise((resolve6) => {
|
|
4476
|
+
const t = setTimeout(() => resolve6(null), 8e3);
|
|
3200
4477
|
t.unref();
|
|
3201
4478
|
})]);
|
|
3202
4479
|
if (!result) {
|
|
@@ -3330,11 +4607,11 @@ async function createCodexSessionCore({
|
|
|
3330
4607
|
activeTurn = true;
|
|
3331
4608
|
requestCount++;
|
|
3332
4609
|
try {
|
|
3333
|
-
const completion = new Promise((
|
|
4610
|
+
const completion = new Promise((resolve6, reject) => {
|
|
3334
4611
|
const listener = (_message, update) => {
|
|
3335
4612
|
if (update.completed) {
|
|
3336
4613
|
notifications.delete(listener);
|
|
3337
|
-
|
|
4614
|
+
resolve6(evidence.snapshot());
|
|
3338
4615
|
}
|
|
3339
4616
|
};
|
|
3340
4617
|
notifications.add(listener);
|
|
@@ -3408,7 +4685,7 @@ function agentAuthority(context, validate, requestedModel, catalogProvider, cata
|
|
|
3408
4685
|
issue(_subject, { binding, tools, ttlMs }) {
|
|
3409
4686
|
validate(context);
|
|
3410
4687
|
if (binding?.location !== "remote" || !Array.isArray(tools) || tools.length || !Number.isSafeInteger(ttlMs) || ttlMs < 1 || ttlMs > 3e5) throw new Error("AGENT_CLI_LEASE_DENIED");
|
|
3411
|
-
const token =
|
|
4688
|
+
const token = randomBytes6(32).toString("base64url");
|
|
3412
4689
|
leases.set(token, { binding: bindingKey(binding), tools: new Set(tools), expiresAt: Math.min(Date.now() + ttlMs, context.expiresAt) });
|
|
3413
4690
|
return { token };
|
|
3414
4691
|
},
|
|
@@ -3521,7 +4798,7 @@ var CliTasks = class {
|
|
|
3521
4798
|
}
|
|
3522
4799
|
async provision(profile, c, home) {
|
|
3523
4800
|
const reference = profile.bots[c.botId].credentialRef;
|
|
3524
|
-
const bytes = await
|
|
4801
|
+
const bytes = await readFile8(reference);
|
|
3525
4802
|
if (bytes.length > 65536) fail("CLI_CREDENTIAL_PROVISION_FAILED");
|
|
3526
4803
|
await writeFile4(join7(home, "auth.json"), bytes, { flag: "wx", mode: 384 });
|
|
3527
4804
|
}
|
|
@@ -3617,19 +4894,19 @@ var CliTasks = class {
|
|
|
3617
4894
|
if (identity) await identity.pin.close();
|
|
3618
4895
|
hooks.clearProcess();
|
|
3619
4896
|
}
|
|
3620
|
-
const base = session?.snapshot(),
|
|
3621
|
-
if (
|
|
3622
|
-
const terminal2 =
|
|
4897
|
+
const base = session?.snapshot(), receipt2 = identity && base ? this.remoteReceipt(identity, base) : null, stopConfirmed = stop?.treeStopped === true;
|
|
4898
|
+
if (receipt2 && stopConfirmed && controller.signal.aborted && !["completed", "failed"].includes(receipt2.terminal)) receipt2.terminal = "interrupted";
|
|
4899
|
+
const terminal2 = receipt2?.terminal === "completed" ? "succeeded" : receipt2?.terminal === "failed" ? "failed" : receipt2?.terminal === "interrupted" ? "cancelled" : "unknown";
|
|
3623
4900
|
const status = !stopConfirmed ? "unknown" : error2 && terminal2 === "succeeded" ? "unknown" : terminal2;
|
|
3624
4901
|
const code = status === "unknown" ? "OUTCOME_UNKNOWN" : status === "cancelled" ? "CANCELLED" : status === "failed" ? error2?.code ?? "CLI_TASK_FAILED" : null;
|
|
3625
4902
|
const result = {
|
|
3626
4903
|
kind: "remote-cli",
|
|
3627
4904
|
schemaVersion: 1,
|
|
3628
|
-
receipt,
|
|
4905
|
+
receipt: receipt2,
|
|
3629
4906
|
stopConfirmed,
|
|
3630
4907
|
stopEvidence: stop ?? { treeStopped: false, status: "unconfirmed" },
|
|
3631
4908
|
exitCode: Number.isSafeInteger(stop?.exitCode) ? stop.exitCode : null,
|
|
3632
|
-
...
|
|
4909
|
+
...receipt2?.cliTurnRequestsDispatched === 0 ? { notStarted: true, possiblyApplied: false } : {}
|
|
3633
4910
|
};
|
|
3634
4911
|
return { status, result, error: code };
|
|
3635
4912
|
}
|
|
@@ -3854,7 +5131,7 @@ var Agent = class {
|
|
|
3854
5131
|
if (data.length > 8 * 1024 * 1024) fail("ARTIFACT_LIMIT", 413);
|
|
3855
5132
|
const usage = await this.evictArtifacts(data.length);
|
|
3856
5133
|
if (usage + data.length > ARTIFACT_QUOTA_BYTES) fail("ARTIFACT_QUOTA", 507);
|
|
3857
|
-
const id2 = `artifact-${
|
|
5134
|
+
const id2 = `artifact-${randomUUID7()}`;
|
|
3858
5135
|
await mkdir6(join8(this.config.dataDir, "artifacts"), { recursive: true, mode: 448 });
|
|
3859
5136
|
await writeFile5(join8(this.config.dataDir, "artifacts", id2), data, { flag: "wx", mode: 384 });
|
|
3860
5137
|
this.store.db.prepare("INSERT INTO artifacts VALUES (?,?,?,?,?)").run(id2, canonical(c), sha(data), data.length, media);
|
|
@@ -3868,7 +5145,7 @@ var Agent = class {
|
|
|
3868
5145
|
if (usage + needed <= ARTIFACT_QUOTA_BYTES) break;
|
|
3869
5146
|
this.store.db.prepare("DELETE FROM artifacts WHERE id=?").run(row.id);
|
|
3870
5147
|
usage -= row.size;
|
|
3871
|
-
await
|
|
5148
|
+
await unlink5(join8(this.config.dataDir, "artifacts", row.id)).catch(() => {
|
|
3872
5149
|
});
|
|
3873
5150
|
}
|
|
3874
5151
|
return usage;
|
|
@@ -3891,7 +5168,7 @@ var Agent = class {
|
|
|
3891
5168
|
if (!row) fail("ARTIFACT_NOT_FOUND", 404);
|
|
3892
5169
|
this.store.own(row.context, c);
|
|
3893
5170
|
if (name === "artifact_stat") return { artifactId: row.id, sha256: row.hash, size: row.size, mediaType: row.media };
|
|
3894
|
-
const offset = integer(a.offset, 0, Number.MAX_SAFE_INTEGER, 0), length = integer(a.length, 1, 1048576, 65536), data = await
|
|
5171
|
+
const offset = integer(a.offset, 0, Number.MAX_SAFE_INTEGER, 0), length = integer(a.length, 1, 1048576, 65536), data = await readFile9(join8(this.config.dataDir, "artifacts", row.id));
|
|
3895
5172
|
if (sha(data) !== row.hash) fail("ARTIFACT_CORRUPT");
|
|
3896
5173
|
return { encoding: "base64", content: data.subarray(offset, offset + length).toString("base64"), eof: offset + length >= row.size };
|
|
3897
5174
|
}
|
|
@@ -3977,9 +5254,9 @@ var Agent = class {
|
|
|
3977
5254
|
if (pin) await pin.verify();
|
|
3978
5255
|
}
|
|
3979
5256
|
async legacyWrite(a, c, signal, target) {
|
|
3980
|
-
const temp = join8(
|
|
5257
|
+
const temp = join8(dirname2(target), `.dsh-write-${randomUUID7()}`);
|
|
3981
5258
|
try {
|
|
3982
|
-
const parent = await stat7(
|
|
5259
|
+
const parent = await stat7(dirname2(target));
|
|
3983
5260
|
let previous = null;
|
|
3984
5261
|
try {
|
|
3985
5262
|
previous = (await this.roots.read(a.rootId, a.relativePath, 0, 1048577)).data;
|
|
@@ -3993,7 +5270,7 @@ var Agent = class {
|
|
|
3993
5270
|
if (signal?.aborted) throw Object.assign(new AgentError("WRITE_CANCELLED", 409), { possiblyApplied: false });
|
|
3994
5271
|
this.store.validate(c);
|
|
3995
5272
|
await this.roots.resolve(a.rootId, a.relativePath, true);
|
|
3996
|
-
const now = await stat7(
|
|
5273
|
+
const now = await stat7(dirname2(target));
|
|
3997
5274
|
if (now.ino !== parent.ino || now.dev !== parent.dev) fail("FILE_CHANGED", 409);
|
|
3998
5275
|
let current = null;
|
|
3999
5276
|
try {
|
|
@@ -4006,7 +5283,7 @@ var Agent = class {
|
|
|
4006
5283
|
await rename(temp, target);
|
|
4007
5284
|
return { sha256: sha(data), size: data.length };
|
|
4008
5285
|
} finally {
|
|
4009
|
-
await
|
|
5286
|
+
await unlink5(temp).catch(() => {
|
|
4010
5287
|
});
|
|
4011
5288
|
}
|
|
4012
5289
|
}
|
|
@@ -4094,9 +5371,9 @@ var Agent = class {
|
|
|
4094
5371
|
this.processes.set(id2, { child, context: c, cancel });
|
|
4095
5372
|
const timer = setTimeout(() => void cancel("PROCESS_TIMEOUT"), timeout);
|
|
4096
5373
|
try {
|
|
4097
|
-
const exit = await new Promise((
|
|
5374
|
+
const exit = await new Promise((resolve6, reject) => {
|
|
4098
5375
|
child.once("error", reject);
|
|
4099
|
-
child.once("close", (code, signal) =>
|
|
5376
|
+
child.once("close", (code, signal) => resolve6({ exitCode: code, signal }));
|
|
4100
5377
|
});
|
|
4101
5378
|
await cancelPromise;
|
|
4102
5379
|
const out = await this.artifact(Buffer.concat(stdout), c, "text/plain"), err = await this.artifact(Buffer.concat(stderr), c, "text/plain");
|
|
@@ -4116,7 +5393,7 @@ var Agent = class {
|
|
|
4116
5393
|
await this.roots.assertPublicTree(source);
|
|
4117
5394
|
if (await this.git(source, ["status", "--porcelain"])) fail("WORKSPACE_DIRTY", 409);
|
|
4118
5395
|
const head = await this.git(source, ["rev-parse", "HEAD"]);
|
|
4119
|
-
const id2 = `worktree-${
|
|
5396
|
+
const id2 = `worktree-${randomUUID7()}`, relative2 = a.worktreeRelativePath;
|
|
4120
5397
|
if (typeof relative2 !== "string") fail("INVALID_ARGUMENT");
|
|
4121
5398
|
const target = await this.roots.resolve(a.rootId, relative2, true);
|
|
4122
5399
|
if (target === source || this.roots.inside(source, target)) fail("INVALID_WORKTREE_PATH");
|
|
@@ -4137,7 +5414,7 @@ var Agent = class {
|
|
|
4137
5414
|
const patch = await this.git(row.path, ["diff", "--binary", row.head]);
|
|
4138
5415
|
if (name === "worktree_diff") return { sourceHead: row.head, ...await this.artifact(Buffer.from(patch + "\n"), c, "text/x-diff") };
|
|
4139
5416
|
if (await this.git(row.path, ["rev-parse", "HEAD"]) !== row.head || await this.git(row.source, ["rev-parse", "HEAD"]) !== row.head || await this.git(row.source, ["status", "--porcelain"])) return { status: "integration-pending", reason: "SOURCE_CHANGED", worktreeId: a.worktreeId };
|
|
4140
|
-
const patchFile = join8(this.config.dataDir, `patch-${
|
|
5417
|
+
const patchFile = join8(this.config.dataDir, `patch-${randomUUID7()}`);
|
|
4141
5418
|
await writeFile5(patchFile, patch + "\n", { mode: 384 });
|
|
4142
5419
|
try {
|
|
4143
5420
|
if (!patch) return { status: "no-changes" };
|
|
@@ -4149,7 +5426,7 @@ var Agent = class {
|
|
|
4149
5426
|
if (e instanceof AgentError) throw e;
|
|
4150
5427
|
return { status: "integration-pending", reason: "PATCH_CONFLICT", worktreeId: a.worktreeId };
|
|
4151
5428
|
} finally {
|
|
4152
|
-
await
|
|
5429
|
+
await unlink5(patchFile);
|
|
4153
5430
|
}
|
|
4154
5431
|
}
|
|
4155
5432
|
async close() {
|
|
@@ -4167,9 +5444,9 @@ var Agent = class {
|
|
|
4167
5444
|
};
|
|
4168
5445
|
|
|
4169
5446
|
// src/transport.ts
|
|
4170
|
-
import
|
|
5447
|
+
import http2 from "node:http";
|
|
4171
5448
|
import https from "node:https";
|
|
4172
|
-
import { readFileSync as
|
|
5449
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
4173
5450
|
import { networkInterfaces } from "node:os";
|
|
4174
5451
|
import { isIP } from "node:net";
|
|
4175
5452
|
import { Server, WebStandardStreamableHTTPServerTransport, SUPPORTED_PROTOCOL_VERSIONS } from "@modelcontextprotocol/server";
|
|
@@ -4240,7 +5517,133 @@ var AuthThrottle = class {
|
|
|
4240
5517
|
};
|
|
4241
5518
|
|
|
4242
5519
|
// src/transport.ts
|
|
5520
|
+
import { WebSocketServer as WebSocketServer2 } from "ws";
|
|
5521
|
+
|
|
5522
|
+
// src/e2e-server.ts
|
|
5523
|
+
import http from "node:http";
|
|
5524
|
+
import { duplexPair } from "node:stream";
|
|
4243
5525
|
import { WebSocketServer } from "ws";
|
|
5526
|
+
function secureTransport(pairing, handler, upgrade) {
|
|
5527
|
+
const sockets = new WebSocketServer({
|
|
5528
|
+
noServer: true,
|
|
5529
|
+
maxPayload: 65535,
|
|
5530
|
+
perMessageDeflate: false,
|
|
5531
|
+
handleProtocols: (protocols) => protocols.has(DIRECT_PROTOCOL) ? DIRECT_PROTOCOL : false
|
|
5532
|
+
});
|
|
5533
|
+
const sessions = /* @__PURE__ */ new Set(), attempts = /* @__PURE__ */ new Map();
|
|
5534
|
+
let windowStart = 0, windowCount = 0, closing = false;
|
|
5535
|
+
const sweep = setInterval(() => {
|
|
5536
|
+
for (const session of sessions) if (session.peer && !pairing.validPeer(session.peer)) session.remove();
|
|
5537
|
+
}, 500);
|
|
5538
|
+
sweep.unref();
|
|
5539
|
+
return {
|
|
5540
|
+
accept(req, socket, head) {
|
|
5541
|
+
if (closing || sockets.clients.size >= 16) fail("E2E_CONNECTION_LIMIT", 429);
|
|
5542
|
+
if (req.headers.origin !== void 0 || req.headers["sec-websocket-protocol"] !== DIRECT_PROTOCOL) fail("E2E_CLIENT_REQUIRED", 403);
|
|
5543
|
+
const now = Date.now(), source = req.socket.remoteAddress ?? "unknown";
|
|
5544
|
+
if (now - windowStart >= 6e4 || now < windowStart) {
|
|
5545
|
+
windowStart = now;
|
|
5546
|
+
windowCount = 0;
|
|
5547
|
+
}
|
|
5548
|
+
const previous = attempts.get(source), row = previous && now - previous.start < 6e4 && now >= previous.start ? previous : { start: now, count: 0 };
|
|
5549
|
+
if (++row.count > 30 || ++windowCount > 120) fail("E2E_CONNECTION_RATE_LIMIT", 429);
|
|
5550
|
+
attempts.delete(source);
|
|
5551
|
+
attempts.set(source, row);
|
|
5552
|
+
while (attempts.size > 256) attempts.delete(attempts.keys().next().value);
|
|
5553
|
+
sockets.handleUpgrade(req, socket, head, (ws) => {
|
|
5554
|
+
sockets.emit("connection", ws, req);
|
|
5555
|
+
const streams = /* @__PURE__ */ new Set();
|
|
5556
|
+
let session;
|
|
5557
|
+
const origin = http.createServer((request, response) => {
|
|
5558
|
+
if (!session.peer || !pairing.validPeer(session.peer)) {
|
|
5559
|
+
response.writeHead(403);
|
|
5560
|
+
response.end();
|
|
5561
|
+
return;
|
|
5562
|
+
}
|
|
5563
|
+
handler(request, response, session.peer);
|
|
5564
|
+
});
|
|
5565
|
+
origin.requestTimeout = 3e4;
|
|
5566
|
+
origin.headersTimeout = 1e4;
|
|
5567
|
+
origin.on("upgrade", (request, connection, bytes) => {
|
|
5568
|
+
if (!session.peer || !pairing.validPeer(session.peer)) return connection.destroy();
|
|
5569
|
+
upgrade(request, connection, bytes, session.peer);
|
|
5570
|
+
});
|
|
5571
|
+
const transport = {
|
|
5572
|
+
send: (message) => {
|
|
5573
|
+
if (ws.readyState === 1) ws.send(message);
|
|
5574
|
+
},
|
|
5575
|
+
close: (code) => {
|
|
5576
|
+
if (ws.readyState < 2) ws.close(code === 1e3 || code >= 3e3 && code <= 4999 ? code : 1e3);
|
|
5577
|
+
},
|
|
5578
|
+
get bufferedAmount() {
|
|
5579
|
+
return ws.bufferedAmount;
|
|
5580
|
+
},
|
|
5581
|
+
onceDrained(listener) {
|
|
5582
|
+
const check = () => {
|
|
5583
|
+
if (ws.readyState !== 1 || ws.bufferedAmount <= SEND_BUFFER_HIGH / 4) listener();
|
|
5584
|
+
else {
|
|
5585
|
+
const timer = setTimeout(check, 5);
|
|
5586
|
+
timer.unref();
|
|
5587
|
+
}
|
|
5588
|
+
};
|
|
5589
|
+
check();
|
|
5590
|
+
}
|
|
5591
|
+
};
|
|
5592
|
+
session = E2ESession.computer({
|
|
5593
|
+
transport,
|
|
5594
|
+
transportByte: TRANSPORT_DIRECT,
|
|
5595
|
+
staticKey: pairing.transportKey,
|
|
5596
|
+
candidates: () => pairing.candidates(),
|
|
5597
|
+
async connectOrigin() {
|
|
5598
|
+
if (!session.peer || !pairing.validPeer(session.peer)) throw new Error("Peer removed");
|
|
5599
|
+
const [client, server] = duplexPair();
|
|
5600
|
+
Object.assign(server, { setTimeout() {
|
|
5601
|
+
return server;
|
|
5602
|
+
}, setNoDelay() {
|
|
5603
|
+
return server;
|
|
5604
|
+
}, setKeepAlive() {
|
|
5605
|
+
return server;
|
|
5606
|
+
}, remoteAddress: source });
|
|
5607
|
+
client.on("error", () => {
|
|
5608
|
+
});
|
|
5609
|
+
server.on("error", () => {
|
|
5610
|
+
});
|
|
5611
|
+
client.once("close", () => server.destroy());
|
|
5612
|
+
server.once("close", () => client.destroy());
|
|
5613
|
+
streams.add(server);
|
|
5614
|
+
server.once("close", () => streams.delete(server));
|
|
5615
|
+
origin.emit("connection", server);
|
|
5616
|
+
return client;
|
|
5617
|
+
}
|
|
5618
|
+
});
|
|
5619
|
+
sessions.add(session);
|
|
5620
|
+
ws.on("message", (data, binary) => {
|
|
5621
|
+
try {
|
|
5622
|
+
session.receive(Buffer.from(data), binary);
|
|
5623
|
+
} catch {
|
|
5624
|
+
session.close(4e3);
|
|
5625
|
+
}
|
|
5626
|
+
});
|
|
5627
|
+
ws.on("error", () => session.close(4e3));
|
|
5628
|
+
ws.once("close", (code) => session.transportClosed(code));
|
|
5629
|
+
void session.closed.then(() => {
|
|
5630
|
+
sessions.delete(session);
|
|
5631
|
+
for (const stream of streams) stream.destroy();
|
|
5632
|
+
origin.close();
|
|
5633
|
+
});
|
|
5634
|
+
});
|
|
5635
|
+
},
|
|
5636
|
+
async close() {
|
|
5637
|
+
closing = true;
|
|
5638
|
+
clearInterval(sweep);
|
|
5639
|
+
for (const session of sessions) session.shutdown();
|
|
5640
|
+
for (const socket of sockets.clients) socket.terminate();
|
|
5641
|
+
await new Promise((resolve6) => sockets.close(() => resolve6()));
|
|
5642
|
+
}
|
|
5643
|
+
};
|
|
5644
|
+
}
|
|
5645
|
+
|
|
5646
|
+
// src/transport.ts
|
|
4244
5647
|
var loopback = (host) => ["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(host);
|
|
4245
5648
|
function validateNetwork(c) {
|
|
4246
5649
|
const host = c.host ?? "127.0.0.1", mode = c.mode ?? "loopback";
|
|
@@ -4273,22 +5676,23 @@ function validateNetwork(c) {
|
|
|
4273
5676
|
}
|
|
4274
5677
|
if (mode === "cloudflare-tunnel" && (!loopback(host) || !c.protectedTunnel)) fail("TUNNEL_BACKEND_MUST_BE_LOOPBACK");
|
|
4275
5678
|
if (mode === "public-tls" && !c.tls && (!loopback(host) || !c.trustedProxies?.length)) fail("TLS_REQUIRED");
|
|
4276
|
-
if (!c.tls && !loopback(host) && !c.protectedTunnel) fail("TLS_REQUIRED");
|
|
4277
|
-
if (!["loopback", "wireguard", "tailscale", "cloudflare-tunnel", "public-tls"].includes(mode)) fail("INVALID_NETWORK_MODE");
|
|
5679
|
+
if (!c.tls && !loopback(host) && !c.protectedTunnel && mode !== "e2e") fail("TLS_REQUIRED");
|
|
5680
|
+
if (!["loopback", "wireguard", "tailscale", "cloudflare-tunnel", "public-tls", "e2e"].includes(mode)) fail("INVALID_NETWORK_MODE");
|
|
4278
5681
|
}
|
|
4279
5682
|
async function serve(agent, c = {}) {
|
|
4280
5683
|
c = structuredClone(c);
|
|
4281
5684
|
validateNetwork(c);
|
|
4282
5685
|
const throttle = new AuthThrottle(c.authLimits), host = c.host ?? "127.0.0.1";
|
|
4283
|
-
|
|
5686
|
+
if (c.mode === "e2e" && !agent.store.pairing) fail("PAIRING_SERVICE_REQUIRED");
|
|
5687
|
+
const requestBoundary = (req, upgrade = false, encrypted2 = false) => {
|
|
4284
5688
|
const address = server.address(), defaultHost = `${host.includes(":") ? `[${host}]` : host}:${address.port}`;
|
|
4285
5689
|
if (!(c.allowedHosts ?? [defaultHost]).includes(req.headers.host ?? "")) fail("HOST_DENIED", 403);
|
|
4286
5690
|
const origin = req.headers.origin;
|
|
4287
5691
|
if (origin !== void 0 && !(c.allowedOrigins ?? []).includes(origin) && !(upgrade && agent.desktopSessions.hasOrigin(origin))) fail("ORIGIN_DENIED", 403);
|
|
4288
|
-
if (c.mode === "public-tls" && !c.tls && (!(c.trustedProxies ?? []).includes(req.socket.remoteAddress ?? "unknown") || req.headers["x-forwarded-proto"] !== "https")) fail("TLS_REQUIRED", 403);
|
|
5692
|
+
if (!encrypted2 && c.mode === "public-tls" && !c.tls && (!(c.trustedProxies ?? []).includes(req.socket.remoteAddress ?? "unknown") || req.headers["x-forwarded-proto"] !== "https")) fail("TLS_REQUIRED", 403);
|
|
4289
5693
|
return defaultHost;
|
|
4290
5694
|
};
|
|
4291
|
-
const handler = async (req, res) => {
|
|
5695
|
+
const handler = async (req, res, peer) => {
|
|
4292
5696
|
let context;
|
|
4293
5697
|
const source = req.socket.remoteAddress ?? "unknown";
|
|
4294
5698
|
const json = (status, value) => {
|
|
@@ -4296,7 +5700,27 @@ async function serve(agent, c = {}) {
|
|
|
4296
5700
|
res.end(JSON.stringify(value));
|
|
4297
5701
|
};
|
|
4298
5702
|
try {
|
|
4299
|
-
const defaultHost = requestBoundary(req);
|
|
5703
|
+
const defaultHost = requestBoundary(req, false, !!peer);
|
|
5704
|
+
if (c.mode === "e2e" && !peer) fail("E2E_REQUIRED", 426);
|
|
5705
|
+
const url = new URL(req.url ?? "/", `http://${defaultHost}`);
|
|
5706
|
+
if (peer && !agent.store.pairing.validPeer(peer)) fail("AUTH_INVALID", 401);
|
|
5707
|
+
if (url.pathname === "/pair/v2/redeem") {
|
|
5708
|
+
if (!peer || peer.kind !== "invite" || req.method !== "POST" || url.search) fail("PAIRING_CHANNEL_REQUIRED", 403);
|
|
5709
|
+
let body2 = Buffer.alloc(0);
|
|
5710
|
+
for await (const chunk of req) {
|
|
5711
|
+
body2 = Buffer.concat([body2, chunk]);
|
|
5712
|
+
if (body2.length > 2048) fail("REQUEST_TOO_LARGE", 413);
|
|
5713
|
+
}
|
|
5714
|
+
let input;
|
|
5715
|
+
try {
|
|
5716
|
+
input = JSON.parse(body2.toString("utf8"));
|
|
5717
|
+
} catch {
|
|
5718
|
+
fail("INVALID_JSON");
|
|
5719
|
+
}
|
|
5720
|
+
json(200, agent.store.pairing.redeem(peer, input));
|
|
5721
|
+
return;
|
|
5722
|
+
}
|
|
5723
|
+
if (peer?.kind === "invite") fail("PAIRING_CHANNEL_ONLY", 403);
|
|
4300
5724
|
const reject = (decision) => {
|
|
4301
5725
|
if (decision.audit) agent.store.audit(decision.code);
|
|
4302
5726
|
if (decision.retryAfterMs) res.setHeader("retry-after", String(Math.max(1, Math.ceil(decision.retryAfterMs / 1e3))));
|
|
@@ -4306,12 +5730,13 @@ async function serve(agent, c = {}) {
|
|
|
4306
5730
|
if (!/^Bearer [A-Za-z0-9_-]{43}$/.test(auth)) {
|
|
4307
5731
|
reject(throttle.failure(source));
|
|
4308
5732
|
}
|
|
4309
|
-
const url = new URL(req.url ?? "/", `http://${defaultHost}`);
|
|
4310
5733
|
let client;
|
|
4311
5734
|
const authenticate = () => {
|
|
4312
5735
|
try {
|
|
5736
|
+
if (peer && !agent.store.pairing.validPeer(peer)) fail("AUTH_INVALID", 401);
|
|
4313
5737
|
if (url.pathname.startsWith("/control/v1/")) client = agent.store.client(auth.slice(7));
|
|
4314
5738
|
else context = agent.store.grant(auth.slice(7));
|
|
5739
|
+
if (peer && (client?.id ?? context?.clientId) !== peer.id) fail("AUTH_INVALID", 401);
|
|
4315
5740
|
} catch {
|
|
4316
5741
|
reject(throttle.failure(source));
|
|
4317
5742
|
}
|
|
@@ -4345,6 +5770,10 @@ async function serve(agent, c = {}) {
|
|
|
4345
5770
|
json(200, result);
|
|
4346
5771
|
return;
|
|
4347
5772
|
}
|
|
5773
|
+
if (url.pathname === "/control/v1/desktop/close") {
|
|
5774
|
+
json(200, await agent.desktopSessions.closeClientSession(client.id, parsed));
|
|
5775
|
+
return;
|
|
5776
|
+
}
|
|
4348
5777
|
if (url.pathname === "/desktop/v1/session") {
|
|
4349
5778
|
json(200, await agent.desktopSessions.open(context, parsed));
|
|
4350
5779
|
return;
|
|
@@ -4389,20 +5818,23 @@ async function serve(agent, c = {}) {
|
|
|
4389
5818
|
json(e instanceof AgentError ? e.status : 500, { error: { code: e instanceof AgentError ? e.code : "AGENT_INTERNAL" } });
|
|
4390
5819
|
}
|
|
4391
5820
|
};
|
|
4392
|
-
const tls = c.tls ? { key:
|
|
4393
|
-
const
|
|
5821
|
+
const tls = c.tls ? { key: readFileSync3(c.tls.keyFile), cert: readFileSync3(c.tls.certFile), ca: c.tls.caFile ? readFileSync3(c.tls.caFile) : void 0, requestCert: !!c.tls.requireClientCertificate, rejectUnauthorized: !!c.tls.requireClientCertificate } : null;
|
|
5822
|
+
const publicHandler = (req, res) => void handler(req, res);
|
|
5823
|
+
const server = tls ? https.createServer(tls, publicHandler) : http2.createServer(publicHandler);
|
|
4394
5824
|
server.requestTimeout = 3e4;
|
|
4395
5825
|
server.headersTimeout = 1e4;
|
|
4396
5826
|
server.maxConnections = 128;
|
|
4397
|
-
const websocket = new
|
|
4398
|
-
|
|
5827
|
+
const websocket = new WebSocketServer2({ noServer: true, clientTracking: true, maxPayload: 65536, perMessageDeflate: false });
|
|
5828
|
+
const desktopUpgrade = (req, socket, head, peer) => {
|
|
4399
5829
|
try {
|
|
4400
|
-
const defaultHost = requestBoundary(req, true), url = new URL(req.url ?? "/", `http://${defaultHost}`);
|
|
5830
|
+
const defaultHost = requestBoundary(req, true, !!peer), url = new URL(req.url ?? "/", `http://${defaultHost}`);
|
|
5831
|
+
if (c.mode === "e2e" && !peer) fail("E2E_REQUIRED", 426);
|
|
5832
|
+
if (peer && (peer.kind !== "client" || !agent.store.pairing.validPeer(peer))) fail("AUTH_INVALID", 401);
|
|
4401
5833
|
if (url.pathname !== "/desktop/v1/socket" || url.search) fail("NOT_FOUND", 404);
|
|
4402
5834
|
if (websocket.clients.size >= 16) fail("DESKTOP_CONNECTION_LIMIT", 429);
|
|
4403
5835
|
websocket.handleUpgrade(req, socket, head, (ws) => {
|
|
4404
5836
|
websocket.emit("connection", ws, req);
|
|
4405
|
-
agent.desktopSessions.accept(ws, req.headers.origin, c.allowedOrigins ?? []);
|
|
5837
|
+
agent.desktopSessions.accept(ws, req.headers.origin, c.allowedOrigins ?? [], peer?.id);
|
|
4406
5838
|
});
|
|
4407
5839
|
} catch (e) {
|
|
4408
5840
|
const status = e instanceof AgentError ? e.status : 500;
|
|
@@ -4410,35 +5842,118 @@ async function serve(agent, c = {}) {
|
|
|
4410
5842
|
Connection: close\r
|
|
4411
5843
|
Content-Length: 0\r
|
|
4412
5844
|
\r
|
|
5845
|
+
`);
|
|
5846
|
+
}
|
|
5847
|
+
};
|
|
5848
|
+
const encrypted = agent.store.pairing ? secureTransport(agent.store.pairing, (req, res, peer) => void handler(req, res, peer), desktopUpgrade) : null;
|
|
5849
|
+
server.on("upgrade", (req, socket, head) => {
|
|
5850
|
+
try {
|
|
5851
|
+
const defaultHost = requestBoundary(req, true), url = new URL(req.url ?? "/", `http://${defaultHost}`);
|
|
5852
|
+
if (url.pathname === "/v1/e2e" && !url.search) {
|
|
5853
|
+
if (!encrypted) fail("NOT_FOUND", 404);
|
|
5854
|
+
encrypted.accept(req, socket, head);
|
|
5855
|
+
return;
|
|
5856
|
+
}
|
|
5857
|
+
desktopUpgrade(req, socket, head);
|
|
5858
|
+
} catch (e) {
|
|
5859
|
+
const status = e instanceof AgentError ? e.status : 500;
|
|
5860
|
+
socket.end(`HTTP/1.1 ${status} Rejected\r
|
|
5861
|
+
Connection: close\r
|
|
5862
|
+
Content-Length: 0\r
|
|
5863
|
+
\r
|
|
4413
5864
|
`);
|
|
4414
5865
|
}
|
|
4415
5866
|
});
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
5867
|
+
try {
|
|
5868
|
+
await new Promise((resolve6, reject) => {
|
|
5869
|
+
server.once("error", reject);
|
|
5870
|
+
server.listen(c.port ?? 9701, host, () => resolve6());
|
|
5871
|
+
});
|
|
5872
|
+
} catch (error2) {
|
|
5873
|
+
await encrypted?.close();
|
|
5874
|
+
throw error2;
|
|
5875
|
+
}
|
|
4420
5876
|
return { server, address: server.address(), supportedMcpVersions: SUPPORTED_PROTOCOL_VERSIONS, close: async () => {
|
|
4421
5877
|
await agent.desktopSessions.close();
|
|
4422
5878
|
for (const client of websocket.clients) client.terminate();
|
|
4423
|
-
await new Promise((
|
|
4424
|
-
await
|
|
5879
|
+
await new Promise((resolve6) => websocket.close(() => resolve6()));
|
|
5880
|
+
await encrypted?.close();
|
|
5881
|
+
await new Promise((resolve6, reject) => server.close((e) => e ? reject(e) : resolve6()));
|
|
4425
5882
|
} };
|
|
4426
5883
|
}
|
|
4427
5884
|
|
|
5885
|
+
// src/pairing-page.ts
|
|
5886
|
+
import QRCode from "qrcode";
|
|
5887
|
+
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
5888
|
+
import { mkdir as mkdir7, writeFile as writeFile6, lstat as lstat8, rename as rename2, rm } from "node:fs/promises";
|
|
5889
|
+
import { dirname as dirname3, resolve as resolve4 } from "node:path";
|
|
5890
|
+
var escape = (value) => String(value).replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]);
|
|
5891
|
+
async function pairingPage(invite) {
|
|
5892
|
+
const { code, ...scannable } = invite;
|
|
5893
|
+
const payload = JSON.stringify(scannable), nonce = randomBytes7(18).toString("base64"), fingerprint = invite.transport.fingerprint.match(/.{1,8}/g).join(" ");
|
|
5894
|
+
let qr = "";
|
|
5895
|
+
try {
|
|
5896
|
+
qr = await QRCode.toString(payload, { type: "svg", errorCorrectionLevel: "M", margin: 4, width: 320 });
|
|
5897
|
+
} catch {
|
|
5898
|
+
}
|
|
5899
|
+
const html = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none';style-src 'unsafe-inline';script-src 'nonce-${nonce}';base-uri 'none';form-action 'none'"><title>\u5B89\u5168\u914D\u5BF9 \xB7 DSH \u7535\u8111</title><style>
|
|
5900
|
+
:root{font-family:system-ui,"Microsoft YaHei",sans-serif;color:#25342f;background:#f5f7f6;color-scheme:light}*{box-sizing:border-box}body{margin:0;padding:24px}.card{max-width:760px;margin:32px auto;padding:32px;background:white;border:1px solid #dfe7e2;border-radius:22px;box-shadow:0 16px 50px #142d2110}h1{margin:8px 0;font-size:25px}p{line-height:1.65;color:#5f6e65}.badge{font-size:12px;color:#317453}.grid{display:grid;grid-template-columns:300px minmax(0,1fr);gap:28px;align-items:center}.qr svg{display:block;width:100%;height:auto}.code{letter-spacing:.18em;font-size:36px;color:#244f3d;font-weight:650}.timer{font-weight:600;color:#317453}.expired{color:#a83a39}.fingerprint{display:block;font-size:12px;line-height:1.8;overflow-wrap:anywhere}textarea{width:100%;min-height:94px;padding:12px;border:1px solid #dfe7e2;border-radius:10px;font:11px monospace;resize:vertical;background:#f8faf8;color:#45584d}button{border:0;border-radius:9px;background:#2d654a;color:white;font:inherit;padding:10px 18px;cursor:pointer}button:disabled{opacity:.5}details{margin-top:20px}summary{cursor:pointer}.footer{font-size:12px;margin-bottom:0}@media(max-width:620px){body{padding:12px}.card{margin:8px auto;padding:22px}.grid{grid-template-columns:1fr;gap:8px}.qr{max-width:300px;margin:auto}}
|
|
5901
|
+
</style></head><body><main class="card"><span class="badge">\u4F60\u7684\u7535\u8111 \xB7 \u7AEF\u5230\u7AEF\u52A0\u5BC6</span><h1>\u628A\u8FD9\u53F0\u7535\u8111\u8FDE\u63A5\u5230 Bot</h1><p>\u5728\u5DF2\u767B\u5F55\u7684\u63D2\u4EF6\u4E2D\u6253\u5F00\u201C\u5B89\u5168\u914D\u5BF9\u7535\u8111\u201D\uFF0C\u626B\u63CF\u6216\u7C98\u8D34\u4E0B\u65B9\u9080\u8BF7\u7801\uFF0C\u518D\u6838\u5BF9\u76EE\u6807\u7535\u8111\u7684\u786E\u8BA4\u7801\u3002</p><div class="grid"><div class="qr">${qr || "<p>\u8FD9\u4EFD\u9080\u8BF7\u5305\u542B\u8F83\u5927\u7684\u6388\u6743\u8303\u56F4\uFF0C\u8BF7\u590D\u5236\u4E0B\u65B9\u9080\u8BF7\u7801\u3002</p>"}</div><section><strong>${escape(invite.name)}</strong><p>${escape(invite.endpoint)}</p><div aria-label="\u516D\u4F4D\u786E\u8BA4\u7801" class="code">${escape(code)}</div><p id="timer" class="timer" role="timer">\u9080\u8BF7\u7801\u6709\u6548\u671F\u4E3A\u4E24\u5206\u949F</p><strong>\u8BBE\u5907\u6307\u7EB9</strong><code class="fingerprint">${escape(fingerprint)}</code></section></div><p>\u5171\u4EAB\u76EE\u5F55\uFF1A${escape(invite.ceiling.roots.join("\u3001"))}\uFF1B\u5141\u8BB8 ${invite.ceiling.tools.length} \u9879\u80FD\u529B\u3002\u8BF7\u5728\u63A5\u6536\u7AEF\u6838\u5BF9\u540C\u4E00\u8303\u56F4\u3002</p><label for="invite">\u8BBE\u5907\u9080\u8BF7\u7801\uFF08\u4E0D\u542B\u786E\u8BA4\u7801\uFF09</label><textarea id="invite" readonly spellcheck="false">${escape(payload)}</textarea><p><button id="copy" type="button">\u590D\u5236\u9080\u8BF7\u7801</button> <span id="feedback" role="status"></span></p><details><summary>\u67E5\u770B\u80FD\u529B\u8303\u56F4</summary><p>${escape(invite.ceiling.tools.join("\u3001"))}</p></details><p class="footer">\u6B64\u9875\u9762\u5728\u672C\u673A\u751F\u6210\uFF0C\u4E0D\u52A0\u8F7D\u8FDC\u7A0B\u8D44\u6E90\u3002\u9080\u8BF7\u7801\u8FC7\u671F\u540E\u8FD0\u884C pair-device.cmd \u91CD\u65B0\u751F\u6210\uFF1B\u5DF2\u914D\u5BF9\u7684\u8BBE\u5907\u9700\u5728\u76EE\u6807\u7535\u8111\u6838\u5BF9\u540E\u7BA1\u7406\u3002</p></main><script nonce="${nonce}">
|
|
5902
|
+
const expiresAt=${invite.expiresAt};const timer=document.getElementById('timer');const update=()=>{const remaining=Math.max(0,Math.ceil((expiresAt-Date.now())/1000));timer.textContent=remaining?'\u9080\u8BF7\u7801\u5269\u4F59 '+remaining+' \u79D2':'\u9080\u8BF7\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210';timer.classList.toggle('expired',!remaining);document.getElementById('copy').disabled=!remaining};update();setInterval(update,1000);
|
|
5903
|
+
document.getElementById('copy').onclick=async()=>{const field=document.getElementById('invite');try{await navigator.clipboard.writeText(field.value);document.getElementById('feedback').textContent='\u5DF2\u590D\u5236'}catch{field.focus();field.select();document.getElementById('feedback').textContent='\u8BF7\u6309 Ctrl+C \u590D\u5236\u9009\u4E2D\u7684\u9080\u8BF7\u7801'}};
|
|
5904
|
+
</script></body></html>`;
|
|
5905
|
+
return { html, qr: !!qr, payload };
|
|
5906
|
+
}
|
|
5907
|
+
async function writePairingPage(invite, destination) {
|
|
5908
|
+
if (!destination) fail("PAIRING_OUTPUT_REQUIRED");
|
|
5909
|
+
const page = resolve4(destination), json = `${page}.private.json`;
|
|
5910
|
+
for (const path4 of [page, json]) {
|
|
5911
|
+
const old = await lstat8(path4).catch((error2) => {
|
|
5912
|
+
if (error2.code === "ENOENT") return null;
|
|
5913
|
+
throw error2;
|
|
5914
|
+
});
|
|
5915
|
+
if (old && (!old.isFile() || old.isSymbolicLink() || old.nlink !== 1)) fail("PAIRING_OUTPUT_INVALID");
|
|
5916
|
+
}
|
|
5917
|
+
await mkdir7(dirname3(page), { recursive: true, mode: 448 });
|
|
5918
|
+
const result = await pairingPage(invite);
|
|
5919
|
+
for (const [path4, text] of [[json, JSON.stringify(JSON.parse(result.payload), null, 2) + "\n"], [page, result.html]]) {
|
|
5920
|
+
const temporary = `${path4}.${randomBytes7(8).toString("hex")}.tmp`;
|
|
5921
|
+
try {
|
|
5922
|
+
await writeFile6(temporary, text, { flag: "wx", mode: 384 });
|
|
5923
|
+
await rename2(temporary, path4);
|
|
5924
|
+
} finally {
|
|
5925
|
+
await rm(temporary, { force: true });
|
|
5926
|
+
}
|
|
5927
|
+
}
|
|
5928
|
+
return { machineId: invite.machineId, clientId: invite.clientId, inviteId: invite.inviteId, expiresAt: invite.expiresAt, transport: invite.transport, pairingFile: json, pageFile: page, qr: result.qr };
|
|
5929
|
+
}
|
|
5930
|
+
|
|
4428
5931
|
// src/cli.ts
|
|
4429
5932
|
var [command, configPath, ...args] = process.argv.slice(2);
|
|
4430
5933
|
try {
|
|
4431
|
-
if (!configPath) fail("USAGE: dsh-remote-agent init|serve|pair|rotate|revoke <config.json> [clientId ceiling.json]");
|
|
4432
|
-
const file =
|
|
5934
|
+
if (!configPath) fail("USAGE: dsh-remote-agent init|serve|identity|devices|invite|invite-page|pair|rotate|revoke <config.json> [clientId ceiling.json endpoint page.html]");
|
|
5935
|
+
const file = resolve5(configPath);
|
|
4433
5936
|
if (command === "init") {
|
|
4434
|
-
const root =
|
|
4435
|
-
const config = { dataDir: join9(
|
|
4436
|
-
await
|
|
4437
|
-
await
|
|
5937
|
+
const root = resolve5(args[0] ?? ".");
|
|
5938
|
+
const config = { dataDir: join9(dirname4(file), "data"), roots: { project: root }, isolation: "application", network: { mode: "loopback", host: "127.0.0.1", port: 9701 }, executables: { node: process.execPath }, browserOrigins: [] };
|
|
5939
|
+
await mkdir8(dirname4(file), { recursive: true, mode: 448 });
|
|
5940
|
+
await writeFile7(file, JSON.stringify(config, null, 2) + "\n", { flag: "wx", mode: 384 });
|
|
4438
5941
|
console.log(JSON.stringify({ configured: file, next: "Run pair with an explicit Bot/tool/root ceiling, then serve." }));
|
|
4439
5942
|
} else {
|
|
4440
|
-
const config = JSON.parse(await
|
|
5943
|
+
const config = JSON.parse(await readFile10(file, "utf8"));
|
|
4441
5944
|
if (command === "serve") {
|
|
5945
|
+
let invitation;
|
|
5946
|
+
if (args.length) {
|
|
5947
|
+
if (args[0] !== "--invite" || args.length !== 4) fail("USAGE: serve <config.json> --invite <clientId> <ceiling.json> <endpoint>");
|
|
5948
|
+
const local = new Store(config.dataDir, void 0, { admin: true });
|
|
5949
|
+
try {
|
|
5950
|
+
const ceiling = JSON.parse(await readFile10(resolve5(args[2]), "utf8"));
|
|
5951
|
+
for (const root of ceiling.roots ?? []) if (!config.roots[root]) fail("UNKNOWN_ROOT");
|
|
5952
|
+
invitation = local.invite(args[1], ceiling, { endpoint: args[3] });
|
|
5953
|
+
} finally {
|
|
5954
|
+
local.close();
|
|
5955
|
+
}
|
|
5956
|
+
}
|
|
4442
5957
|
const agent = new Agent(config);
|
|
4443
5958
|
let handle;
|
|
4444
5959
|
try {
|
|
@@ -4448,6 +5963,7 @@ try {
|
|
|
4448
5963
|
throw e;
|
|
4449
5964
|
}
|
|
4450
5965
|
console.log(JSON.stringify({ machineId: agent.store.machineId, listening: handle.address, protocols: handle.supportedMcpVersions }));
|
|
5966
|
+
if (invitation) console.log(JSON.stringify(invitation));
|
|
4451
5967
|
let stopping = false;
|
|
4452
5968
|
const stop = async () => {
|
|
4453
5969
|
if (stopping) return;
|
|
@@ -4461,11 +5977,15 @@ try {
|
|
|
4461
5977
|
const store = new Store(config.dataDir, void 0, { admin: true });
|
|
4462
5978
|
try {
|
|
4463
5979
|
let result;
|
|
4464
|
-
if (
|
|
4465
|
-
|
|
5980
|
+
if (["pair", "invite", "invite-page"].includes(command)) {
|
|
5981
|
+
if (!args[0] || !args[1] || command === "invite-page" && !args[3]) fail("PAIRING_ARGUMENTS_REQUIRED");
|
|
5982
|
+
const ceiling = JSON.parse(await readFile10(resolve5(args[1]), "utf8"));
|
|
4466
5983
|
for (const root of ceiling.roots ?? []) if (!config.roots[root]) fail("UNKNOWN_ROOT");
|
|
4467
|
-
result = store.pair(args[0], ceiling);
|
|
4468
|
-
|
|
5984
|
+
result = command === "pair" ? store.pair(args[0], ceiling) : store.invite(args[0], ceiling, { endpoint: args[2] });
|
|
5985
|
+
if (command === "invite-page") result = await writePairingPage(result, args[3]);
|
|
5986
|
+
} else if (command === "identity") result = { machineId: store.machineId, transport: store.pairing.identity() };
|
|
5987
|
+
else if (command === "devices") result = { machineId: store.machineId, devices: store.db.prepare("SELECT id,revision,revoked,ceiling FROM clients ORDER BY id").all().map((row) => ({ clientId: row.id, revision: row.revision, revoked: !!row.revoked, ceiling: JSON.parse(row.ceiling) })) };
|
|
5988
|
+
else if (command === "rotate") result = store.rotate(args[0]);
|
|
4469
5989
|
else if (command === "revoke") {
|
|
4470
5990
|
store.revoke(args[0]);
|
|
4471
5991
|
result = { revoked: true };
|