@oscharko-dev/keiko-server 0.2.7 → 0.2.9
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/dist/.tsbuildinfo +1 -1
- package/dist/chat-handlers.d.ts +18 -2
- package/dist/chat-handlers.d.ts.map +1 -1
- package/dist/chat-handlers.js +185 -3
- package/dist/command-runner-errors.d.ts +17 -0
- package/dist/command-runner-errors.d.ts.map +1 -0
- package/dist/command-runner-errors.js +37 -0
- package/dist/command-runner-evidence.d.ts +23 -0
- package/dist/command-runner-evidence.d.ts.map +1 -0
- package/dist/command-runner-evidence.js +69 -0
- package/dist/command-runner-routes.d.ts +7 -0
- package/dist/command-runner-routes.d.ts.map +1 -0
- package/dist/command-runner-routes.js +175 -0
- package/dist/command-runner.d.ts +29 -0
- package/dist/command-runner.d.ts.map +1 -0
- package/dist/command-runner.js +348 -0
- package/dist/conversation-prompt.d.ts +2 -2
- package/dist/conversation-prompt.d.ts.map +1 -1
- package/dist/conversation-prompt.js +17 -1
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +3 -0
- package/dist/deps.d.ts +28 -1
- package/dist/deps.d.ts.map +1 -1
- package/dist/deps.js +288 -13
- package/dist/discussion-prompt.d.ts +4 -0
- package/dist/discussion-prompt.d.ts.map +1 -0
- package/dist/discussion-prompt.js +19 -0
- package/dist/editor/agentActionAudit.d.ts +18 -0
- package/dist/editor/agentActionAudit.d.ts.map +1 -0
- package/dist/editor/agentActionAudit.js +80 -0
- package/dist/editor/agentRoutes.d.ts +1 -0
- package/dist/editor/agentRoutes.d.ts.map +1 -1
- package/dist/editor/agentRoutes.js +292 -55
- package/dist/editor/agentSessionRegistry.d.ts +35 -0
- package/dist/editor/agentSessionRegistry.d.ts.map +1 -0
- package/dist/editor/agentSessionRegistry.js +243 -0
- package/dist/editor/completionRoutes.d.ts.map +1 -1
- package/dist/editor/completionRoutes.js +5 -10
- package/dist/editor/languageRoutes.d.ts +12 -1
- package/dist/editor/languageRoutes.d.ts.map +1 -1
- package/dist/editor/languageRoutes.js +71 -8
- package/dist/editor/languageService.d.ts +3 -2
- package/dist/editor/languageService.d.ts.map +1 -1
- package/dist/editor/languageService.js +41 -3
- package/dist/editor/languageServiceHost.d.ts.map +1 -1
- package/dist/editor/languageServiceHost.js +2 -2
- package/dist/editor/lsp/hostLanguageOperation.d.ts +17 -0
- package/dist/editor/lsp/hostLanguageOperation.d.ts.map +1 -0
- package/dist/editor/lsp/hostLanguageOperation.js +436 -0
- package/dist/editor/lsp/hostLanguageProviders.d.ts +26 -0
- package/dist/editor/lsp/hostLanguageProviders.d.ts.map +1 -0
- package/dist/editor/lsp/hostLanguageProviders.js +161 -0
- package/dist/editor/lsp/lspFrameCodec.d.ts +13 -0
- package/dist/editor/lsp/lspFrameCodec.d.ts.map +1 -0
- package/dist/editor/lsp/lspFrameCodec.js +164 -0
- package/dist/editor/lsp/lspJsonRpcClient.d.ts +34 -0
- package/dist/editor/lsp/lspJsonRpcClient.d.ts.map +1 -0
- package/dist/editor/lsp/lspJsonRpcClient.js +173 -0
- package/dist/editor/lsp/lspLanguageProvider.d.ts +7 -0
- package/dist/editor/lsp/lspLanguageProvider.d.ts.map +1 -0
- package/dist/editor/lsp/lspLanguageProvider.js +29 -0
- package/dist/editor/lsp/lspLifecycleLedger.d.ts +5 -0
- package/dist/editor/lsp/lspLifecycleLedger.d.ts.map +1 -0
- package/dist/editor/lsp/lspLifecycleLedger.js +37 -0
- package/dist/editor/lsp/lspNodeAdapter.d.ts +31 -0
- package/dist/editor/lsp/lspNodeAdapter.d.ts.map +1 -0
- package/dist/editor/lsp/lspNodeAdapter.js +230 -0
- package/dist/editor/lsp/lspProcessManager.d.ts +24 -0
- package/dist/editor/lsp/lspProcessManager.d.ts.map +1 -0
- package/dist/editor/lsp/lspProcessManager.js +255 -0
- package/dist/editor/lsp/lspRestartThrottle.d.ts +6 -0
- package/dist/editor/lsp/lspRestartThrottle.d.ts.map +1 -0
- package/dist/editor/lsp/lspRestartThrottle.js +24 -0
- package/dist/editor/lsp/lspStatusRoute.d.ts +8 -0
- package/dist/editor/lsp/lspStatusRoute.d.ts.map +1 -0
- package/dist/editor/lsp/lspStatusRoute.js +22 -0
- package/dist/editor/lsp/lspTransport.d.ts +19 -0
- package/dist/editor/lsp/lspTransport.d.ts.map +1 -0
- package/dist/editor/lsp/lspTransport.js +55 -0
- package/dist/editor/lsp/testing/fakeLspProcess.d.ts +23 -0
- package/dist/editor/lsp/testing/fakeLspProcess.d.ts.map +1 -0
- package/dist/editor/lsp/testing/fakeLspProcess.js +132 -0
- package/dist/files.d.ts +63 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +799 -1
- package/dist/gateway-readiness.d.ts +6 -0
- package/dist/gateway-readiness.d.ts.map +1 -0
- package/dist/gateway-readiness.js +624 -0
- package/dist/gateway-setup.d.ts +2 -0
- package/dist/gateway-setup.d.ts.map +1 -1
- package/dist/gateway-setup.js +275 -11
- package/dist/gitDelivery/actionSheetProjection.d.ts +30 -0
- package/dist/gitDelivery/actionSheetProjection.d.ts.map +1 -0
- package/dist/gitDelivery/actionSheetProjection.js +206 -0
- package/dist/gitDelivery/actionSheetRoutes.d.ts +29 -0
- package/dist/gitDelivery/actionSheetRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/actionSheetRoutes.js +293 -0
- package/dist/gitDelivery/agentOperationsRoutes.d.ts +33 -0
- package/dist/gitDelivery/agentOperationsRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/agentOperationsRoutes.js +405 -0
- package/dist/gitDelivery/commitRoutes.d.ts +23 -0
- package/dist/gitDelivery/commitRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/commitRoutes.js +204 -0
- package/dist/gitDelivery/evidenceRoutes.d.ts +9 -0
- package/dist/gitDelivery/evidenceRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/evidenceRoutes.js +101 -0
- package/dist/gitDelivery/execution.d.ts +38 -0
- package/dist/gitDelivery/execution.d.ts.map +1 -0
- package/dist/gitDelivery/execution.js +117 -0
- package/dist/gitDelivery/localMutationRoutes.d.ts +30 -0
- package/dist/gitDelivery/localMutationRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/localMutationRoutes.js +165 -0
- package/dist/gitDelivery/mergeExecution.d.ts +63 -0
- package/dist/gitDelivery/mergeExecution.d.ts.map +1 -0
- package/dist/gitDelivery/mergeExecution.js +168 -0
- package/dist/gitDelivery/mergeRoutes.d.ts +12 -0
- package/dist/gitDelivery/mergeRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/mergeRoutes.js +218 -0
- package/dist/gitDelivery/mutationEvidenceLedger.d.ts +23 -0
- package/dist/gitDelivery/mutationEvidenceLedger.d.ts.map +1 -0
- package/dist/gitDelivery/mutationEvidenceLedger.js +87 -0
- package/dist/gitDelivery/prExecution.d.ts +54 -0
- package/dist/gitDelivery/prExecution.d.ts.map +1 -0
- package/dist/gitDelivery/prExecution.js +192 -0
- package/dist/gitDelivery/prRoutes.d.ts +12 -0
- package/dist/gitDelivery/prRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/prRoutes.js +256 -0
- package/dist/gitDelivery/pushExecution.d.ts +43 -0
- package/dist/gitDelivery/pushExecution.d.ts.map +1 -0
- package/dist/gitDelivery/pushExecution.js +124 -0
- package/dist/gitDelivery/pushRoutes.d.ts +12 -0
- package/dist/gitDelivery/pushRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/pushRoutes.js +200 -0
- package/dist/gitDelivery/requestGuards.d.ts +15 -0
- package/dist/gitDelivery/requestGuards.d.ts.map +1 -0
- package/dist/gitDelivery/requestGuards.js +97 -0
- package/dist/gitDelivery/syncEvidence.d.ts +37 -0
- package/dist/gitDelivery/syncEvidence.d.ts.map +1 -0
- package/dist/gitDelivery/syncEvidence.js +85 -0
- package/dist/gitDelivery/syncExecution.d.ts +30 -0
- package/dist/gitDelivery/syncExecution.d.ts.map +1 -0
- package/dist/gitDelivery/syncExecution.js +266 -0
- package/dist/gitDelivery/syncRoutes.d.ts +13 -0
- package/dist/gitDelivery/syncRoutes.d.ts.map +1 -0
- package/dist/gitDelivery/syncRoutes.js +200 -0
- package/dist/gitPorcelainStatus.d.ts +15 -0
- package/dist/gitPorcelainStatus.d.ts.map +1 -0
- package/dist/gitPorcelainStatus.js +104 -0
- package/dist/gitRepositoryReads.d.ts +10 -0
- package/dist/gitRepositoryReads.d.ts.map +1 -0
- package/dist/gitRepositoryReads.js +314 -0
- package/dist/gitRepositoryRoutes.d.ts +7 -0
- package/dist/gitRepositoryRoutes.d.ts.map +1 -0
- package/dist/gitRepositoryRoutes.js +221 -0
- package/dist/gitRoutes.d.ts +66 -0
- package/dist/gitRoutes.d.ts.map +1 -0
- package/dist/gitRoutes.js +543 -0
- package/dist/governed-workflow.d.ts +2 -0
- package/dist/governed-workflow.d.ts.map +1 -1
- package/dist/governed-workflow.js +4 -0
- package/dist/grounded-qa-hybrid.d.ts.map +1 -1
- package/dist/grounded-qa-hybrid.js +2 -0
- package/dist/grounded-qa-multi-source.d.ts.map +1 -1
- package/dist/grounded-qa-multi-source.js +1 -0
- package/dist/grounded-qa.d.ts +11 -0
- package/dist/grounded-qa.d.ts.map +1 -1
- package/dist/grounded-qa.js +14 -4
- package/dist/headers.d.ts +4 -1
- package/dist/headers.d.ts.map +1 -1
- package/dist/headers.js +11 -4
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/local-knowledge-grounded-qa.d.ts.map +1 -1
- package/dist/local-knowledge-grounded-qa.js +11 -2
- package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts +1 -1
- package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts.map +1 -1
- package/dist/qualityIntelligence/figmaSnapshotRoutes.js +1 -1
- package/dist/read-handlers.d.ts +5 -0
- package/dist/read-handlers.d.ts.map +1 -1
- package/dist/read-handlers.js +57 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +260 -12
- package/dist/run-engine.d.ts.map +1 -1
- package/dist/run-engine.js +3 -0
- package/dist/run-handlers.d.ts +0 -1
- package/dist/run-handlers.d.ts.map +1 -1
- package/dist/run-handlers.js +64 -211
- package/dist/run-request.d.ts +11 -0
- package/dist/run-request.d.ts.map +1 -1
- package/dist/run-request.js +158 -10
- package/dist/runtime/capabilityDetector.d.ts +38 -0
- package/dist/runtime/capabilityDetector.d.ts.map +1 -0
- package/dist/runtime/capabilityDetector.js +443 -0
- package/dist/runtime/capabilityRoutes.d.ts +9 -0
- package/dist/runtime/capabilityRoutes.d.ts.map +1 -0
- package/dist/runtime/capabilityRoutes.js +45 -0
- package/dist/runtime/containerEngineDetector.d.ts +17 -0
- package/dist/runtime/containerEngineDetector.d.ts.map +1 -0
- package/dist/runtime/containerEngineDetector.js +222 -0
- package/dist/runtime/containerRoutes.d.ts +8 -0
- package/dist/runtime/containerRoutes.d.ts.map +1 -0
- package/dist/runtime/containerRoutes.js +207 -0
- package/dist/runtime/containerRunner-errors.d.ts +18 -0
- package/dist/runtime/containerRunner-errors.d.ts.map +1 -0
- package/dist/runtime/containerRunner-errors.js +42 -0
- package/dist/runtime/containerRunner-evidence.d.ts +24 -0
- package/dist/runtime/containerRunner-evidence.d.ts.map +1 -0
- package/dist/runtime/containerRunner-evidence.js +74 -0
- package/dist/runtime/containerRunner.d.ts +37 -0
- package/dist/runtime/containerRunner.d.ts.map +1 -0
- package/dist/runtime/containerRunner.js +443 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +24 -4
- package/dist/store/db.d.ts.map +1 -1
- package/dist/store/db.js +2 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/messages.d.ts +2 -1
- package/dist/store/messages.d.ts.map +1 -1
- package/dist/store/messages.js +46 -4
- package/dist/store/schema.d.ts +1 -1
- package/dist/store/schema.d.ts.map +1 -1
- package/dist/store/schema.js +68 -1
- package/dist/store/types.d.ts +3 -2
- package/dist/store/types.d.ts.map +1 -1
- package/dist/task-workspace/active-store.d.ts +21 -0
- package/dist/task-workspace/active-store.d.ts.map +1 -0
- package/dist/task-workspace/active-store.js +55 -0
- package/dist/task-workspace/authorization.d.ts +7 -0
- package/dist/task-workspace/authorization.d.ts.map +1 -0
- package/dist/task-workspace/authorization.js +54 -0
- package/dist/task-workspace/binding.d.ts +3 -0
- package/dist/task-workspace/binding.d.ts.map +1 -0
- package/dist/task-workspace/binding.js +22 -0
- package/dist/task-workspace/cleanup.d.ts +4 -0
- package/dist/task-workspace/cleanup.d.ts.map +1 -0
- package/dist/task-workspace/cleanup.js +428 -0
- package/dist/task-workspace/errors.d.ts +14 -0
- package/dist/task-workspace/errors.d.ts.map +1 -0
- package/dist/task-workspace/errors.js +81 -0
- package/dist/task-workspace/evidence.d.ts +32 -0
- package/dist/task-workspace/evidence.d.ts.map +1 -0
- package/dist/task-workspace/evidence.js +52 -0
- package/dist/task-workspace/field-safety.d.ts +3 -0
- package/dist/task-workspace/field-safety.d.ts.map +1 -0
- package/dist/task-workspace/field-safety.js +42 -0
- package/dist/task-workspace/health.d.ts +4 -0
- package/dist/task-workspace/health.d.ts.map +1 -0
- package/dist/task-workspace/health.js +163 -0
- package/dist/task-workspace/lifecycle.d.ts +3 -0
- package/dist/task-workspace/lifecycle.d.ts.map +1 -0
- package/dist/task-workspace/lifecycle.js +248 -0
- package/dist/task-workspace/locks.d.ts +13 -0
- package/dist/task-workspace/locks.d.ts.map +1 -0
- package/dist/task-workspace/locks.js +44 -0
- package/dist/task-workspace/managed-root.d.ts +7 -0
- package/dist/task-workspace/managed-root.d.ts.map +1 -0
- package/dist/task-workspace/managed-root.js +98 -0
- package/dist/task-workspace/mutex.d.ts +8 -0
- package/dist/task-workspace/mutex.d.ts.map +1 -0
- package/dist/task-workspace/mutex.js +82 -0
- package/dist/task-workspace/naming.d.ts +15 -0
- package/dist/task-workspace/naming.d.ts.map +1 -0
- package/dist/task-workspace/naming.js +0 -0
- package/dist/task-workspace/provisioning.d.ts +3 -0
- package/dist/task-workspace/provisioning.d.ts.map +1 -0
- package/dist/task-workspace/provisioning.js +528 -0
- package/dist/task-workspace/reconciliation.d.ts +15 -0
- package/dist/task-workspace/reconciliation.d.ts.map +1 -0
- package/dist/task-workspace/reconciliation.js +274 -0
- package/dist/task-workspace/repair.d.ts +3 -0
- package/dist/task-workspace/repair.d.ts.map +1 -0
- package/dist/task-workspace/repair.js +286 -0
- package/dist/task-workspace/routes.d.ts +19 -0
- package/dist/task-workspace/routes.d.ts.map +1 -0
- package/dist/task-workspace/routes.js +481 -0
- package/dist/task-workspace/store.d.ts +12 -0
- package/dist/task-workspace/store.d.ts.map +1 -0
- package/dist/task-workspace/store.js +128 -0
- package/dist/task-workspace/types.d.ts +170 -0
- package/dist/task-workspace/types.d.ts.map +1 -0
- package/dist/task-workspace/types.js +5 -0
- package/dist/voice-action-governance.d.ts +23 -0
- package/dist/voice-action-governance.d.ts.map +1 -0
- package/dist/voice-action-governance.js +126 -0
- package/dist/voice-handlers.d.ts +6 -0
- package/dist/voice-handlers.d.ts.map +1 -0
- package/dist/voice-handlers.js +570 -0
- package/dist/voice-realtime-grounded-tool.d.ts +31 -0
- package/dist/voice-realtime-grounded-tool.d.ts.map +1 -0
- package/dist/voice-realtime-grounded-tool.js +322 -0
- package/dist/voice-realtime.d.ts +69 -0
- package/dist/voice-realtime.d.ts.map +1 -0
- package/dist/voice-realtime.js +787 -0
- package/dist/workspace-state-handlers.d.ts +5 -0
- package/dist/workspace-state-handlers.d.ts.map +1 -0
- package/dist/workspace-state-handlers.js +106 -0
- package/package.json +20 -19
- package/dist/grounded-handoff.d.ts +0 -4
- package/dist/grounded-handoff.d.ts.map +0 -1
- package/dist/grounded-handoff.js +0 -445
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
// Issue #1388 (Epic #1491, ADR-0070, D2/D3) — the governed container-run pilot manager. A request
|
|
2
|
+
// names a `taskId` from a server-frozen CLOSED catalog; the server resolves it to a vetted
|
|
3
|
+
// ContainerTask whose image is in a closed allowlist and constructs the EXACT, server-frozen
|
|
4
|
+
// `docker run` argv (no client-supplied image/argv/mount/flag). Execution reuses the single governed
|
|
5
|
+
// runCommand spawn boundary (ADR-0043 D2) exactly as the #1387 command runner does — no Docker SDK,
|
|
6
|
+
// no daemon-socket client, no second spawn path.
|
|
7
|
+
//
|
|
8
|
+
// Engine-unavailable is a GOVERNANCE failure, not an execution outcome: `execute` THROWS
|
|
9
|
+
// ContainerRunnerError("CONTAINER_ENGINE_UNAVAILABLE") BEFORE any run id is minted, so there is no
|
|
10
|
+
// run/event/evidence for a run that never started. Only real execution outcomes become a
|
|
11
|
+
// ContainerRunResult.
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { CommandCancelledError, CommandDeniedError, CommandTimeoutError, DEFAULT_SANDBOX_POLICY, runCommand, } from "@oscharko-dev/keiko-tools";
|
|
14
|
+
import { nodeSpawnFn } from "@oscharko-dev/keiko-tools/internal/exec";
|
|
15
|
+
import { nodeWorkspaceFs } from "@oscharko-dev/keiko-workspace/internal/fs";
|
|
16
|
+
import { CONTAINER_RUNTIME_SCHEMA_VERSION, CONTAINER_TASK_RULES, } from "@oscharko-dev/keiko-contracts";
|
|
17
|
+
import { ContainerRunnerError } from "./containerRunner-errors.js";
|
|
18
|
+
import { appendContainerRunEvidence, buildContainerRunEvidenceEntry, } from "./containerRunner-evidence.js";
|
|
19
|
+
import { detectContainerEngines } from "./containerEngineDetector.js";
|
|
20
|
+
// Tight cap — a container run is a high-trust surface, so a small number of concurrent runs.
|
|
21
|
+
const MAX_CONCURRENT_CONTAINER_RUNS = 2;
|
|
22
|
+
const MIN_TIMEOUT_MS = 1_000;
|
|
23
|
+
// ─── Defaults (conservative; justified inline) ─────────────────────────────────────
|
|
24
|
+
export const DEFAULT_CONTAINER_RESOURCE_LIMITS = {
|
|
25
|
+
pidsLimit: 256, // enough for a small toolchain; blocks fork-bombs
|
|
26
|
+
memoryBytes: 536_870_912, // 512 MiB — bounded, fits a diagnostic image
|
|
27
|
+
cpus: 1, // single core; deterministic, no host saturation
|
|
28
|
+
};
|
|
29
|
+
// The pilot image is pinned by TAG (not digest) because no published Keiko-vetted digest exists yet;
|
|
30
|
+
// the digest-pin path is documented in docs/container-runtime/security-notes.md and is a one-line
|
|
31
|
+
// change here once a digest is vetted. `--pull never` means the image MUST already be present
|
|
32
|
+
// locally — a missing image is a structured image-missing failure, never a silent network fetch.
|
|
33
|
+
const PILOT_IMAGE = "docker.io/library/alpine:3.20";
|
|
34
|
+
export const DEFAULT_CONTAINER_EXECUTION_POLICY = {
|
|
35
|
+
// Closed set: exactly the one pinned pilot image. An image ∉ this list → IMAGE_NOT_ALLOWED.
|
|
36
|
+
imageAllowlist: Object.freeze([PILOT_IMAGE]),
|
|
37
|
+
limits: DEFAULT_CONTAINER_RESOURCE_LIMITS,
|
|
38
|
+
mountMode: "read-only",
|
|
39
|
+
networkMode: "none",
|
|
40
|
+
workspaceMountPath: "/workspace",
|
|
41
|
+
pull: "never",
|
|
42
|
+
};
|
|
43
|
+
export const DEFAULT_CONTAINER_TASKS = Object.freeze([
|
|
44
|
+
{
|
|
45
|
+
id: "container-pilot:diagnostic",
|
|
46
|
+
kind: "diagnostic",
|
|
47
|
+
label: "Container engine pilot diagnostic",
|
|
48
|
+
image: PILOT_IMAGE,
|
|
49
|
+
// Trivial in-container command (NOT docker flags). Proves the detection+policy+spawn composition.
|
|
50
|
+
// Deliberately NOT `sh -c …`: `-c` is a CONTAINER_DENY_FLAGS member (a transitive-shell escalation
|
|
51
|
+
// flag), so a `-c` anywhere in the argv would self-deny at the runCommand boundary. A bare
|
|
52
|
+
// `echo …` argv keeps the frozen hardened argv passing isCommandAllowed (the §1.8 LOAD-BEARING
|
|
53
|
+
// no-self-deny invariant).
|
|
54
|
+
args: Object.freeze(["echo", "keiko-container-pilot ok"]),
|
|
55
|
+
engine: "docker",
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
// ─── Server-frozen argv (D3) — PURE, unit-tested in isolation ──────────────────────
|
|
59
|
+
// Returns the EXACT docker/podman argv. The image MUST already be asserted ∈ policy.imageAllowlist
|
|
60
|
+
// by the caller (execute does this and throws IMAGE_NOT_ALLOWED otherwise); this builder additionally
|
|
61
|
+
// re-asserts so a misuse can never emit a non-allowlisted image. No client input reaches this argv.
|
|
62
|
+
export function buildContainerRunArgv(task, policy, workspaceRoot) {
|
|
63
|
+
if (!policy.imageAllowlist.includes(task.image)) {
|
|
64
|
+
throw new ContainerRunnerError("IMAGE_NOT_ALLOWED", "Task image is not allowlisted.");
|
|
65
|
+
}
|
|
66
|
+
return [
|
|
67
|
+
"run",
|
|
68
|
+
"--rm", // no container persistence (NIST 800-190 4.4)
|
|
69
|
+
"--network",
|
|
70
|
+
policy.networkMode, // "none" — the container has no network (4.1.3)
|
|
71
|
+
"--read-only", // read-only root filesystem (3.1.2 / 4.5.2)
|
|
72
|
+
"--cap-drop",
|
|
73
|
+
"ALL", // drop all Linux capabilities (4.5.3)
|
|
74
|
+
"--security-opt",
|
|
75
|
+
"no-new-privileges", // forbid privilege escalation (4.5.3)
|
|
76
|
+
"--pids-limit",
|
|
77
|
+
String(policy.limits.pidsLimit),
|
|
78
|
+
"--memory",
|
|
79
|
+
String(policy.limits.memoryBytes),
|
|
80
|
+
"--cpus",
|
|
81
|
+
String(policy.limits.cpus),
|
|
82
|
+
"--pull",
|
|
83
|
+
policy.pull, // "never" — the image must already be present locally (3.1.1 / 4.2)
|
|
84
|
+
"-v",
|
|
85
|
+
// read-only workspace bind mount at a fixed in-container path; NO read-write/broad host mount,
|
|
86
|
+
// and NEVER the Docker socket.
|
|
87
|
+
`${workspaceRoot}:${policy.workspaceMountPath}:ro`,
|
|
88
|
+
task.image,
|
|
89
|
+
...task.args,
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
const IMAGE_MISSING_SIGNATURES = [
|
|
93
|
+
"no such image",
|
|
94
|
+
"unable to find image",
|
|
95
|
+
"image not known",
|
|
96
|
+
"manifest unknown",
|
|
97
|
+
];
|
|
98
|
+
const PULL_DENIED_SIGNATURES = [
|
|
99
|
+
"pull policy",
|
|
100
|
+
"pull never",
|
|
101
|
+
"--pull=never",
|
|
102
|
+
"image must be present",
|
|
103
|
+
];
|
|
104
|
+
const MOUNT_FAILURE_SIGNATURES = [
|
|
105
|
+
"error mounting",
|
|
106
|
+
"invalid mount",
|
|
107
|
+
"bind mount",
|
|
108
|
+
"no such file or directory", // bind source absent
|
|
109
|
+
"are you trying to mount",
|
|
110
|
+
];
|
|
111
|
+
function matchesSignature(haystack, signatures) {
|
|
112
|
+
const lower = haystack.toLowerCase();
|
|
113
|
+
return signatures.some((needle) => lower.includes(needle));
|
|
114
|
+
}
|
|
115
|
+
// Classifies a NON-ZERO exit into a container-specific failure reason by stderr signature, so the
|
|
116
|
+
// audit distinguishes a missing image / denied pull / mount failure from a plain non-zero exit.
|
|
117
|
+
function classifyNonZeroFailure(stderr) {
|
|
118
|
+
if (matchesSignature(stderr, IMAGE_MISSING_SIGNATURES))
|
|
119
|
+
return "image-missing";
|
|
120
|
+
if (matchesSignature(stderr, PULL_DENIED_SIGNATURES))
|
|
121
|
+
return "pull-denied";
|
|
122
|
+
if (matchesSignature(stderr, MOUNT_FAILURE_SIGNATURES))
|
|
123
|
+
return "mount-failure";
|
|
124
|
+
return "non-zero-exit";
|
|
125
|
+
}
|
|
126
|
+
function outcomeFromResult(result) {
|
|
127
|
+
// runCommand resolves only on a real process exit (timeout/cancel reject). A truncation that
|
|
128
|
+
// killed the child is terminal → output-capped; otherwise classify by exit code + stderr.
|
|
129
|
+
const failureReason = result.truncated
|
|
130
|
+
? "output-capped"
|
|
131
|
+
: result.exitCode === 0
|
|
132
|
+
? "none"
|
|
133
|
+
: classifyNonZeroFailure(result.stderr);
|
|
134
|
+
return {
|
|
135
|
+
exitCode: result.exitCode,
|
|
136
|
+
durationMs: result.durationMs,
|
|
137
|
+
truncated: result.truncated,
|
|
138
|
+
timedOut: false,
|
|
139
|
+
failureReason,
|
|
140
|
+
eventKind: failureReason === "none" ? "run-completed" : "run-failed",
|
|
141
|
+
stdout: result.stdout,
|
|
142
|
+
stderr: result.stderr,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function deniedFailureReason(error) {
|
|
146
|
+
if (error instanceof CommandDeniedError && !error.message.includes("not found on PATH")) {
|
|
147
|
+
return "denied";
|
|
148
|
+
}
|
|
149
|
+
return "spawn-error";
|
|
150
|
+
}
|
|
151
|
+
function outcomeFromError(error, cancelledByUser, durationMs) {
|
|
152
|
+
const base = { exitCode: null, durationMs, truncated: false, stdout: "", stderr: "" };
|
|
153
|
+
if (error instanceof CommandCancelledError || cancelledByUser) {
|
|
154
|
+
return { ...base, timedOut: false, failureReason: "cancelled", eventKind: "run-cancelled" };
|
|
155
|
+
}
|
|
156
|
+
if (error instanceof CommandTimeoutError) {
|
|
157
|
+
return { ...base, timedOut: true, failureReason: "timed-out", eventKind: "run-failed" };
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
...base,
|
|
161
|
+
timedOut: false,
|
|
162
|
+
failureReason: deniedFailureReason(error),
|
|
163
|
+
eventKind: "run-failed",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function clampTimeout(requested, ceiling) {
|
|
167
|
+
if (requested === undefined || !Number.isFinite(requested)) {
|
|
168
|
+
return ceiling;
|
|
169
|
+
}
|
|
170
|
+
const rounded = Math.round(requested);
|
|
171
|
+
if (rounded <= MIN_TIMEOUT_MS)
|
|
172
|
+
return MIN_TIMEOUT_MS;
|
|
173
|
+
if (rounded >= ceiling)
|
|
174
|
+
return ceiling;
|
|
175
|
+
return rounded;
|
|
176
|
+
}
|
|
177
|
+
function requestIdPayload(input) {
|
|
178
|
+
return input.requestId === undefined ? {} : { requestId: input.requestId };
|
|
179
|
+
}
|
|
180
|
+
function projectFor(store, projectId) {
|
|
181
|
+
for (const project of store.listProjects()) {
|
|
182
|
+
if (project.path === projectId) {
|
|
183
|
+
return project;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
function projectRootOrThrow(project) {
|
|
189
|
+
try {
|
|
190
|
+
return nodeWorkspaceFs.realPath(project.path);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
throw new ContainerRunnerError("PROJECT_NOT_FOUND", "Project root path could not be resolved.");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function buildWorkspaceInfo(projectRoot) {
|
|
197
|
+
return {
|
|
198
|
+
root: projectRoot,
|
|
199
|
+
name: undefined,
|
|
200
|
+
version: undefined,
|
|
201
|
+
testFramework: "unknown",
|
|
202
|
+
sourceDirs: [],
|
|
203
|
+
testDirs: [],
|
|
204
|
+
languages: [],
|
|
205
|
+
ignoreLines: [],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
class ContainerRunnerManagerImpl {
|
|
209
|
+
store;
|
|
210
|
+
evidenceStore;
|
|
211
|
+
policy;
|
|
212
|
+
executionPolicy;
|
|
213
|
+
catalog;
|
|
214
|
+
processEnv;
|
|
215
|
+
redactor;
|
|
216
|
+
runDeps;
|
|
217
|
+
detect;
|
|
218
|
+
now;
|
|
219
|
+
runs = new Map();
|
|
220
|
+
subscribers = new Set();
|
|
221
|
+
constructor(opts) {
|
|
222
|
+
this.store = opts.store;
|
|
223
|
+
this.evidenceStore = opts.evidenceStore;
|
|
224
|
+
this.policy = opts.policy ?? DEFAULT_SANDBOX_POLICY;
|
|
225
|
+
this.executionPolicy = opts.executionPolicy ?? DEFAULT_CONTAINER_EXECUTION_POLICY;
|
|
226
|
+
this.catalog = opts.catalog ?? DEFAULT_CONTAINER_TASKS;
|
|
227
|
+
this.processEnv = opts.processEnv ?? process.env;
|
|
228
|
+
this.redactor = opts.redactor ?? ((input) => input);
|
|
229
|
+
this.runDeps = opts.runDeps ?? {};
|
|
230
|
+
this.detect = opts.detect;
|
|
231
|
+
this.now = opts.now ?? Date.now;
|
|
232
|
+
}
|
|
233
|
+
inFlightCount = () => this.runs.size;
|
|
234
|
+
subscribe = (listener) => {
|
|
235
|
+
this.subscribers.add(listener);
|
|
236
|
+
return () => {
|
|
237
|
+
this.subscribers.delete(listener);
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
abort = (runId) => {
|
|
241
|
+
const entry = this.runs.get(runId);
|
|
242
|
+
if (entry === undefined)
|
|
243
|
+
return false;
|
|
244
|
+
entry.cancelledByUser = true;
|
|
245
|
+
entry.controller.abort();
|
|
246
|
+
return true;
|
|
247
|
+
};
|
|
248
|
+
capability = (projectId) => {
|
|
249
|
+
return this.resolveCapability(projectId);
|
|
250
|
+
};
|
|
251
|
+
listCatalog = async (projectId) => {
|
|
252
|
+
const capability = await this.resolveCapability(projectId);
|
|
253
|
+
// Graceful degradation: no engine → engineAvailable:false + tasks:[] (never an error).
|
|
254
|
+
const engineAvailable = capability.anyAvailable;
|
|
255
|
+
return {
|
|
256
|
+
schemaVersion: CONTAINER_RUNTIME_SCHEMA_VERSION,
|
|
257
|
+
projectId,
|
|
258
|
+
engineAvailable,
|
|
259
|
+
tasks: engineAvailable ? this.catalog : [],
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
execute = async (input) => {
|
|
263
|
+
const workspace = this.resolveWorkspace(input.projectId);
|
|
264
|
+
// Engine-unavailable is a GOVERNANCE failure: throw BEFORE minting a run id (route → 503).
|
|
265
|
+
const capability = await this.resolveCapability(input.projectId);
|
|
266
|
+
if (!capability.anyAvailable) {
|
|
267
|
+
throw new ContainerRunnerError("CONTAINER_ENGINE_UNAVAILABLE", "No container engine is available.");
|
|
268
|
+
}
|
|
269
|
+
const task = this.catalog.find((entry) => entry.id === input.taskId);
|
|
270
|
+
if (task === undefined) {
|
|
271
|
+
throw new ContainerRunnerError("TASK_NOT_FOUND", "Task is not in the container catalog.");
|
|
272
|
+
}
|
|
273
|
+
if (!this.executionPolicy.imageAllowlist.includes(task.image)) {
|
|
274
|
+
throw new ContainerRunnerError("IMAGE_NOT_ALLOWED", "Task image is not allowlisted.");
|
|
275
|
+
}
|
|
276
|
+
if (this.runs.size >= MAX_CONCURRENT_CONTAINER_RUNS) {
|
|
277
|
+
throw new ContainerRunnerError("RUN_LIMIT_EXCEEDED", "Too many in-flight container runs.");
|
|
278
|
+
}
|
|
279
|
+
return this.runExecution(task, workspace, input);
|
|
280
|
+
};
|
|
281
|
+
resolveCapability(projectId) {
|
|
282
|
+
if (this.detect !== undefined) {
|
|
283
|
+
return this.detect(projectId);
|
|
284
|
+
}
|
|
285
|
+
const probeDeps = {
|
|
286
|
+
runCommand,
|
|
287
|
+
workspace: this.tryWorkspace(projectId),
|
|
288
|
+
policy: this.policy,
|
|
289
|
+
processEnv: this.processEnv,
|
|
290
|
+
now: this.now,
|
|
291
|
+
};
|
|
292
|
+
return detectContainerEngines(probeDeps);
|
|
293
|
+
}
|
|
294
|
+
tryWorkspace(projectId) {
|
|
295
|
+
const project = projectFor(this.store, projectId);
|
|
296
|
+
if (project === undefined) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
return buildWorkspaceInfo(projectRootOrThrow(project));
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
resolveWorkspace(projectId) {
|
|
307
|
+
const project = projectFor(this.store, projectId);
|
|
308
|
+
if (project === undefined) {
|
|
309
|
+
throw new ContainerRunnerError("PROJECT_NOT_FOUND", "Project not found.");
|
|
310
|
+
}
|
|
311
|
+
return buildWorkspaceInfo(projectRootOrThrow(project));
|
|
312
|
+
}
|
|
313
|
+
buildRunDeps(workspace) {
|
|
314
|
+
return {
|
|
315
|
+
workspace,
|
|
316
|
+
// Host CLI policy: network:"inherit" so the docker/podman CLI reaches the daemon socket. The
|
|
317
|
+
// CONTAINER is isolated by --network none in the frozen argv (D4).
|
|
318
|
+
policy: { ...this.policy, network: "inherit" },
|
|
319
|
+
commandRules: CONTAINER_TASK_RULES,
|
|
320
|
+
spawn: this.runDeps.spawn ?? nodeSpawnFn,
|
|
321
|
+
processEnv: this.processEnv,
|
|
322
|
+
now: this.runDeps.now ?? this.now,
|
|
323
|
+
...(this.runDeps.resolveExecutable === undefined
|
|
324
|
+
? {}
|
|
325
|
+
: { resolveExecutable: this.runDeps.resolveExecutable }),
|
|
326
|
+
...(this.runDeps.fs === undefined ? {} : { fs: this.runDeps.fs }),
|
|
327
|
+
...(this.runDeps.home === undefined ? {} : { home: this.runDeps.home }),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
async runExecution(task, workspace, input) {
|
|
331
|
+
const runId = randomUUID();
|
|
332
|
+
const controller = new AbortController();
|
|
333
|
+
const entry = { controller, cancelledByUser: false };
|
|
334
|
+
this.runs.set(runId, entry);
|
|
335
|
+
const startedAt = this.now();
|
|
336
|
+
this.emit({
|
|
337
|
+
kind: "run-started",
|
|
338
|
+
runId,
|
|
339
|
+
payload: {
|
|
340
|
+
taskId: task.id,
|
|
341
|
+
kind: task.kind,
|
|
342
|
+
engine: task.engine,
|
|
343
|
+
startedAt,
|
|
344
|
+
...requestIdPayload(input),
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
try {
|
|
348
|
+
return await this.invoke(runId, task, workspace, input, entry, startedAt);
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
this.runs.delete(runId);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async invoke(runId, task, workspace, input, entry, startedAt) {
|
|
355
|
+
const deps = this.buildRunDeps(workspace);
|
|
356
|
+
const argv = buildContainerRunArgv(task, this.executionPolicy, workspace.root);
|
|
357
|
+
const timeoutMs = clampTimeout(input.timeoutMs, this.policy.defaultTimeoutMs);
|
|
358
|
+
try {
|
|
359
|
+
// Single governed spawn boundary; the injected fake spawn (if any) rides in `deps.spawn`.
|
|
360
|
+
const result = await runCommand({
|
|
361
|
+
command: task.engine,
|
|
362
|
+
args: argv,
|
|
363
|
+
cwd: undefined,
|
|
364
|
+
timeoutMs,
|
|
365
|
+
signal: entry.controller.signal,
|
|
366
|
+
}, deps);
|
|
367
|
+
return this.finalize(runId, task, argv.length, input, outcomeFromResult(result), startedAt);
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
const outcome = outcomeFromError(error, entry.cancelledByUser, this.now() - startedAt);
|
|
371
|
+
return this.finalize(runId, task, argv.length, input, outcome, startedAt);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
finalize(runId, task, argCount, input, outcome, startedAt) {
|
|
375
|
+
this.persist(runId, task, argCount, input, outcome, startedAt);
|
|
376
|
+
this.emit({
|
|
377
|
+
kind: outcome.eventKind,
|
|
378
|
+
runId,
|
|
379
|
+
payload: {
|
|
380
|
+
exitCode: outcome.exitCode,
|
|
381
|
+
durationMs: outcome.durationMs,
|
|
382
|
+
truncated: outcome.truncated,
|
|
383
|
+
timedOut: outcome.timedOut,
|
|
384
|
+
failureReason: outcome.failureReason,
|
|
385
|
+
...requestIdPayload(input),
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
return {
|
|
389
|
+
schemaVersion: CONTAINER_RUNTIME_SCHEMA_VERSION,
|
|
390
|
+
runId,
|
|
391
|
+
taskId: task.id,
|
|
392
|
+
kind: task.kind,
|
|
393
|
+
engine: task.engine,
|
|
394
|
+
exitCode: outcome.exitCode,
|
|
395
|
+
durationMs: outcome.durationMs,
|
|
396
|
+
truncated: outcome.truncated,
|
|
397
|
+
timedOut: outcome.timedOut,
|
|
398
|
+
failureReason: outcome.failureReason,
|
|
399
|
+
stdout: outcome.stdout,
|
|
400
|
+
stderr: outcome.stderr,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
persist(runId, task, argCount, input, outcome, startedAt) {
|
|
404
|
+
if (this.evidenceStore === undefined)
|
|
405
|
+
return;
|
|
406
|
+
try {
|
|
407
|
+
const evidence = buildContainerRunEvidenceEntry({
|
|
408
|
+
runId,
|
|
409
|
+
projectId: input.projectId,
|
|
410
|
+
taskId: task.id,
|
|
411
|
+
kind: task.kind,
|
|
412
|
+
engine: task.engine,
|
|
413
|
+
imageId: task.id, // closed-catalog id, NOT the raw image ref free-text
|
|
414
|
+
argCount,
|
|
415
|
+
exitCode: outcome.exitCode,
|
|
416
|
+
durationMs: outcome.durationMs,
|
|
417
|
+
timedOut: outcome.timedOut,
|
|
418
|
+
truncated: outcome.truncated,
|
|
419
|
+
failureReason: outcome.failureReason,
|
|
420
|
+
stdoutBytes: Buffer.byteLength(outcome.stdout, "utf8"),
|
|
421
|
+
stderrBytes: Buffer.byteLength(outcome.stderr, "utf8"),
|
|
422
|
+
startedAt,
|
|
423
|
+
});
|
|
424
|
+
appendContainerRunEvidence(this.evidenceStore, evidence, this.redactor);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// Evidence is best-effort process-evidence; a write hiccup must not corrupt a real run result.
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
emit(event) {
|
|
431
|
+
for (const listener of [...this.subscribers]) {
|
|
432
|
+
try {
|
|
433
|
+
listener(event);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
// A subscriber throwing must not stop fan-out (matches the command-runner pattern).
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
export function createContainerRunnerManager(opts) {
|
|
442
|
+
return new ContainerRunnerManagerImpl(opts);
|
|
443
|
+
}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAejG,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAejG,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAM9D,eAAO,MAAM,eAAe,OAAO,CAAC;AACpC,eAAO,MAAM,OAAO,cAAc,CAAC;AAEnC,MAAM,WAAW,YAAY;IAE3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAE5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAGrB,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IAEpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAGtB,QAAQ,CAAC,WAAW,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;CAClD;AAiKD,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CA2BzD"}
|
package/dist/server.js
CHANGED
|
@@ -8,6 +8,8 @@ import { isAllowedHost } from "./host-check.js";
|
|
|
8
8
|
import { resolveContainedPath, serveFile } from "./static.js";
|
|
9
9
|
import { errorBody, isApiPath, matchRoute, methodNotAllowedBody, notFoundBody, STREAMING, } from "./routes.js";
|
|
10
10
|
import { buildRedactor } from "./deps.js";
|
|
11
|
+
import { isVoiceDictationCapable, isVoiceRealtimeCapable } from "./read-handlers.js";
|
|
12
|
+
import { createVoiceControlPlane } from "./voice-realtime.js";
|
|
11
13
|
import { createRunRegistry } from "./runs.js";
|
|
12
14
|
import { createInMemoryUiStore } from "./store/index.js";
|
|
13
15
|
export const DEFAULT_UI_PORT = 1983;
|
|
@@ -117,7 +119,12 @@ async function resolveCsp(deps) {
|
|
|
117
119
|
async function handle(deps, handlerDeps, req, res) {
|
|
118
120
|
const url = new URL(req.url ?? "/", `http://${UI_HOST}`);
|
|
119
121
|
const apiPath = isApiPath(url.pathname);
|
|
120
|
-
|
|
122
|
+
// Issue #495/#497 — scope the Permissions-Policy microphone directive to deployments that advertise
|
|
123
|
+
// speech-to-text dictation OR full-realtime voice (whose WebRTC capture track also needs the mic);
|
|
124
|
+
// a no-voice deployment keeps the strict `microphone=()` default, never widened beyond `(self)`.
|
|
125
|
+
applySecurityHeaders(res, await resolveCsp(deps), apiPath, {
|
|
126
|
+
allowMicrophone: isVoiceDictationCapable(handlerDeps) || isVoiceRealtimeCapable(handlerDeps),
|
|
127
|
+
});
|
|
121
128
|
if (!isAllowedHost(req, deps.port)) {
|
|
122
129
|
rejectForbiddenHost(res);
|
|
123
130
|
return;
|
|
@@ -130,10 +137,16 @@ async function handle(deps, handlerDeps, req, res) {
|
|
|
130
137
|
await serveStatic(res, deps.staticRoot, url.pathname);
|
|
131
138
|
}
|
|
132
139
|
// Creates the BFF server. The caller binds it with `server.listen(deps.port, UI_HOST)` so it never
|
|
133
|
-
// listens on a non-loopback interface. The previous PTY WebSocket upgrade handler is removed —
|
|
134
|
-
//
|
|
140
|
+
// listens on a non-loopback interface. The previous PTY WebSocket upgrade handler is removed — the
|
|
141
|
+
// terminal tool is now bounded-exec over plain HTTP (ADR-0018 D1/D8). Issue #497 (ADR-0058 D3,
|
|
142
|
+
// ADR-0059) re-opens the upgrade for the single loopback voice control path `/api/voice/control`, and
|
|
143
|
+
// ONLY when the deployment is full-realtime voice capable; every other upgrade keeps the hard reject.
|
|
135
144
|
export function createUiServer(deps) {
|
|
136
145
|
const handlerDeps = deps.handlerDeps ?? fallbackDeps();
|
|
146
|
+
const voiceControl = createVoiceControlPlane({
|
|
147
|
+
port: deps.port,
|
|
148
|
+
handlerDeps: () => handlerDeps,
|
|
149
|
+
});
|
|
137
150
|
const server = createServer((req, res) => {
|
|
138
151
|
void handle(deps, handlerDeps, req, res).catch(() => {
|
|
139
152
|
if (!res.headersSent) {
|
|
@@ -144,9 +157,16 @@ export function createUiServer(deps) {
|
|
|
144
157
|
}
|
|
145
158
|
});
|
|
146
159
|
});
|
|
147
|
-
server.on("upgrade", (
|
|
160
|
+
server.on("upgrade", (req, socket, head) => {
|
|
161
|
+
if (voiceControl.handleUpgrade(req, socket, head)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Default: every non-voice-control or ungated upgrade is hard-rejected, as before.
|
|
148
165
|
socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
149
166
|
socket.destroy();
|
|
150
167
|
});
|
|
168
|
+
server.on("close", () => {
|
|
169
|
+
voiceControl.closeAll();
|
|
170
|
+
});
|
|
151
171
|
return server;
|
|
152
172
|
}
|
package/dist/store/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/store/db.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,OAAO,KAAK,EAMV,OAAO,EACP,qBAAqB,EAKtB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/store/db.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,OAAO,KAAK,EAMV,OAAO,EACP,qBAAqB,EAKtB,MAAM,YAAY,CAAC;AAkCpB,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAM9E;AAiKD,eAAO,MAAM,qBAAqB,OAAQ,CAAC;AAa3C,wBAAgB,qBAAqB,CAAC,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAI3E;AAgCD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAkB/D;AAED,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAEhG;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAEvF"}
|
package/dist/store/db.js
CHANGED
|
@@ -8,7 +8,7 @@ import { randomUUID } from "node:crypto";
|
|
|
8
8
|
import { runMigrations } from "./schema.js";
|
|
9
9
|
import { deleteProject as sqlDeleteProject, getProject as sqlGetProject, listProjects as sqlListProjects, updateProject as sqlUpdateProject, upsertProject as sqlUpsertProject, } from "./projects.js";
|
|
10
10
|
import { deleteChat as sqlDeleteChat, findChatById as sqlFindChatById, insertChat as sqlInsertChat, listChats as sqlListChats, listChatsLimited as sqlListChatsLimited, touchChat as sqlTouchChat, updateChat as sqlUpdateChat, } from "./chats.js";
|
|
11
|
-
import { findMessageById as sqlFindMessageById, insertMessage as sqlInsertMessage, listMessages as sqlListMessages, listMessagesLimited as sqlListMessagesLimited, updateMessage as sqlUpdateMessage, } from "./messages.js";
|
|
11
|
+
import { findMessageById as sqlFindMessageById, attachGroundedAnswer as sqlAttachGroundedAnswer, insertMessage as sqlInsertMessage, listMessages as sqlListMessages, listMessagesLimited as sqlListMessagesLimited, updateMessage as sqlUpdateMessage, } from "./messages.js";
|
|
12
12
|
import { validateProjectPath } from "./validation.js";
|
|
13
13
|
import { basename } from "node:path";
|
|
14
14
|
import { invalidRequest } from "./errors.js";
|
|
@@ -111,6 +111,7 @@ function buildStore(db, options) {
|
|
|
111
111
|
},
|
|
112
112
|
createMessages: (messages) => createMessageBatch(db, options, messages),
|
|
113
113
|
updateMessage: (id, patch) => sqlUpdateMessage(db, id, patch, options.redactString),
|
|
114
|
+
attachGroundedAnswer: (id, answer) => sqlAttachGroundedAnswer(db, id, answer, options.redactString),
|
|
114
115
|
close: () => {
|
|
115
116
|
db.close();
|
|
116
117
|
},
|
package/dist/store/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { Chat, ChatConnectedScope, ChatLocalKnowledgeScope, ChatMessage, ChatRole, CreateChatOptions, NewChatMessage, Project, UiStore, UiStoreFactoryOptions, UpdateChatOptions, UpdateChatMessagePatch, UpdateChatPatch, UpdateProjectPatch, WorkflowStatus, } from "./types.js";
|
|
1
|
+
export type { Chat, ChatConnectedScope, ChatLocalKnowledgeScope, ChatMessage, ChatRole, CreateChatOptions, GroundedAnswer, NewChatMessage, Project, UiStore, UiStoreFactoryOptions, UpdateChatOptions, UpdateChatMessagePatch, UpdateChatPatch, UpdateProjectPatch, WorkflowStatus, } from "./types.js";
|
|
2
2
|
export { UiStoreError, type UiStoreErrorCode, invalidPath, invalidRequest, notFound, pathNotDirectory, pathNotFound, projectExists, } from "./errors.js";
|
|
3
3
|
export { classifyPathShape, validateProjectPath, type PathShape, type ValidateProjectPathOptions, } from "./validation.js";
|
|
4
4
|
export { assertUiDbOutsideProject, resolveUiDbPath, UI_DB_FILENAME, UI_DB_DIRNAME, } from "./paths.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AAEA,YAAY,EACV,IAAI,EACJ,kBAAkB,EAClB,uBAAuB,EACvB,WAAW,EACX,QAAQ,EACR,iBAAiB,EACjB,cAAc,EACd,OAAO,EACP,OAAO,EACP,qBAAqB,EACrB,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,KAAK,gBAAgB,EACrB,WAAW,EACX,cAAc,EACd,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,SAAS,EACd,KAAK,0BAA0B,GAChC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AAEA,YAAY,EACV,IAAI,EACJ,kBAAkB,EAClB,uBAAuB,EACvB,WAAW,EACX,QAAQ,EACR,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,OAAO,EACP,OAAO,EACP,qBAAqB,EACrB,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,KAAK,gBAAgB,EACrB,WAAW,EACX,cAAc,EACd,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,SAAS,EACd,KAAK,0BAA0B,GAChC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,SAAS,CAAC"}
|
package/dist/store/messages.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
-
import type { ChatMessage, NewChatMessage, UpdateChatMessagePatch } from "./types.js";
|
|
2
|
+
import type { ChatMessage, GroundedAnswer, NewChatMessage, UpdateChatMessagePatch } from "./types.js";
|
|
3
3
|
export declare function listMessages(db: DatabaseSync, chatId: string): readonly ChatMessage[];
|
|
4
4
|
export declare function listMessagesLimited(db: DatabaseSync, chatId: string, limit: number): readonly ChatMessage[];
|
|
5
5
|
export declare function findMessageById(db: DatabaseSync, id: string): ChatMessage | undefined;
|
|
6
6
|
export declare function insertMessage(db: DatabaseSync, id: string, msg: NewChatMessage, redactString: (s: string) => string): ChatMessage;
|
|
7
|
+
export declare function attachGroundedAnswer(db: DatabaseSync, id: string, answer: GroundedAnswer, redactString: (s: string) => string): ChatMessage;
|
|
7
8
|
export declare function updateMessage(db: DatabaseSync, id: string, patch: UpdateChatMessagePatch, redactString: (s: string) => string): ChatMessage;
|
|
8
9
|
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/store/messages.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EACV,WAAW,EAEX,cAAc,EACd,sBAAsB,EAEvB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/store/messages.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EACV,WAAW,EAEX,cAAc,EACd,cAAc,EACd,sBAAsB,EAEvB,MAAM,YAAY,CAAC;AA4IpB,wBAAgB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,WAAW,EAAE,CAErF;AAED,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,YAAY,EAChB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,SAAS,WAAW,EAAE,CAOxB;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAGrF;AAED,wBAAgB,aAAa,CAC3B,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,cAAc,EACnB,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAClC,WAAW,CAsBb;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,cAAc,EACtB,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAClC,WAAW,CAcb;AAOD,wBAAgB,aAAa,CAC3B,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,sBAAsB,EAC7B,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAClC,WAAW,CA6Bb"}
|
package/dist/store/messages.js
CHANGED
|
@@ -17,7 +17,18 @@ const STATUSES = new Set([
|
|
|
17
17
|
"failed",
|
|
18
18
|
"cancelled",
|
|
19
19
|
]);
|
|
20
|
+
function parseGroundedAnswer(raw) {
|
|
21
|
+
if (raw === null)
|
|
22
|
+
return undefined;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
20
30
|
function rowToMessage(row) {
|
|
31
|
+
const groundedAnswer = parseGroundedAnswer(row.grounded_answer_json);
|
|
21
32
|
return {
|
|
22
33
|
id: row.id,
|
|
23
34
|
chatId: row.chat_id,
|
|
@@ -29,17 +40,18 @@ function rowToMessage(row) {
|
|
|
29
40
|
workflowStatus: (row.workflow_status ?? undefined),
|
|
30
41
|
shortResult: row.short_result ?? undefined,
|
|
31
42
|
taskType: row.task_type ?? undefined,
|
|
43
|
+
...(groundedAnswer === undefined ? {} : { groundedAnswer }),
|
|
32
44
|
};
|
|
33
45
|
}
|
|
34
|
-
const COLUMNS = "id, chat_id, role, content, timestamp, run_id, workflow_id, workflow_status, short_result, task_type";
|
|
46
|
+
const COLUMNS = "id, chat_id, role, content, timestamp, run_id, workflow_id, workflow_status, short_result, task_type, grounded_answer_json";
|
|
35
47
|
const SQL_LIST = `SELECT ${COLUMNS} FROM chat_messages WHERE chat_id = ? ORDER BY timestamp ASC, rowid ASC`;
|
|
36
48
|
const SQL_LIST_LIMITED = `${SQL_LIST} LIMIT ?`;
|
|
37
49
|
const SQL_FIND_BY_ID = `SELECT ${COLUMNS} FROM chat_messages WHERE id = ? LIMIT 1`;
|
|
38
50
|
const SQL_CHAT_EXISTS = "SELECT 1 FROM chats WHERE id = ?";
|
|
39
51
|
const SQL_INSERT = `
|
|
40
52
|
INSERT INTO chat_messages
|
|
41
|
-
(id, chat_id, role, content, timestamp, run_id, workflow_id, workflow_status, short_result, task_type)
|
|
42
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
53
|
+
(id, chat_id, role, content, timestamp, run_id, workflow_id, workflow_status, short_result, task_type, grounded_answer_json)
|
|
54
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
43
55
|
RETURNING ${COLUMNS}
|
|
44
56
|
`;
|
|
45
57
|
function validateTaskType(value) {
|
|
@@ -66,6 +78,9 @@ function validateRunSummaryScope(msg) {
|
|
|
66
78
|
if (hasRunSummaryFields(msg) && (msg.role !== "system" || msg.runId === undefined)) {
|
|
67
79
|
throw invalidRequest("Run summary fields require a system message with runId.");
|
|
68
80
|
}
|
|
81
|
+
if (msg.groundedAnswer !== undefined && msg.role !== "assistant") {
|
|
82
|
+
throw invalidRequest("Grounded answer metadata requires an assistant message.");
|
|
83
|
+
}
|
|
69
84
|
}
|
|
70
85
|
function validateMessage(msg) {
|
|
71
86
|
if (!ROLES.has(msg.role))
|
|
@@ -86,6 +101,18 @@ function processShortResult(raw, redactString) {
|
|
|
86
101
|
const redacted = redactString(raw);
|
|
87
102
|
return redacted.length > MAX_SHORT_RESULT ? redacted.slice(0, MAX_SHORT_RESULT) : redacted;
|
|
88
103
|
}
|
|
104
|
+
function processGroundedAnswer(raw, redactString) {
|
|
105
|
+
if (raw === undefined)
|
|
106
|
+
return null;
|
|
107
|
+
const redacted = redactString(JSON.stringify(raw));
|
|
108
|
+
try {
|
|
109
|
+
JSON.parse(redacted);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
throw invalidRequest("Grounded answer metadata is invalid.");
|
|
113
|
+
}
|
|
114
|
+
return redacted;
|
|
115
|
+
}
|
|
89
116
|
export function listMessages(db, chatId) {
|
|
90
117
|
return db.prepare(SQL_LIST).all(chatId).map(rowToMessage);
|
|
91
118
|
}
|
|
@@ -105,9 +132,24 @@ export function insertMessage(db, id, msg, redactString) {
|
|
|
105
132
|
if (!chatExists)
|
|
106
133
|
throw notFound("Chat");
|
|
107
134
|
const shortResult = processShortResult(msg.shortResult, redactString);
|
|
135
|
+
const groundedAnswer = processGroundedAnswer(msg.groundedAnswer, redactString);
|
|
108
136
|
const row = db
|
|
109
137
|
.prepare(SQL_INSERT)
|
|
110
|
-
.get(id, msg.chatId, msg.role, msg.content, msg.timestamp, msg.runId ?? null, msg.workflowId ?? null, msg.workflowStatus ?? null, shortResult, msg.taskType ?? null);
|
|
138
|
+
.get(id, msg.chatId, msg.role, msg.content, msg.timestamp, msg.runId ?? null, msg.workflowId ?? null, msg.workflowStatus ?? null, shortResult, msg.taskType ?? null, groundedAnswer);
|
|
139
|
+
return rowToMessage(row);
|
|
140
|
+
}
|
|
141
|
+
export function attachGroundedAnswer(db, id, answer, redactString) {
|
|
142
|
+
const groundedAnswer = processGroundedAnswer(answer, redactString);
|
|
143
|
+
const row = db
|
|
144
|
+
.prepare(`
|
|
145
|
+
UPDATE chat_messages
|
|
146
|
+
SET grounded_answer_json = ?
|
|
147
|
+
WHERE id = ? AND role = 'assistant'
|
|
148
|
+
RETURNING ${COLUMNS}
|
|
149
|
+
`)
|
|
150
|
+
.get(groundedAnswer, id);
|
|
151
|
+
if (row === undefined)
|
|
152
|
+
throw notFound("Message");
|
|
111
153
|
return rowToMessage(row);
|
|
112
154
|
}
|
|
113
155
|
// Issue #66 — Partial PATCH on a system run-summary message. Builds a dynamic SET clause from the
|
package/dist/store/schema.d.ts
CHANGED