@ljwei-stak/dsh-model-route-bot 0.66.4 → 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 +18 -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/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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// Bounded JSON-RPC transport over a stdio-shaped pipe, and the pipe itself.
|
|
2
|
+
//
|
|
3
|
+
// Ported and re-grounded from OpenMausBot server/drivers/acp/host.ts at pin
|
|
4
|
+
// 1bc4749aa8307d7d6998bf86de4ef6478460dd93 (Apache-2.0); see licenses/NOTICE.md.
|
|
5
|
+
// The upstream host frames a child's stdout, maps request ids to pending
|
|
6
|
+
// promises, and fails every pending entry when the process ends. This module
|
|
7
|
+
// keeps that shape and makes every bound explicit: pending count, write
|
|
8
|
+
// backlog, per-request deadline and idle deadline are hard limits, and a
|
|
9
|
+
// duplicate, unknown or late response is counted rather than replayed.
|
|
10
|
+
|
|
11
|
+
import { EventEmitter } from 'node:events'
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_IDLE_TIMEOUT_MS, DEFAULT_MAX_BACKLOG_BYTES, DEFAULT_MAX_FRAME_BYTES, DEFAULT_MAX_PENDING,
|
|
15
|
+
DEFAULT_REQUEST_TIMEOUT_MS, MAX_LOG_PREVIEW, classifyMessage, createFrameDecoder, errorFromWire,
|
|
16
|
+
isValidMessageId, parseFrame, previewMessage,
|
|
17
|
+
} from './rpc-codec.mjs'
|
|
18
|
+
|
|
19
|
+
const SETTLED_ID_MEMORY = 4096
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A child process (or the native supervisor's stdio relay) as one pipe.
|
|
23
|
+
* `exit` resolves with `{ code, signal }` when the peer is really gone.
|
|
24
|
+
*/
|
|
25
|
+
export function createChildPipe({ stdin, stdout, exit, maxFrameBytes = DEFAULT_MAX_FRAME_BYTES, maxBacklogBytes = DEFAULT_MAX_BACKLOG_BYTES, log } = {}) {
|
|
26
|
+
if (!stdin || typeof stdin.write !== 'function') throw new TypeError('ACP_PIPE_STDIN_REQUIRED')
|
|
27
|
+
if (!stdout || typeof stdout.on !== 'function') throw new TypeError('ACP_PIPE_STDOUT_REQUIRED')
|
|
28
|
+
const events = new EventEmitter()
|
|
29
|
+
let closed = false
|
|
30
|
+
let queued = 0
|
|
31
|
+
let writes = 0, dropped = 0, writeErrors = 0
|
|
32
|
+
const decoder = createFrameDecoder({
|
|
33
|
+
maxFrameBytes,
|
|
34
|
+
onFrame: line => {
|
|
35
|
+
const message = parseFrame(line)
|
|
36
|
+
if (!message) {
|
|
37
|
+
events.emit('malformed', { reason: 'unparseable', preview: line.slice(0, MAX_LOG_PREVIEW) })
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
const classified = classifyMessage(message)
|
|
41
|
+
if (classified.kind === 'invalid') {
|
|
42
|
+
events.emit('malformed', { reason: classified.reason, preview: previewMessage(message) })
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
events.emit('message', classified)
|
|
46
|
+
},
|
|
47
|
+
onError: error => {
|
|
48
|
+
log?.({ event: 'acp.frame-limit', message: error.message })
|
|
49
|
+
events.emit('malformed', { reason: error.message, preview: '' })
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
stdout.on('data', chunk => decoder.write(chunk))
|
|
53
|
+
stdout.on('end', () => decoder.end())
|
|
54
|
+
stdout.on('error', error => { writeErrors++; events.emit('failure', error) })
|
|
55
|
+
stdin.on('error', () => { writeErrors++ })
|
|
56
|
+
const finish = (code = null, signal = null) => {
|
|
57
|
+
if (closed) return
|
|
58
|
+
closed = true
|
|
59
|
+
events.emit('closed', { code, signal })
|
|
60
|
+
}
|
|
61
|
+
if (typeof exit?.then === 'function') exit.then(value => finish(value?.code ?? null, value?.signal ?? null), () => finish(null, null))
|
|
62
|
+
const write = value => new Promise((resolve, reject) => {
|
|
63
|
+
const wire = `${JSON.stringify(value)}\n`
|
|
64
|
+
const bytes = Buffer.byteLength(wire)
|
|
65
|
+
// The frame bound is a local validation: an oversized value is refused
|
|
66
|
+
// whether or not the peer is still connected.
|
|
67
|
+
if (bytes > maxFrameBytes) { dropped++; reject(Object.assign(new Error('ACP_FRAME_LIMIT'), { acpFrameLimit: true })); return }
|
|
68
|
+
if (closed) { reject(Object.assign(new Error('ACP_PIPE_CLOSED'), { acpPipeClosed: true })); return }
|
|
69
|
+
queued += bytes
|
|
70
|
+
if (queued > maxBacklogBytes) {
|
|
71
|
+
queued -= bytes
|
|
72
|
+
dropped++
|
|
73
|
+
const error = Object.assign(new Error('ACP_BACKLOG_LIMIT'), { acpBacklogLimit: true })
|
|
74
|
+
events.emit('failure', error)
|
|
75
|
+
finish(null, null)
|
|
76
|
+
reject(error)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
writes++
|
|
80
|
+
stdin.write(wire, error => {
|
|
81
|
+
queued -= bytes
|
|
82
|
+
if (error) { writeErrors++; reject(error); return }
|
|
83
|
+
resolve()
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
/** Fire-and-forget send: a refusal is counted, never thrown into protocol code. */
|
|
87
|
+
const send = value => { void write(value).catch(() => {}) }
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
events,
|
|
90
|
+
write,
|
|
91
|
+
send,
|
|
92
|
+
end: () => { try { stdin.end() } catch {} },
|
|
93
|
+
get closed() { return closed },
|
|
94
|
+
get stats() { return Object.freeze({ ...decoder.stats, writes, dropped, writeErrors, queuedBytes: queued }) },
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Peer-exit and abort errors carry the evidence a caller needs to settle honestly. */
|
|
99
|
+
const peerExitError = (code, signal) => Object.assign(
|
|
100
|
+
new Error(`ACP agent process exited (code ${code ?? 'null'}, signal ${signal ?? 'null'})`),
|
|
101
|
+
{ acpPeerExit: true, exitCode: code, exitSignal: signal },
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The RPC layer: id-mapped requests, notifications and responses, with pending,
|
|
106
|
+
* deadline, idle and abort limits. Inbound requests and notifications are
|
|
107
|
+
* emitted for the session layer to answer; this layer never decides policy.
|
|
108
|
+
*/
|
|
109
|
+
export function createStdioRpc({ pipe, maxPending = DEFAULT_MAX_PENDING, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS, signal, route, log } = {}) {
|
|
110
|
+
if (!pipe || typeof pipe.write !== 'function' || !pipe.events) throw new TypeError('ACP_RPC_PIPE_REQUIRED')
|
|
111
|
+
if (!Number.isSafeInteger(maxPending) || maxPending < 1) throw new TypeError('ACP_PENDING_LIMIT_INVALID')
|
|
112
|
+
const pending = new Map()
|
|
113
|
+
const settled = new Set()
|
|
114
|
+
let nextId = 1
|
|
115
|
+
let closed = false
|
|
116
|
+
let lastInboundAt = null
|
|
117
|
+
const stats = { requests: 0, responses: 0, notifications: 0, inboundRequests: 0, inboundNotifications: 0, unknownResponses: 0, duplicateResponses: 0, lateResponses: 0, malformed: 0, timeouts: 0, idleTimeouts: 0, aborted: 0, pendingLimitRefusals: 0, peerExits: 0 }
|
|
118
|
+
|
|
119
|
+
const remember = id => {
|
|
120
|
+
settled.add(id)
|
|
121
|
+
if (settled.size > SETTLED_ID_MEMORY) settled.delete(settled.values().next().value)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const clearTimers = entry => {
|
|
125
|
+
if (entry.timer) clearTimeout(entry.timer)
|
|
126
|
+
if (entry.idleTimer) clearTimeout(entry.idleTimer)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const settleEntry = (entry, error) => {
|
|
130
|
+
if (!pending.delete(entry.id)) return false
|
|
131
|
+
clearTimers(entry)
|
|
132
|
+
remember(entry.id)
|
|
133
|
+
entry.reject(error)
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const touch = (owner = null) => {
|
|
138
|
+
lastInboundAt = Date.now()
|
|
139
|
+
for (const entry of pending.values()) {
|
|
140
|
+
// A session's own traffic restarts only its own deadlines; a process-level
|
|
141
|
+
// request (issued before any session exists) restarts on any traffic.
|
|
142
|
+
if (entry.owner === null || entry.owner === owner) entry.armIdle()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const failAll = (error, cause) => {
|
|
147
|
+
for (const entry of [...pending.values()]) {
|
|
148
|
+
if (!pending.delete(entry.id)) continue
|
|
149
|
+
clearTimers(entry)
|
|
150
|
+
remember(entry.id)
|
|
151
|
+
entry.reject(error)
|
|
152
|
+
}
|
|
153
|
+
if (cause === 'peer-exit') stats.peerExits++
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Reject every pending request owned by one session; other sessions go on. */
|
|
157
|
+
const failOwner = (owner, error) => {
|
|
158
|
+
if (typeof owner !== 'string' || !owner) return 0
|
|
159
|
+
let failed = 0
|
|
160
|
+
for (const entry of [...pending.values()]) {
|
|
161
|
+
if (entry.owner !== owner) continue
|
|
162
|
+
if (!pending.delete(entry.id)) continue
|
|
163
|
+
clearTimers(entry)
|
|
164
|
+
remember(entry.id)
|
|
165
|
+
entry.reject(error)
|
|
166
|
+
failed++
|
|
167
|
+
}
|
|
168
|
+
return failed
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const close = (reason = 'closed') => {
|
|
172
|
+
if (closed) return
|
|
173
|
+
closed = true
|
|
174
|
+
failAll(Object.assign(new Error(`ACP connection ${reason}`), { acpClosed: true }), 'close')
|
|
175
|
+
pipe.events.off('message', onMessage)
|
|
176
|
+
pipe.events.off('malformed', onMalformed)
|
|
177
|
+
pipe.events.off('failure', onFailure)
|
|
178
|
+
pipe.events.off('closed', onClosed)
|
|
179
|
+
pipe.end()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function onMessage(classified) {
|
|
183
|
+
touch(typeof route === 'function' ? route(classified) ?? null : null)
|
|
184
|
+
if (classified.kind === 'response') {
|
|
185
|
+
stats.responses++
|
|
186
|
+
const entry = pending.get(classified.id)
|
|
187
|
+
if (!entry) {
|
|
188
|
+
// A response for an id this side never issued, or already settled.
|
|
189
|
+
if (settled.has(classified.id)) { stats.duplicateResponses++; stats.lateResponses++ } else stats.unknownResponses++
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
pending.delete(classified.id)
|
|
193
|
+
clearTimers(entry)
|
|
194
|
+
remember(classified.id)
|
|
195
|
+
if (classified.error !== undefined) {
|
|
196
|
+
entry.reject(errorFromWire(classified.error, entry.method))
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
entry.onReceive?.(classified.result)
|
|
200
|
+
entry.resolve(classified.result)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
if (classified.kind === 'request') {
|
|
204
|
+
stats.inboundRequests++
|
|
205
|
+
pipe.events.emit('rpc-request', classified)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
stats.inboundNotifications++
|
|
209
|
+
pipe.events.emit('rpc-notification', classified)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function onMalformed(detail) {
|
|
213
|
+
stats.malformed++
|
|
214
|
+
log?.({ event: 'acp.malformed', reason: detail?.reason ?? 'unknown' })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function onFailure(error) {
|
|
218
|
+
log?.({ event: 'acp.pipe-failure', message: error?.message ?? String(error) })
|
|
219
|
+
failAll(error instanceof Error ? error : new Error(String(error)), 'failure')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function onClosed(detail) {
|
|
223
|
+
if (closed) return
|
|
224
|
+
closed = true
|
|
225
|
+
failAll(peerExitError(detail?.code ?? null, detail?.signal ?? null), 'peer-exit')
|
|
226
|
+
pipe.events.off('message', onMessage)
|
|
227
|
+
pipe.events.off('malformed', onMalformed)
|
|
228
|
+
pipe.events.off('failure', onFailure)
|
|
229
|
+
pipe.events.off('closed', onClosed)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
pipe.events.on('message', onMessage)
|
|
233
|
+
pipe.events.on('malformed', onMalformed)
|
|
234
|
+
pipe.events.on('failure', onFailure)
|
|
235
|
+
pipe.events.on('closed', onClosed)
|
|
236
|
+
|
|
237
|
+
const onAbort = () => close('aborted')
|
|
238
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
239
|
+
if (signal?.aborted) onAbort()
|
|
240
|
+
|
|
241
|
+
return Object.freeze({
|
|
242
|
+
/** Peer-exit and abort errors carry the evidence a caller needs to settle honestly. */
|
|
243
|
+
/** The inbound event bus: `rpc-request`, `rpc-notification`, `malformed`, `failure`, `closed`. */
|
|
244
|
+
events: pipe.events,
|
|
245
|
+
request(method, params, options = {}) {
|
|
246
|
+
if (closed) return Promise.reject(Object.assign(new Error('ACP connection closed'), { acpClosed: true }))
|
|
247
|
+
if (typeof method !== 'string' || !method) return Promise.reject(Object.assign(new TypeError('ACP_METHOD_REQUIRED'), { acpInvalidRequest: true }))
|
|
248
|
+
if (pending.size >= maxPending) {
|
|
249
|
+
stats.pendingLimitRefusals++
|
|
250
|
+
return Promise.reject(Object.assign(new Error(`ACP pending limit reached (${maxPending})`), { acpPendingLimit: true }))
|
|
251
|
+
}
|
|
252
|
+
if (options.signal?.aborted) return Promise.reject(Object.assign(new Error('ACP request aborted'), { acpAborted: true }))
|
|
253
|
+
const timeoutMs = Number.isSafeInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : requestTimeoutMs
|
|
254
|
+
const idleMs = Number.isSafeInteger(options.idleMs) && options.idleMs > 0 ? options.idleMs : idleTimeoutMs
|
|
255
|
+
const id = nextId++
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
257
|
+
const entry = {
|
|
258
|
+
id, method, resolve, reject,
|
|
259
|
+
owner: typeof options.owner === 'string' && options.owner ? options.owner : null,
|
|
260
|
+
onReceive: typeof options.onReceive === 'function' ? options.onReceive : null,
|
|
261
|
+
timer: null, idleTimer: null, idleMs,
|
|
262
|
+
armIdle: () => {
|
|
263
|
+
if (!(entry.idleMs > 0)) return
|
|
264
|
+
if (entry.idleTimer) clearTimeout(entry.idleTimer)
|
|
265
|
+
entry.idleTimer = setTimeout(() => {
|
|
266
|
+
if (!pending.delete(id)) return
|
|
267
|
+
remember(id)
|
|
268
|
+
stats.idleTimeouts++
|
|
269
|
+
reject(Object.assign(new Error(`${method} stopped responding`), { acpIdleTimeout: true }))
|
|
270
|
+
}, entry.idleMs)
|
|
271
|
+
entry.idleTimer.unref?.()
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
entry.timer = setTimeout(() => {
|
|
275
|
+
if (!pending.delete(id)) return
|
|
276
|
+
remember(id)
|
|
277
|
+
stats.timeouts++
|
|
278
|
+
reject(Object.assign(new Error(`${method} timed out after ${timeoutMs}ms`), { acpRequestTimeout: true }))
|
|
279
|
+
}, timeoutMs)
|
|
280
|
+
entry.timer.unref?.()
|
|
281
|
+
entry.armIdle()
|
|
282
|
+
pending.set(id, entry)
|
|
283
|
+
stats.requests++
|
|
284
|
+
const onRequestAbort = () => {
|
|
285
|
+
if (!pending.delete(id)) return
|
|
286
|
+
clearTimers(entry)
|
|
287
|
+
remember(id)
|
|
288
|
+
stats.aborted++
|
|
289
|
+
reject(Object.assign(new Error(`${method} aborted`), { acpAborted: true }))
|
|
290
|
+
}
|
|
291
|
+
if (options.signal) {
|
|
292
|
+
entry.onAbort = onRequestAbort
|
|
293
|
+
options.signal.addEventListener('abort', onRequestAbort, { once: true })
|
|
294
|
+
}
|
|
295
|
+
pipe.write({ jsonrpc: '2.0', id, method, params: params === undefined ? {} : params }).catch(error => {
|
|
296
|
+
if (!pending.delete(id)) return
|
|
297
|
+
clearTimers(entry)
|
|
298
|
+
remember(id)
|
|
299
|
+
if (options.signal) options.signal.removeEventListener?.('abort', onRequestAbort)
|
|
300
|
+
reject(error)
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
},
|
|
304
|
+
/** A notification: no id, no reply expected. */
|
|
305
|
+
notify(method, params) {
|
|
306
|
+
if (closed || typeof method !== 'string' || !method) return false
|
|
307
|
+
stats.notifications++
|
|
308
|
+
pipe.send({ jsonrpc: '2.0', method, params: params === undefined ? {} : params })
|
|
309
|
+
return true
|
|
310
|
+
},
|
|
311
|
+
/** Answer a server request. Unknown ids are the peer's problem, not ours. */
|
|
312
|
+
respond(id, result) {
|
|
313
|
+
if (closed || !isValidMessageId(id)) return false
|
|
314
|
+
pipe.send({ jsonrpc: '2.0', id, result: result === undefined ? {} : result })
|
|
315
|
+
return true
|
|
316
|
+
},
|
|
317
|
+
respondError(id, code, message, data) {
|
|
318
|
+
if (closed || !isValidMessageId(id)) return false
|
|
319
|
+
pipe.send({ jsonrpc: '2.0', id, error: { code, message, ...(data !== undefined ? { data } : {}) } })
|
|
320
|
+
return true
|
|
321
|
+
},
|
|
322
|
+
get pendingCount() { return pending.size },
|
|
323
|
+
failOwner,
|
|
324
|
+
get closed() { return closed },
|
|
325
|
+
get lastInboundAt() { return lastInboundAt },
|
|
326
|
+
get stats() { return Object.freeze({ ...stats, pending: pending.size }) },
|
|
327
|
+
close,
|
|
328
|
+
})
|
|
329
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# packages/bot-engines
|
|
2
|
+
|
|
3
|
+
Native Bot conversation engines: a closed catalog of the 26 upstream OpenMausBot
|
|
4
|
+
built-in drivers, plus the first ported engine — the **Claude** conversation-session
|
|
5
|
+
adapter, its public LLM bridge and Host-owned registration. The product can explicitly
|
|
6
|
+
configure a pinned Claude text/subscription route and select it through the existing Bot
|
|
7
|
+
model binding. Serving, grants, request history and the one money authority remain in the Host.
|
|
8
|
+
|
|
9
|
+
Status: `claude` is implemented and covered by real offline child-process tests. Every
|
|
10
|
+
other catalog entry is explicitly **unavailable** with a concrete reason and target
|
|
11
|
+
capability. Custom ACP engines remain in migration scope: the generic ACP core now exists
|
|
12
|
+
in `packages/bot-acp` (session lifecycle, permissions, bounded stdio transport, native
|
|
13
|
+
launch — offline-verified, **not** wired into product composition and no ACP CLI claimed
|
|
14
|
+
usable), so `customAcp` is gated on trusted owner registration of pinned entries, grants
|
|
15
|
+
and the permission broker rather than on the core itself.
|
|
16
|
+
|
|
17
|
+
## Modules
|
|
18
|
+
|
|
19
|
+
| File | Purpose |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `src/driver-catalog.mjs` | Closed 26-entry catalog (`BOT_ENGINE_DRIVERS`, `resolveBotEngine`, `assertNoAdHocDriverInput`) |
|
|
22
|
+
| `src/claude-engine.mjs` | `createClaudeEngine` / `pinClaudeEntry` — the conversation-session adapter |
|
|
23
|
+
| `src/claude-protocol.mjs` | Pure stream-json helpers (frame classification, usage/cost math, broker wire) |
|
|
24
|
+
| `src/claude-permission-broker.mjs` | Host-side ask/answer broker (named pipe / unix socket, fail-closed) |
|
|
25
|
+
| `src/claude-permission-proxy.mjs` | MCP stdio `approve` proxy mounted as `mcp__dsh` in the CLI's `--mcp-config` |
|
|
26
|
+
| `src/claude-llm-turn.mjs` | Pure request projection for the LLM route (capability gate, retained-history continuity, usage/finish mapping) |
|
|
27
|
+
| `src/claude-llm-adapter.mjs` | `ClaudeLlmAdapter` / `createClaudeLlmAdapter` — the public `LlmAdapter` bridge over the retained Claude session |
|
|
28
|
+
| `src/host-registrations.mjs` | Closed Host profile validation, public model registration and private CLI credential provisioning |
|
|
29
|
+
| `licenses/NOTICE.md`, `licenses/LICENSE-OpenMausBot` | Upstream attribution and full Apache-2.0 license (pin `1bc4749a…`) |
|
|
30
|
+
|
|
31
|
+
## Policy invariants
|
|
32
|
+
|
|
33
|
+
- **Closed catalog.** Exactly the 26 upstream built-in driver ids exist. Unknown ids are
|
|
34
|
+
an error, not a fallback. `customAcp` awaits the generic ACP core in `packages/bot-acp`
|
|
35
|
+
(present, offline-verified, unwired) plus trusted owner registration of pinned entries,
|
|
36
|
+
grants and the permission broker. Turn/model request JSON cannot register a command.
|
|
37
|
+
- **Authority is Host state.** Model routes, tools, executable paths, digests,
|
|
38
|
+
environments and credentials never arrive through turn payloads. A driver request
|
|
39
|
+
carrying any of `executable/path/command/endpoint/url/args/env/credentials/apiKey/
|
|
40
|
+
token/autoApprove/alwaysAllow/bypassPermissions` is rejected (`HOST_OWNED_ENGINE_INPUT`).
|
|
41
|
+
- **Fail-closed permissions.** `bypassPermissions` is not an available mode. `--tools ""`
|
|
42
|
+
disables built-in tools, including reads and edits that otherwise need no prompt.
|
|
43
|
+
`--allowedTools` only pre-approves the configured MCP tools. Only `mcp__dsh_task__<grantedTool>`
|
|
44
|
+
permission asks can ever be allowed; everything else (e.g. `Bash`) is auto-denied by the
|
|
45
|
+
engine without reaching the host. Duplicate ask ids, post-close asks and unanswered
|
|
46
|
+
permission asks all deny.
|
|
47
|
+
- **Configuration boundary.** `--setting-sources ""` excludes user/project/local
|
|
48
|
+
settings, `disableAllHooks` disables their hooks, and `--disable-slash-commands`
|
|
49
|
+
disables skills and commands. The private home and strict MCP config are retained.
|
|
50
|
+
Machine-managed CLI policy still applies; this is not a filesystem sandbox.
|
|
51
|
+
Subscription credentials are provisioned into the private home by the Host; bare
|
|
52
|
+
mode is not used because it ignores OAuth login. A CLI lacking required flags must
|
|
53
|
+
fail startup; it must not retry with broader settings.
|
|
54
|
+
- **One ledger.** Cost/usage figures flow only to the Host hooks (`onBeforeRequest`,
|
|
55
|
+
`onObserve`, `onSettle`, `onClose`). Values the CLI does not report stay `null` /
|
|
56
|
+
`'unknown'`; nothing is fabricated here.
|
|
57
|
+
- **Every turn re-checks authorization** (admission hook, model route, grant revision,
|
|
58
|
+
broker liveness), and a refusal before dispatch provably dispatches nothing.
|
|
59
|
+
- **Unknown outcomes are never blindly replayed.** A `--resume` that the CLI refuses
|
|
60
|
+
(error result before init, or exit before any frame) settles `resume-refused` and stops
|
|
61
|
+
— the earlier task is never replayed into a fresh session.
|
|
62
|
+
- **Stop requests are not stop confirmations.** `treeStopped: true` appears only from the
|
|
63
|
+
launcher's own accounting (Windows job object `ActiveProcesses == 0`).
|
|
64
|
+
|
|
65
|
+
## Turn lifecycle fence
|
|
66
|
+
|
|
67
|
+
- **The turn slot is reserved synchronously.** `submitTurn` takes the single slot before
|
|
68
|
+
its first `await` (the admission hook). A second submit in the same tick — or while
|
|
69
|
+
admission, launch, dispatch, result or the Host settlement is pending — is refused
|
|
70
|
+
`ENGINE_TURN_IN_PROGRESS` with zero extra admissions and zero launches. The slot is
|
|
71
|
+
freed only by a completed Host settlement (or an admission refusal, which provably
|
|
72
|
+
dispatched nothing); a legal next turn may then be submitted.
|
|
73
|
+
- **The Host settlement completes the turn.** The public `submitTurn` promise and the
|
|
74
|
+
`turn` event fire only after the unique `onSettle` callback ran. If the settlement
|
|
75
|
+
hook throws, the turn promise rejects with `ENGINE_SETTLE_HOOK_FAILED`, the native
|
|
76
|
+
evidence attached untouched (`error.turnResult`), `fundingStatus: 'unknown'` (the CLI
|
|
77
|
+
really may have run), an `turn-settle-failed` event is emitted, and the engine stops:
|
|
78
|
+
no next turn, no fresh process pretending success, never a replay.
|
|
79
|
+
- **Post-admission failures settle exactly once**, each receipt bound to its `turnId`
|
|
80
|
+
and the engine's `operationId` (no second ledger): a failure provably before the
|
|
81
|
+
prompt was written to the CLI's stdin settles `not-run` with `usage: null` and
|
|
82
|
+
`costUsd: null` (nothing is invented); a failed stdin write settles `unknown`
|
|
83
|
+
(may-dispatch). A failed write does not kill the engine — the retained process may
|
|
84
|
+
serve the next turn.
|
|
85
|
+
- **Old generations cannot settle new turns.** A `result` frame only settles the turn
|
|
86
|
+
currently in flight on the process that produced it; frames from a dead or former
|
|
87
|
+
process generation are dropped at that state gate (they do not even count as strays),
|
|
88
|
+
and a late twin result after the settle gate is counted (`strayResults`), never applied.
|
|
89
|
+
- **stop() ends the native tree first**, independently of every Host hook: the current
|
|
90
|
+
process and any still-pending launch are ended before waiting on the settlement chain,
|
|
91
|
+
the `tree-stopped` event carries the supervisor's job-object evidence while a slow
|
|
92
|
+
settlement is still pending, and `stop()` itself resolves only after the settlement
|
|
93
|
+
chain and `onClose` ran. A trusted hook that never returns leaves `stop()` pending —
|
|
94
|
+
observable through `snapshot().settlementPending` — but the tree above is already down.
|
|
95
|
+
- **`hooks.onObserve` is observation, not the ledger.** Its failures are counted
|
|
96
|
+
(`snapshot().observeHookFailures`) and emitted (`observe-failed`), never silently
|
|
97
|
+
swallowed, and never a turn or financial outcome.
|
|
98
|
+
|
|
99
|
+
## The Claude engine in one page
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
createClaudeEngine({
|
|
103
|
+
authority, subject, binding, operationId, // trusted Host state
|
|
104
|
+
entry: { executable, sha256, version, // digest-pinned native entry
|
|
105
|
+
script?, scriptSha256? }, // (script: pinned cli.js-style entry)
|
|
106
|
+
runtimeRoot, workspace, // isolated dirs
|
|
107
|
+
model: { catalogProvider, catalogModel }, // asserted against the authority
|
|
108
|
+
permissionMode: 'default'|'acceptEdits'|'auto', // bypassPermissions refused
|
|
109
|
+
tools: ['read_notes'], handlers: [...], // committed-grant MCP bridge
|
|
110
|
+
hooks: { onBeforeRequest, onObserve, onSettle, onClose },
|
|
111
|
+
onPermissionRequest?, provisionCredentials?, deadlineMs, signal, launcher?,
|
|
112
|
+
})
|
|
113
|
+
→ { submitTurn({text}), respondToRequest(id, {behavior,message?}), stop(), snapshot(), events }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- **Session**: one retained CLI process per session (`-p --output-format stream-json
|
|
117
|
+
--input-format stream-json --verbose --include-partial-messages --strict-mcp-config`).
|
|
118
|
+
The `system/init` frame confirms the session id and the model; a model mismatch is
|
|
119
|
+
fatal. After a process exit the next turn relaunches with `--resume <session_id>`.
|
|
120
|
+
- **Turns**: `submitTurn({ text })` — text (1..64000 bytes, no NUL) is the ONLY accepted
|
|
121
|
+
field. Each turn is settled by the CLI's own `result` frame.
|
|
122
|
+
- **Cost**: `total_cost_usd` is a running total; a turn's cost is the delta since the
|
|
123
|
+
previous result (first reading taken whole; a shrinking total taken whole — the CLI may
|
|
124
|
+
restore an older baseline after `--resume`).
|
|
125
|
+
- **Stop evidence**: `{status, pid, exitCode, nativeStopReason, containment, treeStopped,
|
|
126
|
+
forcedKill, stopRequestedIsNotConfirmation}` — `status: 'confirmed'` only when the
|
|
127
|
+
launcher's job-object accounting says the whole tree ended.
|
|
128
|
+
|
|
129
|
+
## The Claude LLM bridge (`claude-llm-adapter.mjs`)
|
|
130
|
+
|
|
131
|
+
The retained Claude session as a **public SDK route**: a `LlmAdapter` the Host registers
|
|
132
|
+
with the installed `ctx.llm.registerAdapter([provider], adapter)`, so one Bot conversation
|
|
133
|
+
becomes one `provider/model` route that the Host's own `llm/stream` waterfall — the same one
|
|
134
|
+
an integrated turn goes through — admits, prices and receipts. The bridge holds no money and
|
|
135
|
+
issues no receipt of its own; it is not a second chat surface and not a second ledger.
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
createClaudeLlmAdapter({
|
|
139
|
+
provider, model, // the one committed route
|
|
140
|
+
funding: 'subscription', // the only open kind
|
|
141
|
+
entry | entryFor, // Host pinned installation reference
|
|
142
|
+
runtimeRoot, workspace | workspaceFor, // isolated absolute dirs
|
|
143
|
+
identityFor: ({provider, model, sessionId}) => ({ // trusted Host resolver
|
|
144
|
+
subject, botId, authority, binding, persona?,
|
|
145
|
+
}),
|
|
146
|
+
hooks, onPermissionRequest, provisionCredentials, // Host state, never request state
|
|
147
|
+
tools, handlers, permissionMode, deadlineMs, launcher, engineFactory,
|
|
148
|
+
})
|
|
149
|
+
→ LlmAdapter { listModels, resolveModel, prepareCall, stream, replaceRoute, close, snapshot }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- **Trust boundary.** Only a Host constructs it. A request may carry
|
|
153
|
+
`provider/model/messages/system/signal/sessionId` and nothing else: an extra field
|
|
154
|
+
(identity, path, command, credential, engine configuration) is refused
|
|
155
|
+
(`CLAUDE_ROUTE_UNKNOWN_REQUEST_FIELD`), and so are `tools`, `toolHistory`, `maxTokens`,
|
|
156
|
+
`reasoningEffort`, `temperature`, `stop` and `purpose` — capabilities this route cannot
|
|
157
|
+
honour and therefore does not silently ignore.
|
|
158
|
+
- **One session per conversation.** `stream(options)` is one turn: the trailing new user
|
|
159
|
+
text is projected out of the request history (the retained CLI already holds everything
|
|
160
|
+
before it) and dispatched on the one native session. Continuity is proved by durable
|
|
161
|
+
message id and by the SDK's own model provenance; a rewritten prefix, an unseen history, a
|
|
162
|
+
repeated dispatch, a changed Bot/model/persona/grant revision or a different route
|
|
163
|
+
generation retire the old session and refuse — never a quiet drop and never a replay.
|
|
164
|
+
- **Honest outcomes.** Usage the CLI does not report emits no `usage` chunk; a turn with no
|
|
165
|
+
output, an `unknown` outcome or a cancellation is an `error`/`aborted` finish, never a
|
|
166
|
+
success. Exactly one finish per turn, emitted only after the engine's own Host settlement
|
|
167
|
+
ran. Model provenance in the replay state proves the CLI-configured init only.
|
|
168
|
+
- **Cancellation belongs to the turn.** The session is never bound to the first turn's
|
|
169
|
+
short-lived signal; cancelling stops the native tree, waits for the engine's settlement and
|
|
170
|
+
retires the session. `close()` refuses further calls, waits for an engine that is still
|
|
171
|
+
being created and stops every live tree — it creates nothing.
|
|
172
|
+
|
|
173
|
+
## Tests (offline, real child processes)
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
node --test --test-concurrency=1 tests/bot-engine-llm-adapter.test.mjs # 33 pure cases, no process
|
|
177
|
+
node --test --test-concurrency=1 tests/bot-engine-llm-adapter.native.mjs # 9 native cases
|
|
178
|
+
node --test --test-concurrency=1 tests/bot-engine-catalog.test.mjs tests/bot-engine-protocol.test.mjs tests/bot-engine-claude-session.native.mjs
|
|
179
|
+
node scripts/verify-bot-engines.mjs
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The native suite installs the real Cordis + `@deepseek-ai/dsh-llm` `LlmRuntime`, registers
|
|
183
|
+
this adapter through the public `registerAdapter`/`prepareCall` seam, and drives the real
|
|
184
|
+
`createClaudeEngine` (no engine-factory seam) through the real native-peer supervisor into a
|
|
185
|
+
real Node child (`tests/fixtures/bot-engine-claude-fixture.mjs`). It proves: two turns on one
|
|
186
|
+
retained PID with only the new input reaching the CLI, a same-tick second turn refused by the
|
|
187
|
+
engine fence, a refused `--resume` that is never a replay, a turn lost to a dead process
|
|
188
|
+
refused as unknown, cancellation and teardown, a route re-registration refused on the
|
|
189
|
+
captured generation, a revoked grant retiring the native tree, an API-funded route refused
|
|
190
|
+
before anything launches, and the parent's real `createModelAccounting` recording exactly one
|
|
191
|
+
receipt with the CLI-reported usage and no invented cost.
|
|
192
|
+
|
|
193
|
+
No claude CLI, no network. The native suite drives a real Node fixture
|
|
194
|
+
(`tests/fixtures/bot-engine-claude-fixture.mjs`) through the real PowerShell
|
|
195
|
+
native-peer `supervise` launcher: two-turn retained session, permission allow/deny/
|
|
196
|
+
question flows, out-of-band host answers, missing usage, utf8/oversize/split/stray
|
|
197
|
+
frames, model mismatch, admission refusal with zero dispatch, mid-turn revoke, host stop
|
|
198
|
+
and abort with job-object tree-stop evidence, and both resume-refusal paths. Two further
|
|
199
|
+
groups prove the turn-lifecycle fence: a native group (same-tick double submit, stray
|
|
200
|
+
results during a pending settlement, a failing settlement hook, tree stop ahead of a
|
|
201
|
+
slow settlement, a failing observation hook) and a launcher-seam group whose gated
|
|
202
|
+
in-memory supervisor exercises the states a healthy process never produces (stuck
|
|
203
|
+
admission, stop during admission/launch, stdin write failure, a throwing `supervise()`,
|
|
204
|
+
a tampered entry script, late frames from a dead generation). Gates are explicit
|
|
205
|
+
promises; the seam models the pipe's causal order (frames cannot overtake the stdin
|
|
206
|
+
write completion). Run serially (`--test-concurrency=1`); the machine has memory limits.
|
|
207
|
+
|
|
208
|
+
## Remaining ports and acceptance
|
|
209
|
+
|
|
210
|
+
- **ACP products**: the generic core is implemented; trusted owner registration and
|
|
211
|
+
real-driver integration are still required for the upstream ACP drivers.
|
|
212
|
+
- **Other engine ports** (`pending-engine-port`): codex, museCode, pi.
|
|
213
|
+
- **API providers** (8): stateless OpenAI chat-completions channels — they belong to the
|
|
214
|
+
Host model-routing channel, not to conversation engines.
|
|
215
|
+
- **Host acceptance**: configured Claude routes have whole-product offline native tests.
|
|
216
|
+
Live installed CLI/account acceptance, broader controls and remaining engines are pending.
|
|
217
|
+
|
|
218
|
+
## Configured product route
|
|
219
|
+
|
|
220
|
+
The unreleased Bot plugin accepts a Host-owned `conversationEngines` array. Each profile
|
|
221
|
+
has `id`, `driver: 'claude'`, a unique `provider`, exact `model`, optional `name`, pinned
|
|
222
|
+
`entry` (`executable`, `sha256`, `version`, optional `script`/`scriptSha256`),
|
|
223
|
+
`credentialRef: 'ds-secret/NAME'`, optional `deadlineMs` (default 30 minutes), and
|
|
224
|
+
optional `hostTools` (default false).
|
|
225
|
+
The entry comes from installation verification; this setting neither installs nor logs in.
|
|
226
|
+
|
|
227
|
+
`credentialRef` selects an explicit Host-only secret with an empty Bot scope. Its value
|
|
228
|
+
is the opaque JSON content of the selected CLI credential profile. The Host validates it
|
|
229
|
+
and writes it to the protected engine home under `.claude/.credentials.json`; ambient
|
|
230
|
+
user accounts are not discovered. Changes/removal retire the old Bot session before the
|
|
231
|
+
next request. The Host-confirmed Bot persona uses a private `system-prompt.txt` through
|
|
232
|
+
`--append-system-prompt-file`. Neither credential nor persona contents go in process argv.
|
|
233
|
+
|
|
234
|
+
Enable the independent runtime and choose the configured model in the existing Bot
|
|
235
|
+
settings. The trusted catalog permits subscription funding for this route. It exposes
|
|
236
|
+
text conversation by default. With `hostTools: true`, the native process also
|
|
237
|
+
receives a private MCP server for the Bot's available file, approved container
|
|
238
|
+
command, browser, desktop, operation, artifact, Task and data tools. The catalog is resolved when a native
|
|
239
|
+
session starts. Start a new conversation to refresh it after adding tools. Each
|
|
240
|
+
call rechecks current route funding, credentials, committed Bot grants and paired
|
|
241
|
+
computer scope. File writes and container commands use the existing Task service. A retained child can call
|
|
242
|
+
tools only during its admitted turn. Cancellation reaches the MCP handler and
|
|
243
|
+
requests cancellation of a submitted Task when the Bot has that permission. Its
|
|
244
|
+
durable record retains the actual outcome. Bridge request IDs are single-use
|
|
245
|
+
within a process connection. An uncertain write returns its original Task ID
|
|
246
|
+
and a reconciliation instruction so the model can query `bot_task_status`.
|
|
247
|
+
Browser tools open, snapshot, type, click and close an Agent browser. Desktop tools capture PNG,
|
|
248
|
+
click, type, press keys and scroll. The Host stores conversation-owned operation identities before
|
|
249
|
+
dispatch. Unknown mutations fence later mutations on that target until `bot_remote_operation_status`
|
|
250
|
+
or `bot_remote_operation_cancel` reads the original Agent operation. Browser sessions and screenshot
|
|
251
|
+
references belong to the Bot conversation that created them. Screenshots use the Agent artifact
|
|
252
|
+
channel and arrive as MCP image content, chunked over the private pipe with bounded reassembly.
|
|
253
|
+
Verbose CLI `user/tool_result` image echoes are reduced to size and SHA256 receipts before Host
|
|
254
|
+
events are emitted. The CLI receives the original pixels through MCP. Echo parsing allows bounded
|
|
255
|
+
image frames while retaining the ordinary text frame and session output limits.
|
|
256
|
+
The five data tools share their descriptions and business argument projection with the SDK registry.
|
|
257
|
+
They use the Bot's existing private data engine and controlled library under its committed `tool/data`
|
|
258
|
+
grant, in both chat modes. `data_show.chart` has a validated nested object schema. Saved tables and
|
|
259
|
+
cards are readable in the Data panel, and CSV exports are written to the same Bot library. Current
|
|
260
|
+
support is CSV/TSV/JSON, SQLite, bar/line/area and CSV export. Native cancellation reaches the fixed
|
|
261
|
+
data worker; a cancelled library read cannot later start a table write. Committed changes are not
|
|
262
|
+
rolled back by cancellation, and failed or timed-out data calls are never automatically retried.
|
|
263
|
+
DuckDB and remaining data formats/charts/exports, team tools, browser script/upload/download and rich
|
|
264
|
+
native tool transcripts remain pending.
|
|
265
|
+
Independent chat now supports explicit new conversations. A persisted reset intent blocks
|
|
266
|
+
new turns until the old native tree is proven stopped (or proven never started); unknown
|
|
267
|
+
results stay blocked across restart. Old transcripts remain readable. Whole-product native
|
|
268
|
+
tests prove the next explicit send launches a fresh process with only the new context,
|
|
269
|
+
and resetting itself creates no model receipt. Live account onboarding remains pending. See
|
|
270
|
+
`tests/product-conversation-engine.test.mjs` for the offline whole-product acceptance.
|
|
271
|
+
|
|
272
|
+
The same text route now accepts requests made by the actual installed AgentLoop in
|
|
273
|
+
integration mode. A Host-only projection verifies the SDK marker, frozen request,
|
|
274
|
+
canonical live Session messages and empty tool history, preserves appended Host facts,
|
|
275
|
+
and uses the Host Session persona. Capability assembly omits unsupported native tool
|
|
276
|
+
declarations for that request; Main keeps its full Host tool authority. Switching back
|
|
277
|
+
to an API route restores its tool declarations. The offline public-loop test proves two
|
|
278
|
+
native turns in one child, followed by an explicit API turn with three accounting
|
|
279
|
+
receipts total. The opt-in Host MCP path is tested with the actual SDK, native
|
|
280
|
+
subprocesses, Noise transport, paired Agent and SQLite Task service in both chat modes.
|
|
281
|
+
The remaining native tool families and drivers need further integration.
|