@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,192 @@
|
|
|
1
|
+
// Governed GitHub pull request execution core for the #477 PR routes (Epic #470, ADR-0064).
|
|
2
|
+
//
|
|
3
|
+
// The PR preview + execute routes share ONE path: resolve and authorize the project workspace, build a
|
|
4
|
+
// TRUSTWORTHY snapshot from the live worktree, drive the #477 PR gateway `runGitPullRequest` (the
|
|
5
|
+
// SEPARATE PR-orchestration authority — preflight + policy + approval gates, executing through a narrow
|
|
6
|
+
// `gh api` adapter with a dedicated PR-only allowlist), and append a content-free evidence record
|
|
7
|
+
// through the #474 ledger for EVERY terminal outcome (allowed and blocked alike — AC5). No second
|
|
8
|
+
// orchestrator, no generic shell, no widening of the local mutation or read-only inspection allowlists.
|
|
9
|
+
// The Node `gh api` effect is injected via seams so route tests run deterministically against a fake.
|
|
10
|
+
import { GIT_DELIVERY_POLICY_SCHEMA_VERSION, GIT_DELIVERY_RISK_CLASS_SEVERITY, evaluateGitPolicy, gitPullRequestLabelSuggestionsFor, gitPullRequestLinkageSuggestionsFor, gitPullRequestReadinessFor, gitPullRequestRecommendationFor, synthesizePullRequestMetadata, } from "@oscharko-dev/keiko-contracts";
|
|
11
|
+
import { evaluateGitPullRequestEffectivePolicy, runGitPullRequest, } from "@oscharko-dev/keiko-tools";
|
|
12
|
+
import { createNodeGitPullRequestAdapter } from "@oscharko-dev/keiko-tools/internal/git-mutation";
|
|
13
|
+
import { defaultGitDeliveryActionId, gitDeliveryMutationResponse, persistGitDeliveryEvidence, readWorktreeSnapshotFor, } from "./execution.js";
|
|
14
|
+
// Default trusted PR policy: PERMIT `pr-create` / `pr-update` whose BASE is a legitimate integration
|
|
15
|
+
// branch (dev, main, release/*, feat/*) and only within the protected-or-merge ceiling. A base outside
|
|
16
|
+
// this allow-list is blocked with `policy-pack-blocked`. Opening a PR TO a protected base is a governed,
|
|
17
|
+
// normal operation (unlike a direct push to one, which the #476 publish pack blocks). Per-target
|
|
18
|
+
// approval escalation is a deployment override (an `approval-gated` rule); the gateway supports it but
|
|
19
|
+
// the default keeps PR creation reviewable-by-default with merge-approval deferred to #478. Applies only
|
|
20
|
+
// when governed git delivery is ENABLED and no stricter pack is configured; still EVALUATED every time.
|
|
21
|
+
const PR_BASE_CONSTRAINTS = [
|
|
22
|
+
{ kind: "risk-class-ceiling", maxRiskClass: "protected-or-merge" },
|
|
23
|
+
{
|
|
24
|
+
kind: "branch-pattern",
|
|
25
|
+
patterns: [
|
|
26
|
+
{ matchKind: "exact", value: "dev" },
|
|
27
|
+
{ matchKind: "exact", value: "main" },
|
|
28
|
+
{ matchKind: "prefix", value: "release/" },
|
|
29
|
+
{ matchKind: "prefix", value: "feat/" },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
export const KEIKO_DEFAULT_PR_POLICY_PACK = {
|
|
34
|
+
schemaVersion: GIT_DELIVERY_POLICY_SCHEMA_VERSION,
|
|
35
|
+
repoId: "keiko-pr-default",
|
|
36
|
+
rules: [
|
|
37
|
+
{ actionKind: "pr-create", decision: "constrained", constraints: [...PR_BASE_CONSTRAINTS] },
|
|
38
|
+
{ actionKind: "pr-update", decision: "constrained", constraints: [...PR_BASE_CONSTRAINTS] },
|
|
39
|
+
],
|
|
40
|
+
defaultRule: { decision: "blocked" },
|
|
41
|
+
};
|
|
42
|
+
function prAdapterFor(workspace, seams, now) {
|
|
43
|
+
if (seams.prAdapterFactory !== undefined)
|
|
44
|
+
return seams.prAdapterFactory(workspace);
|
|
45
|
+
return createNodeGitPullRequestAdapter({ workspace, processEnv: process.env, now });
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Runs ONE governed PR operation end-to-end: live snapshot → PR gateway (preflight + policy + approval +
|
|
49
|
+
* execute) → evidence. Returns the gateway lifecycle result; the caller projects it into a content-free
|
|
50
|
+
* HTTP body. Evidence is appended best-effort BEFORE the caller responds, for allowed AND blocked alike.
|
|
51
|
+
*/
|
|
52
|
+
export async function executeGovernedPullRequest(command, approval, workspace, deps, seams) {
|
|
53
|
+
const now = seams.now ?? Date.now;
|
|
54
|
+
const snapshot = await readWorktreeSnapshotFor(workspace, seams, now);
|
|
55
|
+
const adapter = prAdapterFor(workspace, seams, now);
|
|
56
|
+
const packs = seams.policyPacks ?? { repoPack: KEIKO_DEFAULT_PR_POLICY_PACK };
|
|
57
|
+
const newActionId = seams.newActionId ?? (() => defaultGitDeliveryActionId(command, now()));
|
|
58
|
+
const result = await runGitPullRequest({ command, approval }, {
|
|
59
|
+
adapter,
|
|
60
|
+
snapshot,
|
|
61
|
+
...(packs.orgPack !== undefined ? { orgPolicyPack: packs.orgPack } : {}),
|
|
62
|
+
...(packs.repoPack !== undefined ? { repoPolicyPack: packs.repoPack } : {}),
|
|
63
|
+
now,
|
|
64
|
+
newActionId,
|
|
65
|
+
});
|
|
66
|
+
persistGitDeliveryEvidence(deps, result.lifecycle, snapshot, workspace.root, now);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
// ─── Deterministic change-narrative derivation (content-free) ─────────────────────────────────────
|
|
70
|
+
const CHANGE_TYPE_BY_PREFIX = {
|
|
71
|
+
fix: "fix",
|
|
72
|
+
feat: "feat",
|
|
73
|
+
docs: "docs",
|
|
74
|
+
chore: "chore",
|
|
75
|
+
test: "test",
|
|
76
|
+
refactor: "refactor",
|
|
77
|
+
};
|
|
78
|
+
function changeTypeFromBranch(headBranch) {
|
|
79
|
+
const segments = headBranch.split("/");
|
|
80
|
+
const lead = (segments[0] ?? "").toLowerCase();
|
|
81
|
+
return CHANGE_TYPE_BY_PREFIX[lead] ?? "feat";
|
|
82
|
+
}
|
|
83
|
+
// A best-effort, content-free narrative from the live snapshot: commit count proxied by the ahead-count,
|
|
84
|
+
// file count by the working-tree counts, change type inferred from the head-branch namespace. Area tokens
|
|
85
|
+
// are intentionally empty here (the snapshot carries no diff paths); the title falls back to the branch
|
|
86
|
+
// slug, which is the actual, user-meaningful context.
|
|
87
|
+
function narrativeFromSnapshot(headBranch, snapshot) {
|
|
88
|
+
return {
|
|
89
|
+
commitCount: Math.max(snapshot.aheadCount, 1),
|
|
90
|
+
fileCount: snapshot.stagedFileCount + snapshot.unstagedFileCount + snapshot.untrackedFileCount,
|
|
91
|
+
areaCount: 0,
|
|
92
|
+
areas: [],
|
|
93
|
+
touchesTests: false,
|
|
94
|
+
changeType: changeTypeFromBranch(headBranch),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function commandTitle(command) {
|
|
98
|
+
return command.title;
|
|
99
|
+
}
|
|
100
|
+
function commandBody(command) {
|
|
101
|
+
return command.body;
|
|
102
|
+
}
|
|
103
|
+
const UTF8 = new TextEncoder();
|
|
104
|
+
function riskDigestFor(command, policyOutcome) {
|
|
105
|
+
const riskClass = "protected-or-merge";
|
|
106
|
+
return {
|
|
107
|
+
riskClass,
|
|
108
|
+
riskSeverity: GIT_DELIVERY_RISK_CLASS_SEVERITY[riskClass],
|
|
109
|
+
policyOutcome,
|
|
110
|
+
isDraft: command.kind === "pr-create" ? command.isDraft : command.convertToDraft,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function previewReadiness(command, snapshot) {
|
|
114
|
+
return gitPullRequestReadinessFor({
|
|
115
|
+
headBranchName: command.headBranchName,
|
|
116
|
+
baseBranchName: command.baseBranchName,
|
|
117
|
+
// A PR can only be opened from a published head; the current branch's upstream is the proxy.
|
|
118
|
+
headPublished: snapshot.hasUpstream,
|
|
119
|
+
baseExists: true,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// Assembles an editable Markdown body seed from the synthesized draft + linkage refs.
|
|
123
|
+
function composeBody(draft, issueRefs) {
|
|
124
|
+
const linkage = issueRefs.length > 0 ? `\n\n## Linked issues\nRefs ${issueRefs.join(", ")}` : "";
|
|
125
|
+
return `## Summary\n${draft.composedTitle}\n\n## Risk\n${draft.riskNarrative}${linkage}`;
|
|
126
|
+
}
|
|
127
|
+
function derivePrPreviewParts(command, snapshot, packs) {
|
|
128
|
+
const decision = evaluateGitPolicy(packs.orgPack, packs.repoPack, {
|
|
129
|
+
actionKind: command.kind,
|
|
130
|
+
targetBranchName: command.baseBranchName,
|
|
131
|
+
activeProviderCapabilities: [],
|
|
132
|
+
});
|
|
133
|
+
const effective = evaluateGitPullRequestEffectivePolicy(decision, command.baseBranchName, [], command.kind);
|
|
134
|
+
const narrative = narrativeFromSnapshot(command.headBranchName, snapshot);
|
|
135
|
+
const riskDigest = riskDigestFor(command, effective.outcome);
|
|
136
|
+
const draft = synthesizePullRequestMetadata(narrative, riskDigest, command.headBranchName, command.baseBranchName);
|
|
137
|
+
const readiness = previewReadiness(command, snapshot);
|
|
138
|
+
return {
|
|
139
|
+
effectiveOutcome: effective.outcome,
|
|
140
|
+
...(effective.blockReason !== undefined ? { blockReason: effective.blockReason } : {}),
|
|
141
|
+
riskDigest,
|
|
142
|
+
draft,
|
|
143
|
+
readiness,
|
|
144
|
+
recommendation: gitPullRequestRecommendationFor(readiness, riskDigest),
|
|
145
|
+
labels: gitPullRequestLabelSuggestionsFor(narrative).suggestedLabelNames,
|
|
146
|
+
linkage: gitPullRequestLinkageSuggestionsFor(command.headBranchName).suggestedIssueRefs,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function buildGitDeliveryPrPreview(command, snapshot, packs) {
|
|
150
|
+
const parts = derivePrPreviewParts(command, snapshot, packs);
|
|
151
|
+
return {
|
|
152
|
+
schemaVersion: "1",
|
|
153
|
+
actionKind: command.kind,
|
|
154
|
+
headBranchName: command.headBranchName,
|
|
155
|
+
baseBranchName: command.baseBranchName,
|
|
156
|
+
riskClass: parts.riskDigest.riskClass,
|
|
157
|
+
riskSeverity: parts.riskDigest.riskSeverity,
|
|
158
|
+
isDraft: parts.riskDigest.isDraft,
|
|
159
|
+
policyOutcome: parts.effectiveOutcome,
|
|
160
|
+
...(parts.blockReason !== undefined ? { policyBlockReason: parts.blockReason } : {}),
|
|
161
|
+
composedTitle: parts.draft.composedTitle,
|
|
162
|
+
composedBody: composeBody(parts.draft, parts.linkage),
|
|
163
|
+
riskNarrative: parts.draft.riskNarrative,
|
|
164
|
+
recommendation: parts.recommendation,
|
|
165
|
+
readiness: {
|
|
166
|
+
objectExists: parts.readiness.objectExists,
|
|
167
|
+
reviewReady: parts.readiness.reviewReady,
|
|
168
|
+
blockerCodes: parts.readiness.blockers.map((b) => b.code),
|
|
169
|
+
},
|
|
170
|
+
suggestedLabels: parts.labels,
|
|
171
|
+
suggestedIssueRefs: parts.linkage,
|
|
172
|
+
titleByteLength: UTF8.encode(commandTitle(command)).length,
|
|
173
|
+
bodyByteLength: UTF8.encode(commandBody(command)).length,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export function gitDeliveryPrExecuteResponse(result) {
|
|
177
|
+
const base = gitDeliveryMutationResponse(result.lifecycle);
|
|
178
|
+
const withId = result.createdPrExternalId !== undefined
|
|
179
|
+
? { ...base, createdPrExternalId: result.createdPrExternalId }
|
|
180
|
+
: base;
|
|
181
|
+
if (result.rejection === undefined) {
|
|
182
|
+
return withId;
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
...withId,
|
|
186
|
+
prRejectionReason: result.rejection.reason,
|
|
187
|
+
recoveryDisposition: result.rejection.disposition,
|
|
188
|
+
...(result.rejection.actionHint !== undefined
|
|
189
|
+
? { recoveryActionHint: result.rejection.actionHint }
|
|
190
|
+
: {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RouteContext, RouteDefinition, RouteResult } from "../routes.js";
|
|
2
|
+
import type { UiHandlerDeps } from "../deps.js";
|
|
3
|
+
import { type GitDeliveryPullRequestSeams } from "./prExecution.js";
|
|
4
|
+
export type GitDeliveryPrErrorCode = "GIT_DELIVERY_PR_BAD_REQUEST" | "GIT_DELIVERY_PR_PAYLOAD_TOO_LARGE" | "GIT_DELIVERY_PR_FORBIDDEN_PAYLOAD" | "GIT_DELIVERY_PR_UNKNOWN_PROJECT" | "GIT_DELIVERY_PR_WORKTREE_UNAVAILABLE";
|
|
5
|
+
export interface GitDeliveryPrRouteOptions {
|
|
6
|
+
readonly execution?: GitDeliveryPullRequestSeams;
|
|
7
|
+
}
|
|
8
|
+
export declare const createHandlePrPreview: (options?: GitDeliveryPrRouteOptions) => ((ctx: RouteContext, deps: UiHandlerDeps) => Promise<RouteResult>);
|
|
9
|
+
export declare const createHandlePrExecute: (options?: GitDeliveryPrRouteOptions) => ((ctx: RouteContext, deps: UiHandlerDeps) => Promise<RouteResult>);
|
|
10
|
+
export declare const createGitDeliveryPrRouteGroup: (options?: GitDeliveryPrRouteOptions) => readonly RouteDefinition[];
|
|
11
|
+
export declare const GIT_DELIVERY_PR_ROUTE_GROUP: readonly RouteDefinition[];
|
|
12
|
+
//# sourceMappingURL=prRoutes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prRoutes.d.ts","sourceRoot":"","sources":["../../src/gitDelivery/prRoutes.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,OAAO,EAKL,KAAK,2BAA2B,EACjC,MAAM,kBAAkB,CAAC;AAa1B,MAAM,MAAM,sBAAsB,GAC9B,6BAA6B,GAC7B,mCAAmC,GACnC,mCAAmC,GACnC,iCAAiC,GACjC,sCAAsC,CAAC;AAmB3C,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,SAAS,CAAC,EAAE,2BAA2B,CAAC;CAClD;AAmMD,eAAO,MAAM,qBAAqB,GAChC,UAAS,yBAA8B,KACtC,CAAC,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,WAAW,CAAC,CAsBnE,CAAC;AAIF,eAAO,MAAM,qBAAqB,GAChC,UAAS,yBAA8B,KACtC,CAAC,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,WAAW,CAAC,CAmBnE,CAAC;AAIF,eAAO,MAAM,6BAA6B,GACxC,UAAS,yBAA8B,KACtC,SAAS,eAAe,EAW1B,CAAC;AAEF,eAAO,MAAM,2BAA2B,EAAE,SAAS,eAAe,EACjC,CAAC"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// Governed GitHub pull request routes (Issue #477, Epic #470, ADR-0064).
|
|
2
|
+
//
|
|
3
|
+
// * POST /api/git-delivery/pr/preview — READ-ONLY. Builds the pre-create context: the synthesized,
|
|
4
|
+
// user-editable metadata draft (title/body/risk narrative), the readiness summary (objectExists vs
|
|
5
|
+
// reviewReady) with structured blockers, the draft-vs-ready recommendation, the reviewer/label/
|
|
6
|
+
// linkage suggestions, and the effective policy decision. Never mutates, never records evidence.
|
|
7
|
+
// * POST /api/git-delivery/pr/execute — Governed. Drives the #477 PR gateway end-to-end through
|
|
8
|
+
// executeGovernedPullRequest (preflight + policy + approval + the dedicated `gh api` adapter) and
|
|
9
|
+
// appends content-free evidence for the allowed AND blocked outcome alike. Returns the typed
|
|
10
|
+
// provider-rejection reason + reused recovery hint so a rejected operation recovers without
|
|
11
|
+
// guessing, and the provider-assigned PR number on a successful create.
|
|
12
|
+
//
|
|
13
|
+
// Content-free in evidence: title/body strings flow to the provider but only their byte lengths enter
|
|
14
|
+
// the ledger. CSRF + JSON content type are enforced centrally by server.ts.
|
|
15
|
+
import { isGitDeliveryApprovalRequirement, } from "@oscharko-dev/keiko-contracts";
|
|
16
|
+
import { readWorktreeSnapshotFor, resolveProjectWorkspace } from "./execution.js";
|
|
17
|
+
import { buildGitDeliveryPrPreview, executeGovernedPullRequest, gitDeliveryPrExecuteResponse, KEIKO_DEFAULT_PR_POLICY_PACK, } from "./prExecution.js";
|
|
18
|
+
import { GitDeliveryBodyTooLargeError, hasOnlyAllowedKeys, isNonEmptyString, isPlainObject, readGitDeliveryBody, scanForbiddenStrings, scanUnsafeFormatChars, } from "./requestGuards.js";
|
|
19
|
+
const SAFE_MESSAGES = {
|
|
20
|
+
GIT_DELIVERY_PR_BAD_REQUEST: "The request body is not a valid governed pull request.",
|
|
21
|
+
GIT_DELIVERY_PR_PAYLOAD_TOO_LARGE: "The governed pull request request exceeds the maximum size.",
|
|
22
|
+
GIT_DELIVERY_PR_FORBIDDEN_PAYLOAD: "The request contained a forbidden field. Requests may not carry credentials or auth headers.",
|
|
23
|
+
GIT_DELIVERY_PR_UNKNOWN_PROJECT: "The requested project is not a known workspace.",
|
|
24
|
+
GIT_DELIVERY_PR_WORKTREE_UNAVAILABLE: "The repository worktree could not be inspected. Confirm the project is a Git repository.",
|
|
25
|
+
};
|
|
26
|
+
const errResult = (status, code) => ({
|
|
27
|
+
status,
|
|
28
|
+
body: { error: { code, message: SAFE_MESSAGES[code] } },
|
|
29
|
+
});
|
|
30
|
+
async function readParsed(req) {
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = await readGitDeliveryBody(req);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const result = error instanceof GitDeliveryBodyTooLargeError
|
|
37
|
+
? errResult(413, "GIT_DELIVERY_PR_PAYLOAD_TOO_LARGE")
|
|
38
|
+
: errResult(400, "GIT_DELIVERY_PR_BAD_REQUEST");
|
|
39
|
+
return { ok: false, result };
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { ok: false, result: errResult(400, "GIT_DELIVERY_PR_BAD_REQUEST") };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// A git ref operand: non-empty, no whitespace, no leading "-" (flag-injection guard), no NUL/control.
|
|
49
|
+
// eslint-disable-next-line no-control-regex -- intentionally matches control chars to REJECT them
|
|
50
|
+
const REF_CONTROL_CHAR = new RegExp("[\u0000-\u001f\u007f]");
|
|
51
|
+
function isSafeGitRef(value) {
|
|
52
|
+
if (typeof value !== "string" || value.length === 0)
|
|
53
|
+
return false;
|
|
54
|
+
if (/\s/.test(value))
|
|
55
|
+
return false;
|
|
56
|
+
if (value.startsWith("-"))
|
|
57
|
+
return false;
|
|
58
|
+
if (REF_CONTROL_CHAR.test(value))
|
|
59
|
+
return false;
|
|
60
|
+
if (value.includes(":"))
|
|
61
|
+
return false;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
const OWNER_REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
65
|
+
const PR_NUMBER_RE = /^[1-9][0-9]{0,9}$/;
|
|
66
|
+
function isOwnerAndRepo(value) {
|
|
67
|
+
return typeof value === "string" && OWNER_REPO_RE.test(value);
|
|
68
|
+
}
|
|
69
|
+
const ALLOWED_KEYS = new Set([
|
|
70
|
+
"schemaVersion",
|
|
71
|
+
"projectId",
|
|
72
|
+
"kind",
|
|
73
|
+
"ownerAndRepo",
|
|
74
|
+
"headBranchName",
|
|
75
|
+
"baseBranchName",
|
|
76
|
+
"title",
|
|
77
|
+
"body",
|
|
78
|
+
"isDraft",
|
|
79
|
+
"prExternalId",
|
|
80
|
+
"convertToDraft",
|
|
81
|
+
"convertFromDraft",
|
|
82
|
+
"approval",
|
|
83
|
+
]);
|
|
84
|
+
const NO_APPROVAL = { required: false };
|
|
85
|
+
function parseApproval(value) {
|
|
86
|
+
if (value === undefined)
|
|
87
|
+
return NO_APPROVAL;
|
|
88
|
+
return isGitDeliveryApprovalRequirement(value) ? value : undefined;
|
|
89
|
+
}
|
|
90
|
+
function optionalBool(value) {
|
|
91
|
+
if (value === undefined)
|
|
92
|
+
return false;
|
|
93
|
+
return typeof value === "boolean" ? value : undefined;
|
|
94
|
+
}
|
|
95
|
+
function isBodyString(value) {
|
|
96
|
+
// A PR body may be empty; it must still be a string.
|
|
97
|
+
return typeof value === "string";
|
|
98
|
+
}
|
|
99
|
+
function scanError(parsed) {
|
|
100
|
+
if (scanForbiddenStrings(parsed)) {
|
|
101
|
+
return errResult(400, "GIT_DELIVERY_PR_FORBIDDEN_PAYLOAD");
|
|
102
|
+
}
|
|
103
|
+
if (scanUnsafeFormatChars(parsed)) {
|
|
104
|
+
return errResult(400, "GIT_DELIVERY_PR_BAD_REQUEST");
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
function buildCreateCommand(parsed) {
|
|
109
|
+
if (!isOwnerAndRepo(parsed.ownerAndRepo) ||
|
|
110
|
+
!isSafeGitRef(parsed.headBranchName) ||
|
|
111
|
+
!isSafeGitRef(parsed.baseBranchName) ||
|
|
112
|
+
!isNonEmptyString(parsed.title) ||
|
|
113
|
+
!isBodyString(parsed.body)) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const isDraft = optionalBool(parsed.isDraft);
|
|
117
|
+
if (isDraft === undefined)
|
|
118
|
+
return undefined;
|
|
119
|
+
return {
|
|
120
|
+
kind: "pr-create",
|
|
121
|
+
ownerAndRepo: parsed.ownerAndRepo,
|
|
122
|
+
headBranchName: parsed.headBranchName,
|
|
123
|
+
baseBranchName: parsed.baseBranchName,
|
|
124
|
+
title: parsed.title,
|
|
125
|
+
body: parsed.body,
|
|
126
|
+
isDraft,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function isPrNumberString(value) {
|
|
130
|
+
return typeof value === "string" && PR_NUMBER_RE.test(value);
|
|
131
|
+
}
|
|
132
|
+
function hasValidUpdateFields(parsed) {
|
|
133
|
+
return (isOwnerAndRepo(parsed.ownerAndRepo) &&
|
|
134
|
+
isPrNumberString(parsed.prExternalId) &&
|
|
135
|
+
isSafeGitRef(parsed.headBranchName) &&
|
|
136
|
+
isSafeGitRef(parsed.baseBranchName) &&
|
|
137
|
+
isNonEmptyString(parsed.title) &&
|
|
138
|
+
isBodyString(parsed.body));
|
|
139
|
+
}
|
|
140
|
+
// Exactly one of convert-to-draft / convert-from-draft may be set; both default to false.
|
|
141
|
+
function parseConvertFlags(parsed) {
|
|
142
|
+
const convertToDraft = optionalBool(parsed.convertToDraft);
|
|
143
|
+
const convertFromDraft = optionalBool(parsed.convertFromDraft);
|
|
144
|
+
if (convertToDraft === undefined || convertFromDraft === undefined)
|
|
145
|
+
return undefined;
|
|
146
|
+
if (convertToDraft && convertFromDraft)
|
|
147
|
+
return undefined;
|
|
148
|
+
return { convertToDraft, convertFromDraft };
|
|
149
|
+
}
|
|
150
|
+
function buildUpdateCommand(parsed) {
|
|
151
|
+
if (!hasValidUpdateFields(parsed))
|
|
152
|
+
return undefined;
|
|
153
|
+
const converts = parseConvertFlags(parsed);
|
|
154
|
+
if (converts === undefined)
|
|
155
|
+
return undefined;
|
|
156
|
+
return {
|
|
157
|
+
kind: "pr-update",
|
|
158
|
+
ownerAndRepo: parsed.ownerAndRepo,
|
|
159
|
+
prExternalId: parsed.prExternalId,
|
|
160
|
+
headBranchName: parsed.headBranchName,
|
|
161
|
+
baseBranchName: parsed.baseBranchName,
|
|
162
|
+
title: parsed.title,
|
|
163
|
+
body: parsed.body,
|
|
164
|
+
convertToDraft: converts.convertToDraft,
|
|
165
|
+
convertFromDraft: converts.convertFromDraft,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function buildPrCommand(parsed) {
|
|
169
|
+
if (parsed.kind === "pr-create")
|
|
170
|
+
return buildCreateCommand(parsed);
|
|
171
|
+
if (parsed.kind === "pr-update")
|
|
172
|
+
return buildUpdateCommand(parsed);
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
function validate(parsed) {
|
|
176
|
+
const bad = { kind: "err", result: errResult(400, "GIT_DELIVERY_PR_BAD_REQUEST") };
|
|
177
|
+
if (!isPlainObject(parsed) || !hasOnlyAllowedKeys(parsed, ALLOWED_KEYS))
|
|
178
|
+
return bad;
|
|
179
|
+
if (parsed.schemaVersion !== "1" || !isNonEmptyString(parsed.projectId))
|
|
180
|
+
return bad;
|
|
181
|
+
const scanErr = scanError(parsed);
|
|
182
|
+
if (scanErr !== undefined)
|
|
183
|
+
return { kind: "err", result: scanErr };
|
|
184
|
+
const command = buildPrCommand(parsed);
|
|
185
|
+
const approval = parseApproval(parsed.approval);
|
|
186
|
+
if (command === undefined || approval === undefined)
|
|
187
|
+
return bad;
|
|
188
|
+
return { kind: "ok", value: { projectId: parsed.projectId, command, approval } };
|
|
189
|
+
}
|
|
190
|
+
// ─── Preview handler (read-only) ────────────────────────────────────────────────────────────────
|
|
191
|
+
export const createHandlePrPreview = (options = {}) => {
|
|
192
|
+
const seams = options.execution ?? {};
|
|
193
|
+
const now = () => (seams.now ?? Date.now)();
|
|
194
|
+
return async (ctx, deps) => {
|
|
195
|
+
const read = await readParsed(ctx.req);
|
|
196
|
+
if (!read.ok)
|
|
197
|
+
return read.result;
|
|
198
|
+
const validation = validate(read.value);
|
|
199
|
+
if (validation.kind === "err")
|
|
200
|
+
return validation.result;
|
|
201
|
+
const { projectId, command } = validation.value;
|
|
202
|
+
const workspace = resolveProjectWorkspace(deps, projectId);
|
|
203
|
+
if (workspace === undefined)
|
|
204
|
+
return errResult(404, "GIT_DELIVERY_PR_UNKNOWN_PROJECT");
|
|
205
|
+
const packs = seams.policyPacks ?? { repoPack: KEIKO_DEFAULT_PR_POLICY_PACK };
|
|
206
|
+
try {
|
|
207
|
+
const snapshot = await readWorktreeSnapshotFor(workspace, seams, now);
|
|
208
|
+
return {
|
|
209
|
+
status: 200,
|
|
210
|
+
body: deps.redactor(buildGitDeliveryPrPreview(command, snapshot, packs)),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return errResult(409, "GIT_DELIVERY_PR_WORKTREE_UNAVAILABLE");
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
// ─── Execute handler (governed) ───────────────────────────────────────────────────────────────
|
|
219
|
+
export const createHandlePrExecute = (options = {}) => {
|
|
220
|
+
const seams = options.execution ?? {};
|
|
221
|
+
return async (ctx, deps) => {
|
|
222
|
+
const read = await readParsed(ctx.req);
|
|
223
|
+
if (!read.ok)
|
|
224
|
+
return read.result;
|
|
225
|
+
const validation = validate(read.value);
|
|
226
|
+
if (validation.kind === "err")
|
|
227
|
+
return validation.result;
|
|
228
|
+
const { projectId, command, approval } = validation.value;
|
|
229
|
+
const workspace = resolveProjectWorkspace(deps, projectId);
|
|
230
|
+
if (workspace === undefined)
|
|
231
|
+
return errResult(404, "GIT_DELIVERY_PR_UNKNOWN_PROJECT");
|
|
232
|
+
let result;
|
|
233
|
+
try {
|
|
234
|
+
result = await executeGovernedPullRequest(command, approval, workspace, deps, seams);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Only the read-only snapshot step can throw (not a git repository); the gateway never throws.
|
|
238
|
+
return errResult(409, "GIT_DELIVERY_PR_WORKTREE_UNAVAILABLE");
|
|
239
|
+
}
|
|
240
|
+
return { status: 200, body: deps.redactor(gitDeliveryPrExecuteResponse(result)) };
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
// ─── Route group ───────────────────────────────────────────────────────────────────────────────
|
|
244
|
+
export const createGitDeliveryPrRouteGroup = (options = {}) => [
|
|
245
|
+
{
|
|
246
|
+
method: "POST",
|
|
247
|
+
pattern: "/api/git-delivery/pr/preview",
|
|
248
|
+
handler: createHandlePrPreview(options),
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
method: "POST",
|
|
252
|
+
pattern: "/api/git-delivery/pr/execute",
|
|
253
|
+
handler: createHandlePrExecute(options),
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
export const GIT_DELIVERY_PR_ROUTE_GROUP = createGitDeliveryPrRouteGroup();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { WorkspaceInfo } from "@oscharko-dev/keiko-workspace";
|
|
2
|
+
import { type GitDeliveryApprovalRequirement, type GitDeliveryRepoPolicyPack, type GitDeliveryRiskClass } from "@oscharko-dev/keiko-contracts";
|
|
3
|
+
import { type GitPublishLifecycleResult, type GitPushCommand, type GitRemotePublishAdapter, type GitWorktreeSnapshot } from "@oscharko-dev/keiko-tools";
|
|
4
|
+
import type { UiHandlerDeps } from "../deps.js";
|
|
5
|
+
import type { GitDeliveryTrustedPolicyPacks } from "./actionSheetProjection.js";
|
|
6
|
+
import { type GitDeliveryMutationResponseBody } from "./execution.js";
|
|
7
|
+
export declare const KEIKO_DEFAULT_PUBLISH_POLICY_PACK: GitDeliveryRepoPolicyPack;
|
|
8
|
+
export interface GitDeliveryPublishSeams {
|
|
9
|
+
readonly publishAdapterFactory?: ((workspace: WorkspaceInfo) => GitRemotePublishAdapter) | undefined;
|
|
10
|
+
readonly snapshotReader?: ((workspace: WorkspaceInfo) => Promise<GitWorktreeSnapshot>) | undefined;
|
|
11
|
+
readonly policyPacks?: GitDeliveryTrustedPolicyPacks | undefined;
|
|
12
|
+
readonly now?: (() => number) | undefined;
|
|
13
|
+
readonly newActionId?: (() => string) | undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Runs ONE governed publish end-to-end: live snapshot → publish gateway (preflight + policy + approval +
|
|
17
|
+
* execute) → evidence. Returns the gateway lifecycle result; the caller projects it into a content-free
|
|
18
|
+
* HTTP body. Evidence is appended best-effort BEFORE the caller responds, for allowed AND blocked
|
|
19
|
+
* attempts.
|
|
20
|
+
*/
|
|
21
|
+
export declare function executeGovernedPublish(command: GitPushCommand, approval: GitDeliveryApprovalRequirement, workspace: WorkspaceInfo, deps: Pick<UiHandlerDeps, "evidenceStore" | "redactor">, seams: GitDeliveryPublishSeams): Promise<GitPublishLifecycleResult>;
|
|
22
|
+
export interface GitDeliveryPushPreviewBody {
|
|
23
|
+
readonly schemaVersion: "1";
|
|
24
|
+
readonly remoteAlias: string;
|
|
25
|
+
readonly remoteBranchName: string;
|
|
26
|
+
readonly sourceBranchName: string;
|
|
27
|
+
readonly riskClass: GitDeliveryRiskClass;
|
|
28
|
+
readonly wouldCreateRemoteBranch: boolean;
|
|
29
|
+
readonly wouldTriggerChecks: boolean;
|
|
30
|
+
readonly forceBlocked: boolean;
|
|
31
|
+
readonly preflightBlockingCodes: readonly string[];
|
|
32
|
+
readonly preflightAdvisoryCodes: readonly string[];
|
|
33
|
+
readonly policyOutcome: string;
|
|
34
|
+
readonly policyBlockReason?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function buildGitDeliveryPushPreview(command: GitPushCommand, snapshot: GitWorktreeSnapshot, packs: GitDeliveryTrustedPolicyPacks): GitDeliveryPushPreviewBody;
|
|
37
|
+
export interface GitDeliveryPushExecuteResponseBody extends GitDeliveryMutationResponseBody {
|
|
38
|
+
readonly publishRejectionReason?: string;
|
|
39
|
+
readonly recoveryDisposition?: string;
|
|
40
|
+
readonly recoveryActionHint?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function gitDeliveryPublishExecuteResponse(result: GitPublishLifecycleResult): GitDeliveryPushExecuteResponseBody;
|
|
43
|
+
//# sourceMappingURL=pushExecution.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pushExecution.d.ts","sourceRoot":"","sources":["../../src/gitDelivery/pushExecution.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAIL,KAAK,8BAA8B,EAEnC,KAAK,yBAAyB,EAC9B,KAAK,oBAAoB,EAC1B,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAIL,KAAK,yBAAyB,EAC9B,KAAK,cAAc,EACnB,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AAChF,OAAO,EAKL,KAAK,+BAA+B,EACrC,MAAM,gBAAgB,CAAC;AAQxB,eAAO,MAAM,iCAAiC,EAAE,yBAuB/C,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,qBAAqB,CAAC,EAC3B,CAAC,CAAC,SAAS,EAAE,aAAa,KAAK,uBAAuB,CAAC,GACvD,SAAS,CAAC;IACd,QAAQ,CAAC,cAAc,CAAC,EACpB,CAAC,CAAC,SAAS,EAAE,aAAa,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC,GAC5D,SAAS,CAAC;IACd,QAAQ,CAAC,WAAW,CAAC,EAAE,6BAA6B,GAAG,SAAS,CAAC;IACjE,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;CACnD;AAsBD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,8BAA8B,EACxC,SAAS,EAAE,aAAa,EACxB,IAAI,EAAE,IAAI,CAAC,aAAa,EAAE,eAAe,GAAG,UAAU,CAAC,EACvD,KAAK,EAAE,uBAAuB,GAC7B,OAAO,CAAC,yBAAyB,CAAC,CAoBpC;AAID,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,aAAa,EAAE,GAAG,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,oBAAoB,CAAC;IACzC,QAAQ,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC1C,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IAGrC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,sBAAsB,EAAE,SAAS,MAAM,EAAE,CAAC;IACnD,QAAQ,CAAC,sBAAsB,EAAE,SAAS,MAAM,EAAE,CAAC;IACnD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AAKD,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,6BAA6B,GACnC,0BAA0B,CA8B5B;AAID,MAAM,WAAW,kCAAmC,SAAQ,+BAA+B;IAGzF,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IACzC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC;AAED,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,yBAAyB,GAChC,kCAAkC,CAapC"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Governed remote publish execution core for the #476 push routes (Epic #470, ADR-0063).
|
|
2
|
+
//
|
|
3
|
+
// The push preview + execute routes share ONE path: resolve and authorize the project workspace, build
|
|
4
|
+
// a TRUSTWORTHY snapshot from the live worktree, drive the #476 publish gateway `runGitPublish` (the
|
|
5
|
+
// SEPARATE remote execution authority — preflight + policy + approval gates, executing through a narrow
|
|
6
|
+
// remote adapter with a dedicated push-only allowlist), and append a content-free evidence record through
|
|
7
|
+
// the #474 ledger for EVERY terminal outcome (allowed and blocked alike — AC5). No second orchestrator,
|
|
8
|
+
// no generic shell, no widening of the local mutation or read-only inspection allowlists. The Node push
|
|
9
|
+
// effect is injected via seams so route tests run deterministically against a fake remote.
|
|
10
|
+
import { GIT_DELIVERY_POLICY_SCHEMA_VERSION, evaluateGitPolicy, gitDeliveryRiskClassForInputs, } from "@oscharko-dev/keiko-contracts";
|
|
11
|
+
import { evaluateGitPreflight, evaluateGitPublishEffectivePolicy, runGitPublish, } from "@oscharko-dev/keiko-tools";
|
|
12
|
+
import { createNodeGitPublishAdapter } from "@oscharko-dev/keiko-tools/internal/git-mutation";
|
|
13
|
+
import { defaultGitDeliveryActionId, gitDeliveryMutationResponse, persistGitDeliveryEvidence, readWorktreeSnapshotFor, } from "./execution.js";
|
|
14
|
+
// Default trusted publish policy: PERMIT `push` only to safe, non-protected branch namespaces and only
|
|
15
|
+
// within the `publish` risk ceiling (which fail-closed BLOCKS force pushes, classed recovery-or-rewrite).
|
|
16
|
+
// A push whose REMOTE target is a shared/protected branch (dev, main, release/*, …) falls the branch-
|
|
17
|
+
// pattern constraint and is blocked with `policy-pack-blocked` — stricter than an ordinary user branch
|
|
18
|
+
// (AC2). Every other action kind is denied. Applies only when governed git delivery is ENABLED and no
|
|
19
|
+
// stricter pack is configured; the decision is still EVALUATED for every push, so governance is preserved.
|
|
20
|
+
export const KEIKO_DEFAULT_PUBLISH_POLICY_PACK = {
|
|
21
|
+
schemaVersion: GIT_DELIVERY_POLICY_SCHEMA_VERSION,
|
|
22
|
+
repoId: "keiko-publish-default",
|
|
23
|
+
rules: [
|
|
24
|
+
{
|
|
25
|
+
actionKind: "push",
|
|
26
|
+
decision: "constrained",
|
|
27
|
+
constraints: [
|
|
28
|
+
{ kind: "risk-class-ceiling", maxRiskClass: "publish" },
|
|
29
|
+
{
|
|
30
|
+
kind: "branch-pattern",
|
|
31
|
+
patterns: [
|
|
32
|
+
{ matchKind: "prefix", value: "claude/" },
|
|
33
|
+
{ matchKind: "prefix", value: "feat/" },
|
|
34
|
+
{ matchKind: "prefix", value: "fix/" },
|
|
35
|
+
{ matchKind: "prefix", value: "chore/" },
|
|
36
|
+
{ matchKind: "prefix", value: "docs/" },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
defaultRule: { decision: "blocked" },
|
|
43
|
+
};
|
|
44
|
+
function publishAdapterFor(workspace, seams, now) {
|
|
45
|
+
if (seams.publishAdapterFactory !== undefined)
|
|
46
|
+
return seams.publishAdapterFactory(workspace);
|
|
47
|
+
return createNodeGitPublishAdapter({ workspace, processEnv: process.env, now });
|
|
48
|
+
}
|
|
49
|
+
function pushInputsOf(command) {
|
|
50
|
+
return {
|
|
51
|
+
kind: "push",
|
|
52
|
+
sourceBranchName: command.sourceBranchName,
|
|
53
|
+
remoteAlias: command.remoteAlias,
|
|
54
|
+
remoteBranchName: command.remoteBranchName,
|
|
55
|
+
forcePush: command.forcePush,
|
|
56
|
+
setUpstreamTracking: command.setUpstreamTracking,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Runs ONE governed publish end-to-end: live snapshot → publish gateway (preflight + policy + approval +
|
|
61
|
+
* execute) → evidence. Returns the gateway lifecycle result; the caller projects it into a content-free
|
|
62
|
+
* HTTP body. Evidence is appended best-effort BEFORE the caller responds, for allowed AND blocked
|
|
63
|
+
* attempts.
|
|
64
|
+
*/
|
|
65
|
+
export async function executeGovernedPublish(command, approval, workspace, deps, seams) {
|
|
66
|
+
const now = seams.now ?? Date.now;
|
|
67
|
+
const snapshot = await readWorktreeSnapshotFor(workspace, seams, now);
|
|
68
|
+
const adapter = publishAdapterFor(workspace, seams, now);
|
|
69
|
+
const packs = seams.policyPacks ?? { repoPack: KEIKO_DEFAULT_PUBLISH_POLICY_PACK };
|
|
70
|
+
const newActionId = seams.newActionId ?? (() => defaultGitDeliveryActionId(command, now()));
|
|
71
|
+
const result = await runGitPublish({ command, approval }, {
|
|
72
|
+
adapter,
|
|
73
|
+
snapshot,
|
|
74
|
+
...(packs.orgPack !== undefined ? { orgPolicyPack: packs.orgPack } : {}),
|
|
75
|
+
...(packs.repoPack !== undefined ? { repoPolicyPack: packs.repoPack } : {}),
|
|
76
|
+
now,
|
|
77
|
+
newActionId,
|
|
78
|
+
});
|
|
79
|
+
persistGitDeliveryEvidence(deps, result.lifecycle, snapshot, workspace.root, now);
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
// Builds the read-only push preview from a live snapshot: the remote target, the risk summary, the
|
|
83
|
+
// preflight findings (incl. non-fast-forward and missing-upstream), and the policy decision — no
|
|
84
|
+
// execution, no evidence.
|
|
85
|
+
export function buildGitDeliveryPushPreview(command, snapshot, packs) {
|
|
86
|
+
const inputs = pushInputsOf(command);
|
|
87
|
+
const preflight = evaluateGitPreflight(inputs, snapshot);
|
|
88
|
+
const decision = evaluateGitPolicy(packs.orgPack, packs.repoPack, {
|
|
89
|
+
actionKind: "push",
|
|
90
|
+
targetBranchName: command.remoteBranchName,
|
|
91
|
+
activeProviderCapabilities: [],
|
|
92
|
+
});
|
|
93
|
+
// The EFFECTIVE policy outcome for THIS target, so the preview predicts the execute outcome exactly:
|
|
94
|
+
// a constrained decision is resolved against the target (a protected/shared branch reads as blocked).
|
|
95
|
+
const effective = evaluateGitPublishEffectivePolicy(decision, command.remoteBranchName, [], inputs);
|
|
96
|
+
return {
|
|
97
|
+
schemaVersion: "1",
|
|
98
|
+
remoteAlias: command.remoteAlias,
|
|
99
|
+
remoteBranchName: command.remoteBranchName,
|
|
100
|
+
sourceBranchName: command.sourceBranchName,
|
|
101
|
+
riskClass: gitDeliveryRiskClassForInputs(inputs),
|
|
102
|
+
wouldCreateRemoteBranch: command.setUpstreamTracking && !snapshot.hasUpstream,
|
|
103
|
+
wouldTriggerChecks: true,
|
|
104
|
+
forceBlocked: command.forcePush,
|
|
105
|
+
preflightBlockingCodes: preflight.blocking.map((f) => f.code),
|
|
106
|
+
preflightAdvisoryCodes: preflight.advisory.map((f) => f.code),
|
|
107
|
+
policyOutcome: effective.outcome,
|
|
108
|
+
...(effective.blockReason !== undefined ? { policyBlockReason: effective.blockReason } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function gitDeliveryPublishExecuteResponse(result) {
|
|
112
|
+
const base = gitDeliveryMutationResponse(result.lifecycle);
|
|
113
|
+
if (result.rejection === undefined) {
|
|
114
|
+
return base;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
...base,
|
|
118
|
+
publishRejectionReason: result.rejection.reason,
|
|
119
|
+
recoveryDisposition: result.rejection.disposition,
|
|
120
|
+
...(result.rejection.actionHint !== undefined
|
|
121
|
+
? { recoveryActionHint: result.rejection.actionHint }
|
|
122
|
+
: {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RouteContext, RouteDefinition, RouteResult } from "../routes.js";
|
|
2
|
+
import type { UiHandlerDeps } from "../deps.js";
|
|
3
|
+
import { type GitDeliveryPublishSeams } from "./pushExecution.js";
|
|
4
|
+
export type GitDeliveryPushErrorCode = "GIT_DELIVERY_PUSH_BAD_REQUEST" | "GIT_DELIVERY_PUSH_PAYLOAD_TOO_LARGE" | "GIT_DELIVERY_PUSH_FORBIDDEN_PAYLOAD" | "GIT_DELIVERY_PUSH_UNKNOWN_PROJECT" | "GIT_DELIVERY_PUSH_WORKTREE_UNAVAILABLE";
|
|
5
|
+
export interface GitDeliveryPushRouteOptions {
|
|
6
|
+
readonly execution?: GitDeliveryPublishSeams;
|
|
7
|
+
}
|
|
8
|
+
export declare const createHandlePushPreview: (options?: GitDeliveryPushRouteOptions) => ((ctx: RouteContext, deps: UiHandlerDeps) => Promise<RouteResult>);
|
|
9
|
+
export declare const createHandlePushExecute: (options?: GitDeliveryPushRouteOptions) => ((ctx: RouteContext, deps: UiHandlerDeps) => Promise<RouteResult>);
|
|
10
|
+
export declare const createGitDeliveryPushRouteGroup: (options?: GitDeliveryPushRouteOptions) => readonly RouteDefinition[];
|
|
11
|
+
export declare const GIT_DELIVERY_PUSH_ROUTE_GROUP: readonly RouteDefinition[];
|
|
12
|
+
//# sourceMappingURL=pushRoutes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pushRoutes.d.ts","sourceRoot":"","sources":["../../src/gitDelivery/pushRoutes.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,OAAO,EAKL,KAAK,uBAAuB,EAC7B,MAAM,oBAAoB,CAAC;AAa5B,MAAM,MAAM,wBAAwB,GAChC,+BAA+B,GAC/B,qCAAqC,GACrC,qCAAqC,GACrC,mCAAmC,GACnC,wCAAwC,CAAC;AAmB7C,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,uBAAuB,CAAC;CAC9C;AAyHD,eAAO,MAAM,uBAAuB,GAClC,UAAS,2BAAgC,KACxC,CAAC,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,WAAW,CAAC,CAsBnE,CAAC;AAIF,eAAO,MAAM,uBAAuB,GAClC,UAAS,2BAAgC,KACxC,CAAC,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,WAAW,CAAC,CAmBnE,CAAC;AAIF,eAAO,MAAM,+BAA+B,GAC1C,UAAS,2BAAgC,KACxC,SAAS,eAAe,EAW1B,CAAC;AAEF,eAAO,MAAM,6BAA6B,EAAE,SAAS,eAAe,EACjC,CAAC"}
|