@oscharko-dev/keiko-server 0.2.8 → 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 +27 -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 +45 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +631 -7
- package/dist/gateway-readiness.js +3 -3
- 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.d.ts +11 -0
- package/dist/grounded-qa.d.ts.map +1 -1
- package/dist/grounded-qa.js +13 -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/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 +259 -6
- package/dist/run-engine.d.ts.map +1 -1
- package/dist/run-engine.js +3 -0
- package/dist/run-handlers.d.ts.map +1 -1
- package/dist/run-handlers.js +74 -4
- 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/schema.d.ts +1 -1
- package/dist/store/schema.d.ts.map +1 -1
- package/dist/store/schema.js +62 -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
|
@@ -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/schema.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/store/schema.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,cAAc,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/store/schema.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,cAAc,IAAI,CAAC;AA2RhC,wBAAgB,aAAa,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAepD"}
|
package/dist/store/schema.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ADR-0013 D5 — Schema v1 + migration runner via PRAGMA user_version. Forward-only, idempotent,
|
|
2
2
|
// transactional. Each migration is a `.sql` string of one or more CREATE/ALTER statements; the
|
|
3
3
|
// runner applies migrations whose 1-based index > current user_version.
|
|
4
|
-
export const SCHEMA_VERSION =
|
|
4
|
+
export const SCHEMA_VERSION = 8;
|
|
5
5
|
const V1_SQL = `
|
|
6
6
|
CREATE TABLE projects (
|
|
7
7
|
path TEXT NOT NULL PRIMARY KEY,
|
|
@@ -189,6 +189,65 @@ CREATE INDEX idx_relationship_audit_relationship
|
|
|
189
189
|
const V6_SQL = `
|
|
190
190
|
ALTER TABLE chat_messages ADD COLUMN grounded_answer_json TEXT;
|
|
191
191
|
`;
|
|
192
|
+
// V7 (issue #445, epic #443) — durable managed task-workspace instances. STRICT mode. One row per
|
|
193
|
+
// provisioned/activated task workspace; the partial unique index on (repository_id, task_id) enforces
|
|
194
|
+
// the idempotency invariant at the DB layer as a second barrier alongside the deterministic id
|
|
195
|
+
// derivation (AC3). All columns are content-free per the #444 contract (ids/hashes, enums, ISO
|
|
196
|
+
// timestamps, branch/path names); `lock_json`, `drift_markers_json`, and `recovery_hints_json` carry
|
|
197
|
+
// the nested WorkspaceLock / drift-marker / recovery-hint shapes the contract validator gates.
|
|
198
|
+
const V7_SQL = `
|
|
199
|
+
CREATE TABLE task_workspace_instances (
|
|
200
|
+
workspace_id TEXT NOT NULL PRIMARY KEY,
|
|
201
|
+
schema_version TEXT NOT NULL,
|
|
202
|
+
task_id TEXT NOT NULL,
|
|
203
|
+
repository_id TEXT NOT NULL,
|
|
204
|
+
repository_root TEXT NOT NULL,
|
|
205
|
+
base_branch TEXT NOT NULL,
|
|
206
|
+
task_branch TEXT NOT NULL,
|
|
207
|
+
managed_worktree_path TEXT NOT NULL,
|
|
208
|
+
gitdir_identity TEXT NOT NULL,
|
|
209
|
+
lifecycle_state TEXT NOT NULL,
|
|
210
|
+
health TEXT NOT NULL,
|
|
211
|
+
lock_json TEXT,
|
|
212
|
+
created_at TEXT NOT NULL,
|
|
213
|
+
updated_at TEXT NOT NULL,
|
|
214
|
+
last_verified_at TEXT,
|
|
215
|
+
last_verified_head TEXT,
|
|
216
|
+
drift_markers_json TEXT NOT NULL,
|
|
217
|
+
recovery_hints_json TEXT NOT NULL,
|
|
218
|
+
audit_correlation_id TEXT NOT NULL,
|
|
219
|
+
CHECK (
|
|
220
|
+
schema_version IN ('1')
|
|
221
|
+
AND lifecycle_state IN (
|
|
222
|
+
'provisioning','active','paused','handoff-ready','archived','merged',
|
|
223
|
+
'abandoned','recovery-required','failed','cleanup-pending'
|
|
224
|
+
)
|
|
225
|
+
AND health IN ('healthy','degraded','drifted','locked-out','missing','unknown')
|
|
226
|
+
)
|
|
227
|
+
) STRICT;
|
|
228
|
+
|
|
229
|
+
CREATE UNIQUE INDEX uniq_task_workspace_repo_task
|
|
230
|
+
ON task_workspace_instances(repository_id, task_id);
|
|
231
|
+
CREATE INDEX idx_task_workspace_repository
|
|
232
|
+
ON task_workspace_instances(repository_id, updated_at);
|
|
233
|
+
`;
|
|
234
|
+
// V8 (issue #446, epic #443) — singleton active task-workspace pointer. Studio binds exactly ONE
|
|
235
|
+
// active task workspace at a time; this table holds at most one row (id pinned to the constant
|
|
236
|
+
// 'active', enforced by CHECK). The WorkspaceBinding surfaces consume is DERIVED from the referenced
|
|
237
|
+
// instance (binding.ts), never stored, so the active root has no second copy to drift. Content-free:
|
|
238
|
+
// an opaque workspace id + an opaque actor id + ISO timestamps only. ON DELETE CASCADE clears the
|
|
239
|
+
// pointer when the referenced instance is removed (the lifecycle service also clears it defensively).
|
|
240
|
+
const V8_SQL = `
|
|
241
|
+
CREATE TABLE task_workspace_active_pointer (
|
|
242
|
+
id TEXT NOT NULL PRIMARY KEY DEFAULT 'active',
|
|
243
|
+
workspace_id TEXT NOT NULL,
|
|
244
|
+
set_by TEXT NOT NULL,
|
|
245
|
+
set_at TEXT NOT NULL,
|
|
246
|
+
updated_at TEXT NOT NULL,
|
|
247
|
+
CHECK (id = 'active'),
|
|
248
|
+
FOREIGN KEY (workspace_id) REFERENCES task_workspace_instances(workspace_id) ON DELETE CASCADE
|
|
249
|
+
) STRICT;
|
|
250
|
+
`;
|
|
192
251
|
const MIGRATIONS = [
|
|
193
252
|
{ version: 1, sql: V1_SQL },
|
|
194
253
|
{ version: 2, sql: V2_SQL },
|
|
@@ -196,6 +255,8 @@ const MIGRATIONS = [
|
|
|
196
255
|
{ version: 4, sql: V4_SQL },
|
|
197
256
|
{ version: 5, sql: V5_SQL },
|
|
198
257
|
{ version: 6, sql: V6_SQL },
|
|
258
|
+
{ version: 7, sql: V7_SQL },
|
|
259
|
+
{ version: 8, sql: V8_SQL },
|
|
199
260
|
];
|
|
200
261
|
function currentUserVersion(db) {
|
|
201
262
|
const row = db.prepare("PRAGMA user_version").get();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
export interface ActiveWorkspacePointer {
|
|
3
|
+
readonly workspaceId: string;
|
|
4
|
+
readonly setBy: string;
|
|
5
|
+
readonly setAt: string;
|
|
6
|
+
readonly updatedAt: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ActiveWorkspacePointerStore {
|
|
9
|
+
/** The current active pointer, or undefined in unbound mode (no row). */
|
|
10
|
+
readonly get: () => ActiveWorkspacePointer | undefined;
|
|
11
|
+
/** Upsert the singleton row (id pinned to 'active'); returns the persisted pointer. */
|
|
12
|
+
readonly set: (input: {
|
|
13
|
+
readonly workspaceId: string;
|
|
14
|
+
readonly setBy: string;
|
|
15
|
+
readonly atIso: string;
|
|
16
|
+
}) => ActiveWorkspacePointer;
|
|
17
|
+
/** Delete the singleton row → unbound mode. Idempotent. */
|
|
18
|
+
readonly clear: () => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function buildActiveWorkspacePointerStoreOverDatabase(db: DatabaseSync): ActiveWorkspacePointerStore;
|
|
21
|
+
//# sourceMappingURL=active-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"active-store.d.ts","sourceRoot":"","sources":["../../src/task-workspace/active-store.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,yEAAyE;IACzE,QAAQ,CAAC,GAAG,EAAE,MAAM,sBAAsB,GAAG,SAAS,CAAC;IACvD,uFAAuF;IACvF,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE;QACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;KACxB,KAAK,sBAAsB,CAAC;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;CAC5B;AAsCD,wBAAgB,4CAA4C,CAC1D,EAAE,EAAE,YAAY,GACf,2BAA2B,CAgB7B"}
|