@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,481 @@
|
|
|
1
|
+
// BFF routes for managed task-workspace provisioning + activation (Issue #445, Epic #443).
|
|
2
|
+
//
|
|
3
|
+
// POST /api/task-workspaces provision (create/resume) → { instance, binding }
|
|
4
|
+
// GET /api/task-workspaces/:workspaceId read one persisted instance
|
|
5
|
+
// POST /api/task-workspaces/:workspaceId/activate activate/resume → { instance, binding }
|
|
6
|
+
//
|
|
7
|
+
// These are the controlled server-side mutating actions the Issue requires (no broad shell, no generic
|
|
8
|
+
// Git runner). CSRF is enforced by the server's global state-changing-request gate for POST, exactly
|
|
9
|
+
// like the command-runner routes; the GET is read-only. Domain failures map to the structured
|
|
10
|
+
// TaskWorkspaceError taxonomy; the response body is redacted before it reaches the browser.
|
|
11
|
+
import { isTaskWorkspaceLifecycleState, isWorkspaceRecoveryStrategy, } from "@oscharko-dev/keiko-contracts";
|
|
12
|
+
import { errorBody } from "../routes.js";
|
|
13
|
+
import { FilesError, resolveRoot } from "../files.js";
|
|
14
|
+
import { TaskWorkspaceError } from "./errors.js";
|
|
15
|
+
import { assertSafeFieldValue } from "./field-safety.js";
|
|
16
|
+
const MAX_BODY_BYTES = 16_000;
|
|
17
|
+
const MAX_FIELD_LENGTH = 512;
|
|
18
|
+
class WorkspaceBodyTooLargeError extends Error {
|
|
19
|
+
constructor() {
|
|
20
|
+
super("task workspace request body too large");
|
|
21
|
+
this.name = "WorkspaceBodyTooLargeError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function unavailable() {
|
|
25
|
+
return {
|
|
26
|
+
status: 503,
|
|
27
|
+
body: errorBody("WORKSPACE_PROVISIONING_UNAVAILABLE", "Task-workspace provisioning is not configured for this BFF."),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function requireService(deps) {
|
|
31
|
+
return deps.workspaceProvisioning ?? unavailable();
|
|
32
|
+
}
|
|
33
|
+
function requireLifecycle(deps) {
|
|
34
|
+
return deps.workspaceLifecycle ?? unavailable();
|
|
35
|
+
}
|
|
36
|
+
function requireReconciliation(deps) {
|
|
37
|
+
return deps.workspaceReconciliation ?? unavailable();
|
|
38
|
+
}
|
|
39
|
+
function requireRepair(deps) {
|
|
40
|
+
return deps.workspaceRepair ?? unavailable();
|
|
41
|
+
}
|
|
42
|
+
function requireHealth(deps) {
|
|
43
|
+
return deps.workspaceHealth ?? unavailable();
|
|
44
|
+
}
|
|
45
|
+
function requireCleanup(deps) {
|
|
46
|
+
return deps.workspaceCleanup ?? unavailable();
|
|
47
|
+
}
|
|
48
|
+
function isRouteResult(value) {
|
|
49
|
+
return typeof value.status === "number";
|
|
50
|
+
}
|
|
51
|
+
function readBody(req) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const chunks = [];
|
|
54
|
+
let total = 0;
|
|
55
|
+
let capped = false;
|
|
56
|
+
req.on("data", (chunk) => {
|
|
57
|
+
total += chunk.length;
|
|
58
|
+
if (total > MAX_BODY_BYTES) {
|
|
59
|
+
if (!capped) {
|
|
60
|
+
capped = true;
|
|
61
|
+
chunks.length = 0;
|
|
62
|
+
reject(new WorkspaceBodyTooLargeError());
|
|
63
|
+
req.resume();
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
chunks.push(chunk);
|
|
68
|
+
});
|
|
69
|
+
req.on("end", () => {
|
|
70
|
+
if (!capped)
|
|
71
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
72
|
+
});
|
|
73
|
+
req.on("error", reject);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async function readJsonObject(req) {
|
|
77
|
+
const raw = await readBody(req);
|
|
78
|
+
if (raw.length === 0)
|
|
79
|
+
return {};
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = JSON.parse(raw);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new TaskWorkspaceError("INVALID_REQUEST", "request body is not valid JSON");
|
|
86
|
+
}
|
|
87
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
88
|
+
throw new TaskWorkspaceError("INVALID_REQUEST", "request body must be a JSON object");
|
|
89
|
+
}
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
function boundedString(value) {
|
|
93
|
+
return typeof value === "string" && value.length > 0 && value.length <= MAX_FIELD_LENGTH
|
|
94
|
+
? value
|
|
95
|
+
: undefined;
|
|
96
|
+
}
|
|
97
|
+
// Extract a REQUIRED free-form identity field (requestedBy / taskId): length-bounded AND free of
|
|
98
|
+
// control / zero-width / bidirectional-override code points (#449 PR #1587 follow-up). These values
|
|
99
|
+
// flow into the advisory lock owner, the active-pointer setBy, and operator-visible evidence, so the
|
|
100
|
+
// route boundary rejects rather than strips them (see field-safety.ts). Missing/empty/oversized fields
|
|
101
|
+
// keep the existing "missing or invalid field" message; a present-but-unsafe value throws a distinct
|
|
102
|
+
// "forbidden characters" reason.
|
|
103
|
+
function requireSafeField(value, field) {
|
|
104
|
+
const bounded = boundedString(value);
|
|
105
|
+
if (bounded === undefined) {
|
|
106
|
+
throw new TaskWorkspaceError("INVALID_REQUEST", `missing or invalid field: ${field}`);
|
|
107
|
+
}
|
|
108
|
+
assertSafeFieldValue(bounded, field);
|
|
109
|
+
return bounded;
|
|
110
|
+
}
|
|
111
|
+
// As requireSafeField, but for an OPTIONAL field: absent → undefined, present → must be safe.
|
|
112
|
+
function optionalSafeField(value, field) {
|
|
113
|
+
if (value === undefined)
|
|
114
|
+
return undefined;
|
|
115
|
+
const bounded = boundedString(value);
|
|
116
|
+
if (bounded === undefined) {
|
|
117
|
+
throw new TaskWorkspaceError("INVALID_REQUEST", `missing or invalid field: ${field}`);
|
|
118
|
+
}
|
|
119
|
+
assertSafeFieldValue(bounded, field);
|
|
120
|
+
return bounded;
|
|
121
|
+
}
|
|
122
|
+
function mapError(error) {
|
|
123
|
+
if (error instanceof WorkspaceBodyTooLargeError) {
|
|
124
|
+
return {
|
|
125
|
+
status: 413,
|
|
126
|
+
body: errorBody("PAYLOAD_TOO_LARGE", "Request body exceeds the size limit."),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (error instanceof TaskWorkspaceError) {
|
|
130
|
+
const detail = error.reasons.length > 0 ? `${error.message}: ${error.reasons.join("; ")}` : error.message;
|
|
131
|
+
// Surface the caller-facing failure class (#449, ADR-0093 D3) alongside the code so a BFF/UI caller
|
|
132
|
+
// can branch on a stable signal (retryable / repairable / blocked / policy-denied / terminal) instead
|
|
133
|
+
// of the raw code list. The base body stays the redacted { error: { code, message } } envelope.
|
|
134
|
+
const base = errorBody(error.code, detail);
|
|
135
|
+
return {
|
|
136
|
+
status: error.status,
|
|
137
|
+
body: { ...base, error: { ...base.error, failureClass: error.failureClass } },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (error instanceof FilesError) {
|
|
141
|
+
return { status: error.status, body: errorBody(error.code, error.message) };
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
// Error responses are redacted with the SAME defense-in-depth scrubbing as success bodies, so a
|
|
146
|
+
// future failure detail that ever carries a path/secret-shaped value cannot leak to the browser.
|
|
147
|
+
async function runHandler(deps, work) {
|
|
148
|
+
try {
|
|
149
|
+
return await work();
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
const mapped = mapError(error);
|
|
153
|
+
if (mapped === undefined)
|
|
154
|
+
throw error;
|
|
155
|
+
return { status: mapped.status, body: redacted(deps, mapped.body) };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function redacted(deps, value) {
|
|
159
|
+
return deps.redactor(value);
|
|
160
|
+
}
|
|
161
|
+
function parseProvisionBody(body) {
|
|
162
|
+
const root = boundedString(body.root);
|
|
163
|
+
const taskId = boundedString(body.taskId);
|
|
164
|
+
const baseBranch = boundedString(body.baseBranch);
|
|
165
|
+
const requestedBy = boundedString(body.requestedBy);
|
|
166
|
+
const missing = [];
|
|
167
|
+
if (root === undefined)
|
|
168
|
+
missing.push("root");
|
|
169
|
+
if (taskId === undefined)
|
|
170
|
+
missing.push("taskId");
|
|
171
|
+
if (baseBranch === undefined)
|
|
172
|
+
missing.push("baseBranch");
|
|
173
|
+
if (requestedBy === undefined)
|
|
174
|
+
missing.push("requestedBy");
|
|
175
|
+
if (root === undefined ||
|
|
176
|
+
taskId === undefined ||
|
|
177
|
+
baseBranch === undefined ||
|
|
178
|
+
requestedBy === undefined) {
|
|
179
|
+
throw new TaskWorkspaceError("INVALID_REQUEST", `missing or invalid fields: ${missing.join(", ")}`);
|
|
180
|
+
}
|
|
181
|
+
// The free-form identity fields additionally reject control / zero-width / bidi code points.
|
|
182
|
+
assertSafeFieldValue(taskId, "taskId");
|
|
183
|
+
assertSafeFieldValue(requestedBy, "requestedBy");
|
|
184
|
+
return { root, taskId, baseBranch, requestedBy };
|
|
185
|
+
}
|
|
186
|
+
function parseExpectedState(value) {
|
|
187
|
+
if (value === undefined)
|
|
188
|
+
return undefined;
|
|
189
|
+
if (!isTaskWorkspaceLifecycleState(value)) {
|
|
190
|
+
throw new TaskWorkspaceError("INVALID_REQUEST", "expectedLifecycleState is invalid");
|
|
191
|
+
}
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
// POST /api/task-workspaces — provision (create or resume) a managed task workspace.
|
|
195
|
+
export async function handleProvisionTaskWorkspace(ctx, deps) {
|
|
196
|
+
const guard = requireService(deps);
|
|
197
|
+
if (isRouteResult(guard))
|
|
198
|
+
return guard;
|
|
199
|
+
return runHandler(deps, async () => {
|
|
200
|
+
const body = await readJsonObject(ctx.req);
|
|
201
|
+
const parsed = parseProvisionBody(body);
|
|
202
|
+
const resolvedRoot = await resolveRoot(deps.store, parsed.root, deps.redactor);
|
|
203
|
+
const request = {
|
|
204
|
+
repositoryRequestPath: resolvedRoot.realRoot,
|
|
205
|
+
taskId: parsed.taskId,
|
|
206
|
+
baseBranch: parsed.baseBranch,
|
|
207
|
+
requestedBy: parsed.requestedBy,
|
|
208
|
+
};
|
|
209
|
+
const result = await guard.provision(request);
|
|
210
|
+
return {
|
|
211
|
+
status: result.created ? 201 : 200,
|
|
212
|
+
body: redacted(deps, {
|
|
213
|
+
instance: result.instance,
|
|
214
|
+
binding: result.binding,
|
|
215
|
+
created: result.created,
|
|
216
|
+
}),
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// GET /api/task-workspaces/:workspaceId — read one persisted WorkspaceInstance.
|
|
221
|
+
export function handleGetTaskWorkspace(ctx, deps) {
|
|
222
|
+
const guard = requireService(deps);
|
|
223
|
+
if (isRouteResult(guard))
|
|
224
|
+
return guard;
|
|
225
|
+
const workspaceId = ctx.params.workspaceId ?? "";
|
|
226
|
+
const instance = guard.getInstance(workspaceId);
|
|
227
|
+
if (instance === undefined) {
|
|
228
|
+
return { status: 404, body: errorBody("WORKSPACE_NOT_FOUND", "Task workspace not found.") };
|
|
229
|
+
}
|
|
230
|
+
return { status: 200, body: redacted(deps, { instance }) };
|
|
231
|
+
}
|
|
232
|
+
// POST /api/task-workspaces/:workspaceId/activate — activate/resume a workspace and yield its binding.
|
|
233
|
+
export async function handleActivateTaskWorkspace(ctx, deps) {
|
|
234
|
+
const guard = requireService(deps);
|
|
235
|
+
if (isRouteResult(guard))
|
|
236
|
+
return guard;
|
|
237
|
+
return runHandler(deps, async () => {
|
|
238
|
+
const workspaceId = ctx.params.workspaceId ?? "";
|
|
239
|
+
const body = await readJsonObject(ctx.req);
|
|
240
|
+
const requestedBy = requireSafeField(body.requestedBy, "requestedBy");
|
|
241
|
+
const expectedLifecycleState = parseExpectedState(body.expectedLifecycleState);
|
|
242
|
+
const request = {
|
|
243
|
+
workspaceId,
|
|
244
|
+
taskId: optionalSafeField(body.taskId, "taskId") ?? "",
|
|
245
|
+
requestedBy,
|
|
246
|
+
acquireLock: body.acquireLock === true,
|
|
247
|
+
...(expectedLifecycleState !== undefined ? { expectedLifecycleState } : {}),
|
|
248
|
+
};
|
|
249
|
+
const result = await guard.activate(request);
|
|
250
|
+
return {
|
|
251
|
+
status: 200,
|
|
252
|
+
body: redacted(deps, { instance: result.instance, binding: result.binding }),
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
// ─── #446 active-binding + lifecycle routes ──────────────────────────────────────────────────────
|
|
257
|
+
// The shared active-workspace binding the Studio/editor/runtime/Git-Delivery surfaces consume. These
|
|
258
|
+
// are registered BEFORE `GET /api/task-workspaces/:workspaceId` so the literal `active` and the
|
|
259
|
+
// collection (`?root`) paths win over the `:workspaceId` param route.
|
|
260
|
+
function parseLifecycleActionBody(workspaceId, body) {
|
|
261
|
+
const requestedBy = requireSafeField(body.requestedBy, "requestedBy");
|
|
262
|
+
return { workspaceId, requestedBy };
|
|
263
|
+
}
|
|
264
|
+
// GET /api/task-workspaces?root=<repoRoot> — list the persisted instances for a repository root.
|
|
265
|
+
export async function handleListTaskWorkspaces(ctx, deps) {
|
|
266
|
+
const guard = requireLifecycle(deps);
|
|
267
|
+
if (isRouteResult(guard))
|
|
268
|
+
return guard;
|
|
269
|
+
return runHandler(deps, async () => {
|
|
270
|
+
const rootInput = ctx.url.searchParams.get("root");
|
|
271
|
+
const resolvedRoot = await resolveRoot(deps.store, rootInput, deps.redactor);
|
|
272
|
+
const instances = guard.list(resolvedRoot.realRoot);
|
|
273
|
+
return { status: 200, body: redacted(deps, { instances }) };
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
// GET /api/task-workspaces/active — the current active binding, or null in unbound mode.
|
|
277
|
+
export function handleGetActiveTaskWorkspace(_ctx, deps) {
|
|
278
|
+
const guard = requireLifecycle(deps);
|
|
279
|
+
if (isRouteResult(guard))
|
|
280
|
+
return guard;
|
|
281
|
+
return { status: 200, body: redacted(deps, { active: guard.getActive() ?? null }) };
|
|
282
|
+
}
|
|
283
|
+
// POST /api/task-workspaces/active — atomic switch: activate/resume the target and set it active.
|
|
284
|
+
export async function handleSetActiveTaskWorkspace(ctx, deps) {
|
|
285
|
+
const guard = requireLifecycle(deps);
|
|
286
|
+
if (isRouteResult(guard))
|
|
287
|
+
return guard;
|
|
288
|
+
return runHandler(deps, async () => {
|
|
289
|
+
const body = await readJsonObject(ctx.req);
|
|
290
|
+
const workspaceId = boundedString(body.workspaceId);
|
|
291
|
+
const requestedBy = boundedString(body.requestedBy);
|
|
292
|
+
if (workspaceId === undefined || requestedBy === undefined) {
|
|
293
|
+
throw new TaskWorkspaceError("INVALID_REQUEST", "missing or invalid fields: workspaceId, requestedBy");
|
|
294
|
+
}
|
|
295
|
+
// requestedBy is persisted as the active-pointer setBy — reject control/zero-width/bidi chars.
|
|
296
|
+
assertSafeFieldValue(requestedBy, "requestedBy");
|
|
297
|
+
const result = await guard.setActive({
|
|
298
|
+
workspaceId,
|
|
299
|
+
requestedBy,
|
|
300
|
+
acquireLock: body.acquireLock === true,
|
|
301
|
+
});
|
|
302
|
+
return {
|
|
303
|
+
status: 200,
|
|
304
|
+
body: redacted(deps, { instance: result.instance, binding: result.binding }),
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
// DELETE /api/task-workspaces/active — clear the active pointer → unbound mode.
|
|
309
|
+
export function handleClearActiveTaskWorkspace(_ctx, deps) {
|
|
310
|
+
const guard = requireLifecycle(deps);
|
|
311
|
+
if (isRouteResult(guard))
|
|
312
|
+
return guard;
|
|
313
|
+
guard.clearActive();
|
|
314
|
+
return { status: 200, body: redacted(deps, { active: null }) };
|
|
315
|
+
}
|
|
316
|
+
async function runLifecycleAction(ctx, deps, pick) {
|
|
317
|
+
const guard = requireLifecycle(deps);
|
|
318
|
+
if (isRouteResult(guard))
|
|
319
|
+
return guard;
|
|
320
|
+
return runHandler(deps, async () => {
|
|
321
|
+
const workspaceId = ctx.params.workspaceId ?? "";
|
|
322
|
+
const body = await readJsonObject(ctx.req);
|
|
323
|
+
const request = parseLifecycleActionBody(workspaceId, body);
|
|
324
|
+
const result = await pick(guard)(request);
|
|
325
|
+
return {
|
|
326
|
+
status: 200,
|
|
327
|
+
body: redacted(deps, { instance: result.instance, binding: result.binding }),
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// POST /api/task-workspaces/:workspaceId/pause — active → paused (clears the pointer if it was active).
|
|
332
|
+
export function handlePauseTaskWorkspace(ctx, deps) {
|
|
333
|
+
return runLifecycleAction(ctx, deps, (lifecycle) => lifecycle.pause);
|
|
334
|
+
}
|
|
335
|
+
// POST /api/task-workspaces/:workspaceId/resume — paused → active (sets the pointer).
|
|
336
|
+
export function handleResumeTaskWorkspace(ctx, deps) {
|
|
337
|
+
return runLifecycleAction(ctx, deps, (lifecycle) => lifecycle.resume);
|
|
338
|
+
}
|
|
339
|
+
// POST /api/task-workspaces/:workspaceId/handoff — active|paused → handoff-ready (requires clean worktree).
|
|
340
|
+
export function handleHandoffTaskWorkspace(ctx, deps) {
|
|
341
|
+
return runLifecycleAction(ctx, deps, (lifecycle) => lifecycle.prepareHandoff);
|
|
342
|
+
}
|
|
343
|
+
// ─── #447 reconciliation + repair routes ───────────────────────────────────────────────────────
|
|
344
|
+
// A `root` of undefined reconciles/reports across ALL repositories; a provided root scopes to one
|
|
345
|
+
// repository (resolved + realpath'd through the same containment as the other routes).
|
|
346
|
+
async function resolveOptionalRoot(deps, rootInput) {
|
|
347
|
+
if (rootInput === null || rootInput === undefined || rootInput.length === 0)
|
|
348
|
+
return undefined;
|
|
349
|
+
const resolved = await resolveRoot(deps.store, rootInput, deps.redactor);
|
|
350
|
+
return resolved.realRoot;
|
|
351
|
+
}
|
|
352
|
+
// GET /api/task-workspaces/reconciliation[?root=<repoRoot>] — read-only reconciliation report derived
|
|
353
|
+
// from the persisted (content-free) instance fields, no filesystem/git IO.
|
|
354
|
+
export async function handleGetTaskWorkspaceReconciliation(ctx, deps) {
|
|
355
|
+
const guard = requireReconciliation(deps);
|
|
356
|
+
if (isRouteResult(guard))
|
|
357
|
+
return guard;
|
|
358
|
+
return runHandler(deps, async () => {
|
|
359
|
+
const root = await resolveOptionalRoot(deps, ctx.url.searchParams.get("root"));
|
|
360
|
+
const report = guard.report(root);
|
|
361
|
+
return { status: 200, body: redacted(deps, { report }) };
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
// POST /api/task-workspaces/reconciliation — run a live reconciliation pass (verifies disk + git,
|
|
365
|
+
// persists the classification) and return the fresh report. CSRF is enforced for this POST.
|
|
366
|
+
export async function handleReconcileTaskWorkspaces(ctx, deps) {
|
|
367
|
+
const guard = requireReconciliation(deps);
|
|
368
|
+
if (isRouteResult(guard))
|
|
369
|
+
return guard;
|
|
370
|
+
return runHandler(deps, async () => {
|
|
371
|
+
const body = await readJsonObject(ctx.req);
|
|
372
|
+
const root = await resolveOptionalRoot(deps, optionalSafeField(body.root, "root"));
|
|
373
|
+
const report = await guard.reconcile(root);
|
|
374
|
+
return { status: 200, body: redacted(deps, { report }) };
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
function parseRepairStrategy(value) {
|
|
378
|
+
if (!isWorkspaceRecoveryStrategy(value)) {
|
|
379
|
+
throw new TaskWorkspaceError("INVALID_REQUEST", "missing or invalid field: strategy");
|
|
380
|
+
}
|
|
381
|
+
return value;
|
|
382
|
+
}
|
|
383
|
+
// POST /api/task-workspaces/:workspaceId/repair — controlled, operator-approval-gated repair.
|
|
384
|
+
export async function handleRepairTaskWorkspace(ctx, deps) {
|
|
385
|
+
const guard = requireRepair(deps);
|
|
386
|
+
if (isRouteResult(guard))
|
|
387
|
+
return guard;
|
|
388
|
+
return runHandler(deps, async () => {
|
|
389
|
+
const workspaceId = ctx.params.workspaceId ?? "";
|
|
390
|
+
const body = await readJsonObject(ctx.req);
|
|
391
|
+
const requestedBy = requireSafeField(body.requestedBy, "requestedBy");
|
|
392
|
+
const result = await guard.repair({
|
|
393
|
+
workspaceId,
|
|
394
|
+
requestedBy,
|
|
395
|
+
strategy: parseRepairStrategy(body.strategy),
|
|
396
|
+
operatorApproved: body.operatorApproved === true,
|
|
397
|
+
});
|
|
398
|
+
return {
|
|
399
|
+
status: 200,
|
|
400
|
+
body: redacted(deps, {
|
|
401
|
+
instance: result.instance,
|
|
402
|
+
binding: result.binding,
|
|
403
|
+
strategy: result.strategy,
|
|
404
|
+
applied: result.applied,
|
|
405
|
+
outcome: result.outcome,
|
|
406
|
+
status: result.status,
|
|
407
|
+
driftMarkers: result.driftMarkers,
|
|
408
|
+
operatorActionRequired: result.operatorActionRequired,
|
|
409
|
+
}),
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
// ─── #448 health + governed cleanup routes ──────────────────────────────────────────────────────
|
|
414
|
+
// The literal `health` and `cleanup/orphans` paths are registered BEFORE `:workspaceId` so they win by
|
|
415
|
+
// literal-segment specificity. A `root` of undefined scopes health/orphan-cleanup across ALL managed
|
|
416
|
+
// repositories; a provided root scopes to one (resolved + realpath'd through the same containment).
|
|
417
|
+
// GET /api/task-workspaces/health[?root=<repoRoot>] — content-free operational health + drift + orphan
|
|
418
|
+
// report (read-only; live filesystem + git probing, no persistence).
|
|
419
|
+
export async function handleGetTaskWorkspaceHealth(ctx, deps) {
|
|
420
|
+
const guard = requireHealth(deps);
|
|
421
|
+
if (isRouteResult(guard))
|
|
422
|
+
return guard;
|
|
423
|
+
return runHandler(deps, async () => {
|
|
424
|
+
const root = await resolveOptionalRoot(deps, ctx.url.searchParams.get("root"));
|
|
425
|
+
const report = await guard.report(root);
|
|
426
|
+
return { status: 200, body: redacted(deps, { report }) };
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
function parseCleanupMode(value) {
|
|
430
|
+
if (value === "request" || value === "complete")
|
|
431
|
+
return value;
|
|
432
|
+
throw new TaskWorkspaceError("INVALID_REQUEST", "missing or invalid field: mode (expected 'request' or 'complete')");
|
|
433
|
+
}
|
|
434
|
+
// POST /api/task-workspaces/:workspaceId/cleanup — request (settled → cleanup-pending) or complete
|
|
435
|
+
// (governed, live-verified physical removal). Operator-approval gated; CSRF inherited.
|
|
436
|
+
export async function handleCleanupTaskWorkspace(ctx, deps) {
|
|
437
|
+
const guard = requireCleanup(deps);
|
|
438
|
+
if (isRouteResult(guard))
|
|
439
|
+
return guard;
|
|
440
|
+
return runHandler(deps, async () => {
|
|
441
|
+
const workspaceId = ctx.params.workspaceId ?? "";
|
|
442
|
+
const body = await readJsonObject(ctx.req);
|
|
443
|
+
const requestedBy = requireSafeField(body.requestedBy, "requestedBy");
|
|
444
|
+
const result = await guard.cleanup({
|
|
445
|
+
workspaceId,
|
|
446
|
+
requestedBy,
|
|
447
|
+
operatorApproved: body.operatorApproved === true,
|
|
448
|
+
mode: parseCleanupMode(body.mode),
|
|
449
|
+
});
|
|
450
|
+
return {
|
|
451
|
+
status: 200,
|
|
452
|
+
body: redacted(deps, {
|
|
453
|
+
outcome: result.outcome,
|
|
454
|
+
workspaceId: result.workspaceId,
|
|
455
|
+
...(result.instance !== undefined ? { instance: result.instance } : {}),
|
|
456
|
+
...(result.refusalReason !== undefined ? { refusalReason: result.refusalReason } : {}),
|
|
457
|
+
}),
|
|
458
|
+
};
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
// POST /api/task-workspaces/cleanup/orphans — governed removal of orphaned managed worktrees (on-disk
|
|
462
|
+
// directories with no persisted record). Operator-approval gated; CSRF inherited.
|
|
463
|
+
export async function handleCleanupOrphanTaskWorkspaces(ctx, deps) {
|
|
464
|
+
const guard = requireCleanup(deps);
|
|
465
|
+
if (isRouteResult(guard))
|
|
466
|
+
return guard;
|
|
467
|
+
return runHandler(deps, async () => {
|
|
468
|
+
const body = await readJsonObject(ctx.req);
|
|
469
|
+
const requestedBy = requireSafeField(body.requestedBy, "requestedBy");
|
|
470
|
+
const root = await resolveOptionalRoot(deps, optionalSafeField(body.root, "root"));
|
|
471
|
+
const result = await guard.cleanupOrphans({
|
|
472
|
+
...(root !== undefined ? { repositoryRoot: root } : {}),
|
|
473
|
+
requestedBy,
|
|
474
|
+
operatorApproved: body.operatorApproved === true,
|
|
475
|
+
});
|
|
476
|
+
return {
|
|
477
|
+
status: 200,
|
|
478
|
+
body: redacted(deps, { removed: result.removed, refused: result.refused }),
|
|
479
|
+
};
|
|
480
|
+
});
|
|
481
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { type WorkspaceInstance } from "@oscharko-dev/keiko-contracts";
|
|
3
|
+
export interface WorkspaceInstanceStore {
|
|
4
|
+
readonly getById: (workspaceId: string) => WorkspaceInstance | undefined;
|
|
5
|
+
readonly findByRepositoryAndTask: (repositoryId: string, taskId: string) => WorkspaceInstance | undefined;
|
|
6
|
+
readonly listByRepository: (repositoryId: string) => readonly WorkspaceInstance[];
|
|
7
|
+
readonly listAll: () => readonly WorkspaceInstance[];
|
|
8
|
+
readonly upsert: (instance: WorkspaceInstance) => WorkspaceInstance;
|
|
9
|
+
readonly delete: (workspaceId: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function buildWorkspaceInstanceStoreOverDatabase(db: DatabaseSync): WorkspaceInstanceStore;
|
|
12
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/task-workspace/store.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAKL,KAAK,iBAAiB,EAGvB,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,OAAO,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,iBAAiB,GAAG,SAAS,CAAC;IACzE,QAAQ,CAAC,uBAAuB,EAAE,CAChC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,KACX,iBAAiB,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,gBAAgB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,SAAS,iBAAiB,EAAE,CAAC;IAIlF,QAAQ,CAAC,OAAO,EAAE,MAAM,SAAS,iBAAiB,EAAE,CAAC;IACrD,QAAQ,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,iBAAiB,CAAC;IACpE,QAAQ,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CAChD;AAyHD,wBAAgB,uCAAuC,CAAC,EAAE,EAAE,YAAY,GAAG,sBAAsB,CAmChG"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Durable persistence for managed task-workspace instances (Issue #445, Epic #443).
|
|
2
|
+
//
|
|
3
|
+
// A dedicated store composed over the SAME node:sqlite DatabaseSync handle as the UI store and the
|
|
4
|
+
// relationship engine (schema.ts §V7), mirroring the relationship-store composition pattern so the
|
|
5
|
+
// V7 sibling table shares the single-writer transaction model. The store is the durable home for the
|
|
6
|
+
// `WorkspaceInstance` the contract defines; #446 (binding) and #447 (reconcile/repair) read and
|
|
7
|
+
// extend it without a second persistence layer.
|
|
8
|
+
//
|
|
9
|
+
// Every write is gated by the contract's `validateWorkspaceInstance` (content-free closed allowlist,
|
|
10
|
+
// SC3) and every read re-validates the reconstructed row, so a corrupt or smuggled record can neither
|
|
11
|
+
// be stored nor silently trusted on the way out. The unique (repository_id, task_id) index enforces
|
|
12
|
+
// the idempotency invariant at the DB layer (AC3).
|
|
13
|
+
import { validateWorkspaceInstance, } from "@oscharko-dev/keiko-contracts";
|
|
14
|
+
const COLUMNS = `
|
|
15
|
+
workspace_id, schema_version, task_id, repository_id, repository_root, base_branch, task_branch,
|
|
16
|
+
managed_worktree_path, gitdir_identity, lifecycle_state, health, lock_json, created_at, updated_at,
|
|
17
|
+
last_verified_at, last_verified_head, drift_markers_json, recovery_hints_json, audit_correlation_id
|
|
18
|
+
`;
|
|
19
|
+
const SQL_GET_BY_ID = `SELECT ${COLUMNS} FROM task_workspace_instances WHERE workspace_id = ?`;
|
|
20
|
+
const SQL_FIND_BY_REPO_TASK = `SELECT ${COLUMNS} FROM task_workspace_instances WHERE repository_id = ? AND task_id = ?`;
|
|
21
|
+
const SQL_LIST_BY_REPO = `SELECT ${COLUMNS} FROM task_workspace_instances WHERE repository_id = ? ORDER BY updated_at DESC, workspace_id`;
|
|
22
|
+
const SQL_LIST_ALL = `SELECT ${COLUMNS} FROM task_workspace_instances ORDER BY updated_at DESC, workspace_id`;
|
|
23
|
+
const SQL_DELETE = "DELETE FROM task_workspace_instances WHERE workspace_id = ?";
|
|
24
|
+
const SQL_UPSERT = `
|
|
25
|
+
INSERT INTO task_workspace_instances (${COLUMNS})
|
|
26
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
27
|
+
ON CONFLICT(workspace_id) DO UPDATE SET
|
|
28
|
+
schema_version = excluded.schema_version,
|
|
29
|
+
task_id = excluded.task_id,
|
|
30
|
+
repository_id = excluded.repository_id,
|
|
31
|
+
repository_root = excluded.repository_root,
|
|
32
|
+
base_branch = excluded.base_branch,
|
|
33
|
+
task_branch = excluded.task_branch,
|
|
34
|
+
managed_worktree_path = excluded.managed_worktree_path,
|
|
35
|
+
gitdir_identity = excluded.gitdir_identity,
|
|
36
|
+
lifecycle_state = excluded.lifecycle_state,
|
|
37
|
+
health = excluded.health,
|
|
38
|
+
lock_json = excluded.lock_json,
|
|
39
|
+
created_at = excluded.created_at,
|
|
40
|
+
updated_at = excluded.updated_at,
|
|
41
|
+
last_verified_at = excluded.last_verified_at,
|
|
42
|
+
last_verified_head = excluded.last_verified_head,
|
|
43
|
+
drift_markers_json = excluded.drift_markers_json,
|
|
44
|
+
recovery_hints_json = excluded.recovery_hints_json,
|
|
45
|
+
audit_correlation_id = excluded.audit_correlation_id
|
|
46
|
+
RETURNING ${COLUMNS}
|
|
47
|
+
`;
|
|
48
|
+
function parseJsonArray(json) {
|
|
49
|
+
const parsed = JSON.parse(json);
|
|
50
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
51
|
+
}
|
|
52
|
+
function rowToInstance(row) {
|
|
53
|
+
const lock = row.lock_json === null ? null : JSON.parse(row.lock_json);
|
|
54
|
+
const instance = {
|
|
55
|
+
schemaVersion: row.schema_version,
|
|
56
|
+
workspaceId: row.workspace_id,
|
|
57
|
+
taskId: row.task_id,
|
|
58
|
+
repositoryId: row.repository_id,
|
|
59
|
+
repositoryRoot: row.repository_root,
|
|
60
|
+
baseBranch: row.base_branch,
|
|
61
|
+
taskBranch: row.task_branch,
|
|
62
|
+
managedWorktreePath: row.managed_worktree_path,
|
|
63
|
+
gitdirIdentity: row.gitdir_identity,
|
|
64
|
+
lifecycleState: row.lifecycle_state,
|
|
65
|
+
health: row.health,
|
|
66
|
+
lock,
|
|
67
|
+
createdAt: row.created_at,
|
|
68
|
+
updatedAt: row.updated_at,
|
|
69
|
+
...(row.last_verified_at !== null ? { lastVerifiedAt: row.last_verified_at } : {}),
|
|
70
|
+
...(row.last_verified_head !== null ? { lastVerifiedHead: row.last_verified_head } : {}),
|
|
71
|
+
driftMarkers: parseJsonArray(row.drift_markers_json),
|
|
72
|
+
recoveryHints: parseJsonArray(row.recovery_hints_json),
|
|
73
|
+
auditCorrelationId: row.audit_correlation_id,
|
|
74
|
+
};
|
|
75
|
+
const validation = validateWorkspaceInstance(instance);
|
|
76
|
+
if (!validation.ok) {
|
|
77
|
+
throw new Error(`persisted task-workspace instance is invalid (${row.workspace_id}): ${validation.reasons.join("; ")}`);
|
|
78
|
+
}
|
|
79
|
+
return instance;
|
|
80
|
+
}
|
|
81
|
+
function upsertParams(instance) {
|
|
82
|
+
return [
|
|
83
|
+
instance.workspaceId,
|
|
84
|
+
instance.schemaVersion,
|
|
85
|
+
instance.taskId,
|
|
86
|
+
instance.repositoryId,
|
|
87
|
+
instance.repositoryRoot,
|
|
88
|
+
instance.baseBranch,
|
|
89
|
+
instance.taskBranch,
|
|
90
|
+
instance.managedWorktreePath,
|
|
91
|
+
instance.gitdirIdentity,
|
|
92
|
+
instance.lifecycleState,
|
|
93
|
+
instance.health,
|
|
94
|
+
instance.lock === null ? null : JSON.stringify(instance.lock),
|
|
95
|
+
instance.createdAt,
|
|
96
|
+
instance.updatedAt,
|
|
97
|
+
instance.lastVerifiedAt ?? null,
|
|
98
|
+
instance.lastVerifiedHead ?? null,
|
|
99
|
+
JSON.stringify(instance.driftMarkers),
|
|
100
|
+
JSON.stringify(instance.recoveryHints),
|
|
101
|
+
instance.auditCorrelationId,
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
export function buildWorkspaceInstanceStoreOverDatabase(db) {
|
|
105
|
+
return {
|
|
106
|
+
getById: (workspaceId) => {
|
|
107
|
+
const row = db.prepare(SQL_GET_BY_ID).get(workspaceId);
|
|
108
|
+
return row === undefined ? undefined : rowToInstance(row);
|
|
109
|
+
},
|
|
110
|
+
findByRepositoryAndTask: (repositoryId, taskId) => {
|
|
111
|
+
const row = db.prepare(SQL_FIND_BY_REPO_TASK).get(repositoryId, taskId);
|
|
112
|
+
return row === undefined ? undefined : rowToInstance(row);
|
|
113
|
+
},
|
|
114
|
+
listByRepository: (repositoryId) => db.prepare(SQL_LIST_BY_REPO).all(repositoryId).map(rowToInstance),
|
|
115
|
+
listAll: () => db.prepare(SQL_LIST_ALL).all().map(rowToInstance),
|
|
116
|
+
upsert: (instance) => {
|
|
117
|
+
const validation = validateWorkspaceInstance(instance);
|
|
118
|
+
if (!validation.ok) {
|
|
119
|
+
throw new Error(`refusing to persist invalid workspace instance: ${validation.reasons.join("; ")}`);
|
|
120
|
+
}
|
|
121
|
+
const row = db.prepare(SQL_UPSERT).get(...upsertParams(instance));
|
|
122
|
+
return rowToInstance(row);
|
|
123
|
+
},
|
|
124
|
+
delete: (workspaceId) => {
|
|
125
|
+
db.prepare(SQL_DELETE).run(workspaceId);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|