@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,206 @@
|
|
|
1
|
+
// Pure projection: content-free kernel facts → UI-safe GitDeliveryActionSheet (Issue #473, Epic #470).
|
|
2
|
+
//
|
|
3
|
+
// This module is a PURE function of (resolvedInputs, worktreeSnapshot, trusted policy packs, approval
|
|
4
|
+
// shape, provider state, providerReady). It runs the kernel preflight (keiko-tools) and the policy
|
|
5
|
+
// evaluator (keiko-contracts), maps their content-free outputs into the action-sheet's typed
|
|
6
|
+
// expected-blocker and recovery-hint vocabularies, and hands the assembled facts to the contract's
|
|
7
|
+
// buildGitDeliveryActionSheet. No IO, no clock, no randomness, no request access — the caller (the
|
|
8
|
+
// route handler) gathers the facts and supplies the actionId and trusted packs.
|
|
9
|
+
//
|
|
10
|
+
// AUTHORITY (AC1): the policy decision, approval necessity, and required approvers are derived ONLY
|
|
11
|
+
// from evaluateGitPolicy over the server's TRUSTED packs. The approval payload is consumed for its
|
|
12
|
+
// SHAPE alone (whether a granted approval is attached); it never asserts the policy decision.
|
|
13
|
+
//
|
|
14
|
+
// Content-free: every value produced here is a count, flag, branch name, or typed/closed-vocabulary
|
|
15
|
+
// code. Never diff content, file paths, secrets, command strings, or raw subprocess output.
|
|
16
|
+
import { buildGitDeliveryActionSheet, gitDeliverySuggestedRecoveryStrategy, evaluateGitPolicy, } from "@oscharko-dev/keiko-contracts";
|
|
17
|
+
import { evaluateGitPreflight, } from "@oscharko-dev/keiko-tools";
|
|
18
|
+
// Demotes an expired granted approval to "not attached" so the action-sheet projection never shows an
|
|
19
|
+
// expired grant as satisfied/ready. The #472 execution kernel re-validates the token and expiry
|
|
20
|
+
// independently before any mutation; this is the preview-time mirror of that check.
|
|
21
|
+
function effectiveApproval(approval, nowMs) {
|
|
22
|
+
if (approval.required && approval.expiresAtMs !== undefined && approval.expiresAtMs <= nowMs) {
|
|
23
|
+
return { required: false };
|
|
24
|
+
}
|
|
25
|
+
return approval;
|
|
26
|
+
}
|
|
27
|
+
// ─── Target-branch derivation (for policy evaluation) ───────────────────────────────
|
|
28
|
+
// The affected branch name the policy evaluator matches branch-pattern rules against. Mirrors the
|
|
29
|
+
// contract's affectedBranchOf but is local so this module owns its own policy-context derivation.
|
|
30
|
+
function targetBranchName(inputs) {
|
|
31
|
+
switch (inputs.kind) {
|
|
32
|
+
case "branch-create":
|
|
33
|
+
return inputs.branchName;
|
|
34
|
+
case "push":
|
|
35
|
+
return inputs.sourceBranchName;
|
|
36
|
+
case "pr-create":
|
|
37
|
+
case "pr-update":
|
|
38
|
+
return inputs.headBranchName;
|
|
39
|
+
default:
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// ─── Expected-blocker mapping (AC2/AC3) ─────────────────────────────────────────────
|
|
44
|
+
function preflightBlocker(finding) {
|
|
45
|
+
return {
|
|
46
|
+
source: "preflight",
|
|
47
|
+
severity: finding.severity,
|
|
48
|
+
remediation: finding.remediation,
|
|
49
|
+
reasonCode: finding.code,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function policyBlocker(decision) {
|
|
53
|
+
if (decision.outcome !== "blocked") {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
source: "policy",
|
|
58
|
+
severity: "blocking",
|
|
59
|
+
remediation: "user-actionable",
|
|
60
|
+
reasonCode: decision.reason,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const PROVIDER_BLOCKER_KINDS = [
|
|
64
|
+
"merge",
|
|
65
|
+
"pr-create",
|
|
66
|
+
"pr-update",
|
|
67
|
+
];
|
|
68
|
+
function providerBlocker(inputs, mergeReadiness) {
|
|
69
|
+
if (!PROVIDER_BLOCKER_KINDS.includes(inputs.kind)) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
if (mergeReadiness?.ready !== false || mergeReadiness.blockingReason === undefined) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
source: "provider",
|
|
77
|
+
severity: "advisory",
|
|
78
|
+
remediation: "user-actionable",
|
|
79
|
+
reasonCode: mergeReadiness.blockingReason,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function collectExpectedBlockers(findings, decision, inputs, mergeReadiness) {
|
|
83
|
+
const blockers = findings.map(preflightBlocker);
|
|
84
|
+
const policy = policyBlocker(decision);
|
|
85
|
+
if (policy !== undefined) {
|
|
86
|
+
blockers.push(policy);
|
|
87
|
+
}
|
|
88
|
+
const provider = providerBlocker(inputs, mergeReadiness);
|
|
89
|
+
if (provider !== undefined) {
|
|
90
|
+
blockers.push(provider);
|
|
91
|
+
}
|
|
92
|
+
return blockers;
|
|
93
|
+
}
|
|
94
|
+
// ─── Recovery-hint mapping (AC4 — common failure classes, same surface) ─────────────
|
|
95
|
+
// A deterministic, exhaustive table from each preflight finding code to a recovery action hint. The
|
|
96
|
+
// "recover-via-strategy" hints are produced separately so they can carry a suggestedRecoveryStrategy.
|
|
97
|
+
const STRATEGY_RECOVERY_CODES = new Set([
|
|
98
|
+
"dirty-worktree-impacts-recovery",
|
|
99
|
+
"recovery-target-unset",
|
|
100
|
+
]);
|
|
101
|
+
const FINDING_RECOVERY_HINT = {
|
|
102
|
+
"detached-head": "configure-upstream",
|
|
103
|
+
"branch-already-exists": "adjust-policy-target",
|
|
104
|
+
"base-branch-missing": "adjust-policy-target",
|
|
105
|
+
"switch-target-missing": "adjust-policy-target",
|
|
106
|
+
"no-changes-to-stage": "stage-changes",
|
|
107
|
+
"nothing-staged-to-unstage": "stage-changes",
|
|
108
|
+
"nothing-staged-to-commit": "stage-changes",
|
|
109
|
+
"untracked-files-impacted": "stage-changes",
|
|
110
|
+
"no-upstream-configured": "configure-upstream",
|
|
111
|
+
"nothing-to-push": "retry",
|
|
112
|
+
"non-fast-forward": "resolve-conflicts",
|
|
113
|
+
"remote-alias-missing": "configure-upstream",
|
|
114
|
+
"remote-unreachable": "retry",
|
|
115
|
+
"operation-in-progress": "abort-in-progress-operation",
|
|
116
|
+
"no-operation-to-abort": "retry",
|
|
117
|
+
"recovery-target-unset": "recover-via-strategy",
|
|
118
|
+
"dirty-worktree-impacts-recovery": "recover-via-strategy",
|
|
119
|
+
};
|
|
120
|
+
function worktreeIsDirty(snapshot) {
|
|
121
|
+
return (snapshot.stagedFileCount > 0 ||
|
|
122
|
+
snapshot.unstagedFileCount > 0 ||
|
|
123
|
+
snapshot.untrackedFileCount > 0);
|
|
124
|
+
}
|
|
125
|
+
function recoveryHintForFinding(finding, inputs, snapshot) {
|
|
126
|
+
if (STRATEGY_RECOVERY_CODES.has(finding.code)) {
|
|
127
|
+
return {
|
|
128
|
+
actionHint: "recover-via-strategy",
|
|
129
|
+
remediation: finding.remediation,
|
|
130
|
+
suggestedRecoveryStrategy: gitDeliverySuggestedRecoveryStrategy(inputs.kind, worktreeIsDirty(snapshot)),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return { actionHint: FINDING_RECOVERY_HINT[finding.code], remediation: finding.remediation };
|
|
134
|
+
}
|
|
135
|
+
function policyRecoveryHint(decision) {
|
|
136
|
+
if (decision.outcome === "approval-gated") {
|
|
137
|
+
return { actionHint: "request-approval", remediation: "user-actionable" };
|
|
138
|
+
}
|
|
139
|
+
if (decision.outcome === "blocked") {
|
|
140
|
+
const actionHint = decision.reason === "approval-expired" ? "request-approval" : "adjust-policy-target";
|
|
141
|
+
return { actionHint, remediation: "user-actionable" };
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
// De-duplicates recovery hints on a stable signature (action + strategy) so the same class of fix is
|
|
146
|
+
// surfaced once even when several findings map to it. First occurrence wins (order-preserving).
|
|
147
|
+
function dedupeRecoveryHints(hints) {
|
|
148
|
+
const seen = new Set();
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const hint of hints) {
|
|
151
|
+
const signature = `${hint.actionHint}:${hint.suggestedRecoveryStrategy ?? ""}`;
|
|
152
|
+
if (!seen.has(signature)) {
|
|
153
|
+
seen.add(signature);
|
|
154
|
+
out.push(hint);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
function buildRecoveryHints(findings, decision, inputs, snapshot, providerReady) {
|
|
160
|
+
const hints = findings.map((finding) => recoveryHintForFinding(finding, inputs, snapshot));
|
|
161
|
+
const policy = policyRecoveryHint(decision);
|
|
162
|
+
if (policy !== undefined) {
|
|
163
|
+
hints.push(policy);
|
|
164
|
+
}
|
|
165
|
+
if (!providerReady) {
|
|
166
|
+
hints.push({ actionHint: "wait-for-provider", remediation: "internal" });
|
|
167
|
+
}
|
|
168
|
+
return dedupeRecoveryHints(hints);
|
|
169
|
+
}
|
|
170
|
+
function definedProviderState(state) {
|
|
171
|
+
const out = {};
|
|
172
|
+
if (state.pullRequest !== undefined)
|
|
173
|
+
out.pullRequest = state.pullRequest;
|
|
174
|
+
if (state.mergeReadiness !== undefined)
|
|
175
|
+
out.mergeReadiness = state.mergeReadiness;
|
|
176
|
+
if (state.branchProtection !== undefined)
|
|
177
|
+
out.branchProtection = state.branchProtection;
|
|
178
|
+
if (state.checks !== undefined)
|
|
179
|
+
out.checks = state.checks;
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The pure projection. Runs preflight + policy over the supplied content-free facts and assembles the
|
|
184
|
+
* UI-safe action sheet. Deterministic: identical facts always yield an identical sheet.
|
|
185
|
+
*/
|
|
186
|
+
export function buildActionSheetFromFacts(facts) {
|
|
187
|
+
const { resolvedInputs, worktreeSnapshot, providerState } = facts;
|
|
188
|
+
const preflight = evaluateGitPreflight(resolvedInputs, worktreeSnapshot);
|
|
189
|
+
const policyDecision = evaluateGitPolicy(facts.policyPacks.orgPack, facts.policyPacks.repoPack, {
|
|
190
|
+
actionKind: resolvedInputs.kind,
|
|
191
|
+
targetBranchName: targetBranchName(resolvedInputs),
|
|
192
|
+
activeProviderCapabilities: facts.activeProviderCapabilities,
|
|
193
|
+
});
|
|
194
|
+
const expectedBlockers = collectExpectedBlockers(preflight.findings, policyDecision, resolvedInputs, providerState.mergeReadiness);
|
|
195
|
+
const recovery = buildRecoveryHints(preflight.findings, policyDecision, resolvedInputs, worktreeSnapshot, facts.providerReady);
|
|
196
|
+
return buildGitDeliveryActionSheet({
|
|
197
|
+
actionId: facts.actionId,
|
|
198
|
+
resolvedInputs,
|
|
199
|
+
policyDecision,
|
|
200
|
+
approvalRequirement: effectiveApproval(facts.approvalRequirement, facts.nowMs),
|
|
201
|
+
providerReady: facts.providerReady,
|
|
202
|
+
expectedBlockers,
|
|
203
|
+
recovery,
|
|
204
|
+
...definedProviderState(providerState),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type GitDeliveryApprovalRequirement, type GitDeliveryProviderCapability, type GitDeliveryResolvedInputs } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
import type { GitWorktreeSnapshot } from "@oscharko-dev/keiko-tools";
|
|
3
|
+
import type { RouteContext, RouteDefinition, RouteResult } from "../routes.js";
|
|
4
|
+
import type { UiHandlerDeps } from "../deps.js";
|
|
5
|
+
import { type GitDeliveryProviderStateFacts, type GitDeliveryTrustedPolicyPacks } from "./actionSheetProjection.js";
|
|
6
|
+
export type GitDeliveryActionSheetErrorCode = "GIT_DELIVERY_ACTION_SHEET_BAD_REQUEST" | "GIT_DELIVERY_ACTION_SHEET_PAYLOAD_TOO_LARGE" | "GIT_DELIVERY_ACTION_SHEET_FORBIDDEN_PAYLOAD";
|
|
7
|
+
export interface GitDeliveryActionSheetErrorBody {
|
|
8
|
+
readonly error: {
|
|
9
|
+
readonly code: GitDeliveryActionSheetErrorCode;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
interface ValidatedRequest {
|
|
14
|
+
readonly resolvedInputs: GitDeliveryResolvedInputs;
|
|
15
|
+
readonly worktreeSnapshot: GitWorktreeSnapshot;
|
|
16
|
+
readonly approvalRequirement: GitDeliveryApprovalRequirement;
|
|
17
|
+
readonly providerState: GitDeliveryProviderStateFacts;
|
|
18
|
+
readonly activeProviderCapabilities: readonly GitDeliveryProviderCapability[];
|
|
19
|
+
}
|
|
20
|
+
export interface GitDeliveryActionSheetRouteOptions {
|
|
21
|
+
readonly idGenerator?: (request: ValidatedRequest) => string;
|
|
22
|
+
readonly policyPacks?: (deps: UiHandlerDeps) => GitDeliveryTrustedPolicyPacks;
|
|
23
|
+
readonly now?: () => number;
|
|
24
|
+
}
|
|
25
|
+
export declare const createHandleGitDeliveryActionSheet: (options?: GitDeliveryActionSheetRouteOptions) => ((ctx: RouteContext, deps: UiHandlerDeps) => Promise<RouteResult>);
|
|
26
|
+
export declare const handleGitDeliveryActionSheet: (ctx: RouteContext, deps: UiHandlerDeps) => Promise<RouteResult>;
|
|
27
|
+
export declare const GIT_DELIVERY_ACTION_SHEET_ROUTE_GROUP: readonly RouteDefinition[];
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=actionSheetRoutes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actionSheetRoutes.d.ts","sourceRoot":"","sources":["../../src/gitDelivery/actionSheetRoutes.ts"],"names":[],"mappings":"AA2BA,OAAO,EAQL,KAAK,8BAA8B,EACnC,KAAK,6BAA6B,EAClC,KAAK,yBAAyB,EAC/B,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,OAAO,EAEL,KAAK,6BAA6B,EAClC,KAAK,6BAA6B,EACnC,MAAM,4BAA4B,CAAC;AAIpC,MAAM,MAAM,+BAA+B,GACvC,uCAAuC,GACvC,6CAA6C,GAC7C,6CAA6C,CAAC;AAElD,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,IAAI,EAAE,+BAA+B,CAAC;QAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;KAC1B,CAAC;CACH;AA6MD,UAAU,gBAAgB;IACxB,QAAQ,CAAC,cAAc,EAAE,yBAAyB,CAAC;IACnD,QAAQ,CAAC,gBAAgB,EAAE,mBAAmB,CAAC;IAC/C,QAAQ,CAAC,mBAAmB,EAAE,8BAA8B,CAAC;IAC7D,QAAQ,CAAC,aAAa,EAAE,6BAA6B,CAAC;IACtD,QAAQ,CAAC,0BAA0B,EAAE,SAAS,6BAA6B,EAAE,CAAC;CAC/E;AAgFD,MAAM,WAAW,kCAAkC;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,CAAC;IAE7D,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,6BAA6B,CAAC;IAG9E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED,eAAO,MAAM,kCAAkC,GAC7C,UAAS,kCAAuC,KAC/C,CAAC,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,WAAW,CAAC,CAqCnE,CAAC;AAEF,eAAO,MAAM,4BAA4B,QAvC/B,YAAY,QAAQ,aAAa,KAAK,OAAO,CAAC,WAAW,CAuCa,CAAC;AAIjF,eAAO,MAAM,qCAAqC,EAAE,SAAS,eAAe,EAM3E,CAAC"}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// Governed Git delivery action-sheet BFF route (Issue #473, Epic #470).
|
|
2
|
+
//
|
|
3
|
+
// Single additive, READ-ONLY HTTP handler:
|
|
4
|
+
// * POST /api/git-delivery/action-sheet
|
|
5
|
+
//
|
|
6
|
+
// It is a COMPUTATIONAL endpoint: it NEVER mutates the repository and NEVER calls runGitMutation. It
|
|
7
|
+
// validates a content-free request envelope, runs the kernel preflight + the server-side policy
|
|
8
|
+
// evaluator over TRUSTED packs, and returns a UI-safe GitDeliveryActionSheet projection.
|
|
9
|
+
//
|
|
10
|
+
// AUTHORITY (AC1): the policy decision, approval necessity, and required approvers come from
|
|
11
|
+
// evaluateGitPolicy over the server's trusted packs — NEVER from a client-asserted field. The request
|
|
12
|
+
// may carry a GRANTED approval payload; only its SHAPE is validated (isGitDeliveryApprovalRequirement).
|
|
13
|
+
// There is no client-supplied policy-decision field, and there must never be one.
|
|
14
|
+
//
|
|
15
|
+
// Hard constraints (mirroring qualityIntelligence/handoffRoutes.ts):
|
|
16
|
+
// * Unknown top-level keys are REJECTED so a leaky client cannot smuggle content through the seam.
|
|
17
|
+
// * Every string field is scrubbed for credential-shaped substrings before processing.
|
|
18
|
+
// * Bounded body read (~64 KiB) → 413; bad JSON / failed validation → 400; both as a content-free
|
|
19
|
+
// `{ error: { code, message } }` envelope keyed by a typed code union.
|
|
20
|
+
// * CSRF + JSON content-type are enforced CENTRALLY by server.ts for POST — no per-route opt-in.
|
|
21
|
+
//
|
|
22
|
+
// Content-free: the response carries counts, flags, branch names, and typed codes only — never diff
|
|
23
|
+
// content, file paths, secrets, command strings, or raw subprocess output. The assembled sheet is
|
|
24
|
+
// additionally deep-redacted through deps.redactor before it leaves the server (defence-in-depth).
|
|
25
|
+
import { sha256Hex } from "@oscharko-dev/keiko-security";
|
|
26
|
+
import { isGitDeliveryApprovalRequirement, isGitDeliveryBranchProtection, isGitDeliveryChecksState, isGitDeliveryMergeReadiness, isGitDeliveryProviderCapability, isGitDeliveryPullRequestState, parseGitDeliveryResolvedInputs, } from "@oscharko-dev/keiko-contracts";
|
|
27
|
+
import { containsForbiddenSecretShape } from "../qualityIntelligence/connectorErrors.js";
|
|
28
|
+
import { buildActionSheetFromFacts, } from "./actionSheetProjection.js";
|
|
29
|
+
const SAFE_MESSAGES = {
|
|
30
|
+
GIT_DELIVERY_ACTION_SHEET_BAD_REQUEST: "The request body is not a valid Git delivery action-sheet request.",
|
|
31
|
+
GIT_DELIVERY_ACTION_SHEET_PAYLOAD_TOO_LARGE: "The Git delivery action-sheet request exceeds the maximum permitted size.",
|
|
32
|
+
GIT_DELIVERY_ACTION_SHEET_FORBIDDEN_PAYLOAD: "The action-sheet request contained a forbidden field. Requests may not carry credentials, headers, or URLs.",
|
|
33
|
+
};
|
|
34
|
+
const errResult = (status, code) => ({
|
|
35
|
+
status,
|
|
36
|
+
body: { error: { code, message: SAFE_MESSAGES[code] } },
|
|
37
|
+
});
|
|
38
|
+
// ─── Bounded body reading ──────────────────────────────────────────────────────────────
|
|
39
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
40
|
+
class BodyTooLargeError extends Error {
|
|
41
|
+
constructor() {
|
|
42
|
+
super("Action-sheet body exceeds the route cap");
|
|
43
|
+
this.name = "BodyTooLargeError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const readBody = (req) => new Promise((resolve, reject) => {
|
|
47
|
+
const chunks = [];
|
|
48
|
+
let total = 0;
|
|
49
|
+
let capped = false;
|
|
50
|
+
req.on("data", (chunk) => {
|
|
51
|
+
total += chunk.length;
|
|
52
|
+
if (total > MAX_BODY_BYTES) {
|
|
53
|
+
if (!capped) {
|
|
54
|
+
capped = true;
|
|
55
|
+
chunks.length = 0;
|
|
56
|
+
reject(new BodyTooLargeError());
|
|
57
|
+
req.resume();
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
chunks.push(chunk);
|
|
62
|
+
});
|
|
63
|
+
req.on("end", () => {
|
|
64
|
+
if (!capped)
|
|
65
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
66
|
+
});
|
|
67
|
+
req.on("error", reject);
|
|
68
|
+
});
|
|
69
|
+
// ─── Validation helpers ──────────────────────────────────────────────────────────────
|
|
70
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
71
|
+
const hasOnlyAllowedKeys = (obj, allowed) => {
|
|
72
|
+
for (const key of Object.keys(obj)) {
|
|
73
|
+
if (!allowed.has(key))
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
};
|
|
78
|
+
const ALLOWED_REQUEST_KEYS = new Set([
|
|
79
|
+
"schemaVersion",
|
|
80
|
+
"resolvedInputs",
|
|
81
|
+
"worktreeSnapshot",
|
|
82
|
+
"approval",
|
|
83
|
+
"providerState",
|
|
84
|
+
"activeProviderCapabilities",
|
|
85
|
+
]);
|
|
86
|
+
const ALLOWED_PROVIDER_STATE_KEYS = new Set([
|
|
87
|
+
"pullRequest",
|
|
88
|
+
"mergeReadiness",
|
|
89
|
+
"branchProtection",
|
|
90
|
+
"checks",
|
|
91
|
+
]);
|
|
92
|
+
const scanForbiddenStrings = (value) => {
|
|
93
|
+
if (typeof value === "string")
|
|
94
|
+
return containsForbiddenSecretShape(value);
|
|
95
|
+
if (Array.isArray(value))
|
|
96
|
+
return value.some(scanForbiddenStrings);
|
|
97
|
+
if (isPlainObject(value)) {
|
|
98
|
+
for (const v of Object.values(value)) {
|
|
99
|
+
if (scanForbiddenStrings(v))
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
};
|
|
106
|
+
// Zero-width, bidi-control, and BOM format characters. Branch names / refs / IDs in this content-free
|
|
107
|
+
// request never legitimately contain them; a U+202E override or zero-width char would spoof the
|
|
108
|
+
// approval target on the surface whose whole purpose is to show WHICH branch is being approved
|
|
109
|
+
// (Trojan-Source). Reject fail-closed at the boundary. (C0/C1 controls are already barred by git ref
|
|
110
|
+
// rules and the kernel adapter's NUL/flag guards, so this set deliberately omits them.)
|
|
111
|
+
const UNSAFE_FORMAT_CHAR = new RegExp("[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF]", "u");
|
|
112
|
+
const scanUnsafeFormatChars = (value) => {
|
|
113
|
+
if (typeof value === "string")
|
|
114
|
+
return UNSAFE_FORMAT_CHAR.test(value);
|
|
115
|
+
if (Array.isArray(value))
|
|
116
|
+
return value.some(scanUnsafeFormatChars);
|
|
117
|
+
if (isPlainObject(value))
|
|
118
|
+
return Object.values(value).some(scanUnsafeFormatChars);
|
|
119
|
+
return false;
|
|
120
|
+
};
|
|
121
|
+
// ─── Worktree-snapshot shape validation (content-free; counts/flags/names only) ──────
|
|
122
|
+
const isNonNegInt = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
123
|
+
const isBool = (value) => typeof value === "boolean";
|
|
124
|
+
const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
125
|
+
const isOptional = (check, v) => v === undefined || check(v);
|
|
126
|
+
const ABORTABLE_OPERATIONS = new Set([
|
|
127
|
+
"merge",
|
|
128
|
+
"rebase",
|
|
129
|
+
"cherry-pick",
|
|
130
|
+
"revert",
|
|
131
|
+
"bisect",
|
|
132
|
+
]);
|
|
133
|
+
function snapshotCountFieldsValid(value) {
|
|
134
|
+
return (isBool(value.headDetached) &&
|
|
135
|
+
isOptional((v) => typeof v === "string", value.currentBranchName) &&
|
|
136
|
+
isNonNegInt(value.stagedFileCount) &&
|
|
137
|
+
isNonNegInt(value.unstagedFileCount) &&
|
|
138
|
+
isNonNegInt(value.untrackedFileCount));
|
|
139
|
+
}
|
|
140
|
+
function snapshotRemoteFieldsValid(value) {
|
|
141
|
+
return (isBool(value.hasUpstream) &&
|
|
142
|
+
isNonNegInt(value.aheadCount) &&
|
|
143
|
+
isNonNegInt(value.behindCount) &&
|
|
144
|
+
isStringArray(value.existingLocalBranchNames) &&
|
|
145
|
+
isStringArray(value.remoteAliases) &&
|
|
146
|
+
isOptional(isBool, value.remoteReachable) &&
|
|
147
|
+
isOptional((v) => typeof v === "string" && ABORTABLE_OPERATIONS.has(v), value.operationInProgress));
|
|
148
|
+
}
|
|
149
|
+
function isGitWorktreeSnapshot(value) {
|
|
150
|
+
return (isPlainObject(value) && snapshotCountFieldsValid(value) && snapshotRemoteFieldsValid(value));
|
|
151
|
+
}
|
|
152
|
+
// ─── Provider-state validation ───────────────────────────────────────────────────────
|
|
153
|
+
function validateProviderState(raw) {
|
|
154
|
+
if (raw === undefined)
|
|
155
|
+
return { ok: true, value: {} };
|
|
156
|
+
if (!isPlainObject(raw) || !hasOnlyAllowedKeys(raw, ALLOWED_PROVIDER_STATE_KEYS)) {
|
|
157
|
+
return { ok: false };
|
|
158
|
+
}
|
|
159
|
+
if (!isOptional(isGitDeliveryPullRequestState, raw.pullRequest) ||
|
|
160
|
+
!isOptional(isGitDeliveryMergeReadiness, raw.mergeReadiness) ||
|
|
161
|
+
!isOptional(isGitDeliveryBranchProtection, raw.branchProtection) ||
|
|
162
|
+
!isOptional(isGitDeliveryChecksState, raw.checks)) {
|
|
163
|
+
return { ok: false };
|
|
164
|
+
}
|
|
165
|
+
return { ok: true, value: raw };
|
|
166
|
+
}
|
|
167
|
+
function validateProviderCapabilities(raw) {
|
|
168
|
+
if (raw === undefined)
|
|
169
|
+
return { ok: true, value: [] };
|
|
170
|
+
if (!Array.isArray(raw) || !raw.every(isGitDeliveryProviderCapability)) {
|
|
171
|
+
return { ok: false };
|
|
172
|
+
}
|
|
173
|
+
return { ok: true, value: raw };
|
|
174
|
+
}
|
|
175
|
+
function validateApproval(raw) {
|
|
176
|
+
if (raw === undefined)
|
|
177
|
+
return { ok: true, value: { required: false } };
|
|
178
|
+
if (!isGitDeliveryApprovalRequirement(raw))
|
|
179
|
+
return { ok: false };
|
|
180
|
+
return { ok: true, value: raw };
|
|
181
|
+
}
|
|
182
|
+
const badRequest = () => ({
|
|
183
|
+
kind: "err",
|
|
184
|
+
result: errResult(400, "GIT_DELIVERY_ACTION_SHEET_BAD_REQUEST"),
|
|
185
|
+
});
|
|
186
|
+
// Envelope-level gate: shape, allowed keys, schema version, secret-shape scan, and unsafe-format-char
|
|
187
|
+
// (bidi/zero-width/BOM) scan. Extracted so validateRequest stays within the complexity budget.
|
|
188
|
+
function preValidateEnvelope(parsed) {
|
|
189
|
+
if (!isPlainObject(parsed) || !hasOnlyAllowedKeys(parsed, ALLOWED_REQUEST_KEYS)) {
|
|
190
|
+
return { ok: false, result: errResult(400, "GIT_DELIVERY_ACTION_SHEET_BAD_REQUEST") };
|
|
191
|
+
}
|
|
192
|
+
if (parsed.schemaVersion !== "1") {
|
|
193
|
+
return { ok: false, result: errResult(400, "GIT_DELIVERY_ACTION_SHEET_BAD_REQUEST") };
|
|
194
|
+
}
|
|
195
|
+
if (scanForbiddenStrings(parsed)) {
|
|
196
|
+
return { ok: false, result: errResult(400, "GIT_DELIVERY_ACTION_SHEET_FORBIDDEN_PAYLOAD") };
|
|
197
|
+
}
|
|
198
|
+
if (scanUnsafeFormatChars(parsed)) {
|
|
199
|
+
return { ok: false, result: errResult(400, "GIT_DELIVERY_ACTION_SHEET_BAD_REQUEST") };
|
|
200
|
+
}
|
|
201
|
+
return { ok: true, obj: parsed };
|
|
202
|
+
}
|
|
203
|
+
function validateRequest(parsed) {
|
|
204
|
+
const envelope = preValidateEnvelope(parsed);
|
|
205
|
+
if (!envelope.ok)
|
|
206
|
+
return { kind: "err", result: envelope.result };
|
|
207
|
+
const obj = envelope.obj;
|
|
208
|
+
const inputs = parseGitDeliveryResolvedInputs(obj.resolvedInputs);
|
|
209
|
+
if (!inputs.ok)
|
|
210
|
+
return badRequest();
|
|
211
|
+
if (!isGitWorktreeSnapshot(obj.worktreeSnapshot))
|
|
212
|
+
return badRequest();
|
|
213
|
+
const approval = validateApproval(obj.approval);
|
|
214
|
+
if (!approval.ok)
|
|
215
|
+
return badRequest();
|
|
216
|
+
const providerState = validateProviderState(obj.providerState);
|
|
217
|
+
if (!providerState.ok)
|
|
218
|
+
return badRequest();
|
|
219
|
+
const capabilities = validateProviderCapabilities(obj.activeProviderCapabilities);
|
|
220
|
+
if (!capabilities.ok)
|
|
221
|
+
return badRequest();
|
|
222
|
+
return {
|
|
223
|
+
kind: "ok",
|
|
224
|
+
request: {
|
|
225
|
+
resolvedInputs: inputs.value,
|
|
226
|
+
worktreeSnapshot: obj.worktreeSnapshot,
|
|
227
|
+
approvalRequirement: approval.value,
|
|
228
|
+
providerState: providerState.value,
|
|
229
|
+
activeProviderCapabilities: capabilities.value,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
// ─── Deterministic action id ─────────────────────────────────────────────────────────
|
|
234
|
+
// A stable id derived from the content-free request facts so identical requests are addressable
|
|
235
|
+
// identically and the projection is reproducible. Injectable via options.idGenerator for tests.
|
|
236
|
+
function defaultActionId(request) {
|
|
237
|
+
const seed = JSON.stringify({
|
|
238
|
+
inputs: request.resolvedInputs,
|
|
239
|
+
snapshot: request.worktreeSnapshot,
|
|
240
|
+
capabilities: request.activeProviderCapabilities,
|
|
241
|
+
});
|
|
242
|
+
return `git-delivery-action-${sha256Hex(seed).slice(0, 24)}`;
|
|
243
|
+
}
|
|
244
|
+
export const createHandleGitDeliveryActionSheet = (options = {}) => {
|
|
245
|
+
const idGenerator = options.idGenerator ?? defaultActionId;
|
|
246
|
+
const resolvePolicyPacks = options.policyPacks ?? (() => ({}));
|
|
247
|
+
const now = options.now ?? (() => Date.now());
|
|
248
|
+
return async (ctx, deps) => {
|
|
249
|
+
let raw;
|
|
250
|
+
try {
|
|
251
|
+
raw = await readBody(ctx.req);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
if (error instanceof BodyTooLargeError) {
|
|
255
|
+
return errResult(413, "GIT_DELIVERY_ACTION_SHEET_PAYLOAD_TOO_LARGE");
|
|
256
|
+
}
|
|
257
|
+
return errResult(400, "GIT_DELIVERY_ACTION_SHEET_BAD_REQUEST");
|
|
258
|
+
}
|
|
259
|
+
let parsed;
|
|
260
|
+
try {
|
|
261
|
+
parsed = JSON.parse(raw);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return errResult(400, "GIT_DELIVERY_ACTION_SHEET_BAD_REQUEST");
|
|
265
|
+
}
|
|
266
|
+
const validation = validateRequest(parsed);
|
|
267
|
+
if (validation.kind === "err")
|
|
268
|
+
return validation.result;
|
|
269
|
+
const { request } = validation;
|
|
270
|
+
const sheet = buildActionSheetFromFacts({
|
|
271
|
+
actionId: idGenerator(request),
|
|
272
|
+
resolvedInputs: request.resolvedInputs,
|
|
273
|
+
worktreeSnapshot: request.worktreeSnapshot,
|
|
274
|
+
approvalRequirement: request.approvalRequirement,
|
|
275
|
+
policyPacks: resolvePolicyPacks(deps),
|
|
276
|
+
activeProviderCapabilities: request.activeProviderCapabilities,
|
|
277
|
+
providerState: request.providerState,
|
|
278
|
+
providerReady: true,
|
|
279
|
+
nowMs: now(),
|
|
280
|
+
});
|
|
281
|
+
return { status: 200, body: deps.redactor(sheet) };
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
export const handleGitDeliveryActionSheet = createHandleGitDeliveryActionSheet();
|
|
285
|
+
// Mechanically-mergeable route group. routes.ts spreads this into API_ROUTES, mirroring
|
|
286
|
+
// QI_HANDOFF_ROUTE_GROUP, so concurrent #470 epic merges stay merge-safe.
|
|
287
|
+
export const GIT_DELIVERY_ACTION_SHEET_ROUTE_GROUP = [
|
|
288
|
+
{
|
|
289
|
+
method: "POST",
|
|
290
|
+
pattern: "/api/git-delivery/action-sheet",
|
|
291
|
+
handler: handleGitDeliveryActionSheet,
|
|
292
|
+
},
|
|
293
|
+
];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type GitRepositoryAgentOperationRequest, type GitRepositoryAgentOperationResponse } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
import { type RouteContext, type RouteDefinition, type RouteResult } from "../routes.js";
|
|
3
|
+
import type { UiHandlerDeps } from "../deps.js";
|
|
4
|
+
interface IdempotencyEntry {
|
|
5
|
+
readonly fingerprint: string;
|
|
6
|
+
readonly pending?: Promise<GitRepositoryAgentOperationResponse>;
|
|
7
|
+
readonly result?: GitRepositoryAgentOperationResponse;
|
|
8
|
+
}
|
|
9
|
+
export declare const DEFAULT_IDEMPOTENCY_MAX_ENTRIES = 1024;
|
|
10
|
+
export declare const DEFAULT_IDEMPOTENCY_TTL_MS: number;
|
|
11
|
+
export interface IdempotencyCacheOptions {
|
|
12
|
+
readonly maxEntries?: number;
|
|
13
|
+
readonly ttlMs?: number;
|
|
14
|
+
readonly now?: () => number;
|
|
15
|
+
}
|
|
16
|
+
export declare class IdempotencyCache {
|
|
17
|
+
private readonly entries;
|
|
18
|
+
private readonly maxEntries;
|
|
19
|
+
private readonly ttlMs;
|
|
20
|
+
private readonly now;
|
|
21
|
+
constructor(options?: IdempotencyCacheOptions);
|
|
22
|
+
get size(): number;
|
|
23
|
+
get(key: string): IdempotencyEntry | undefined;
|
|
24
|
+
set(key: string, entry: IdempotencyEntry): void;
|
|
25
|
+
delete(key: string): void;
|
|
26
|
+
private pruneExpired;
|
|
27
|
+
private evictOverflow;
|
|
28
|
+
}
|
|
29
|
+
export declare function handleGitAgentOperationWithDelegate(request: GitRepositoryAgentOperationRequest, fingerprint: string, delegate: () => Promise<RouteResult>, cache?: IdempotencyCache): Promise<RouteResult>;
|
|
30
|
+
export declare function handleGitAgentOperation(ctx: RouteContext, deps: UiHandlerDeps): Promise<RouteResult>;
|
|
31
|
+
export declare const GIT_AGENT_OPERATION_ROUTE_GROUP: readonly RouteDefinition[];
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=agentOperationsRoutes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentOperationsRoutes.d.ts","sourceRoot":"","sources":["../../src/gitDelivery/agentOperationsRoutes.ts"],"names":[],"mappings":"AAUA,OAAO,EAKL,KAAK,kCAAkC,EACvC,KAAK,mCAAmC,EACzC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAa,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAEpG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AA+ChD,UAAU,gBAAgB;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,mCAAmC,CAAC,CAAC;IAChE,QAAQ,CAAC,MAAM,CAAC,EAAE,mCAAmC,CAAC;CACvD;AAKD,eAAO,MAAM,+BAA+B,OAAO,CAAC;AACpD,eAAO,MAAM,0BAA0B,QAAiB,CAAC;AAEzD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAaD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAC1D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEhB,OAAO,GAAE,uBAA4B;IASxD,IAAW,IAAI,IAAI,MAAM,CAExB;IAEM,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;IAa9C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAO/C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIhC,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,aAAa;CAatB;AAqVD,wBAAsB,mCAAmC,CACvD,OAAO,EAAE,kCAAkC,EAC3C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,EACpC,KAAK,GAAE,gBAAmC,GACzC,OAAO,CAAC,WAAW,CAAC,CAuBtB;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,WAAW,CAAC,CAMtB;AAED,eAAO,MAAM,+BAA+B,EAAE,SAAS,eAAe,EAMrE,CAAC"}
|