@oscharko-dev/keiko-tools 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/git-commit-intent-node.d.ts +7 -0
  3. package/dist/git-commit-intent-node.d.ts.map +1 -0
  4. package/dist/git-commit-intent-node.js +42 -0
  5. package/dist/git-merge-gateway.d.ts +95 -0
  6. package/dist/git-merge-gateway.d.ts.map +1 -0
  7. package/dist/git-merge-gateway.js +536 -0
  8. package/dist/git-merge-node.d.ts +17 -0
  9. package/dist/git-merge-node.d.ts.map +1 -0
  10. package/dist/git-merge-node.js +243 -0
  11. package/dist/git-mutation-adapter.d.ts +51 -0
  12. package/dist/git-mutation-adapter.d.ts.map +1 -0
  13. package/dist/git-mutation-adapter.js +207 -0
  14. package/dist/git-mutation-evidence.d.ts +23 -0
  15. package/dist/git-mutation-evidence.d.ts.map +1 -0
  16. package/dist/git-mutation-evidence.js +283 -0
  17. package/dist/git-mutation-node.d.ts +22 -0
  18. package/dist/git-mutation-node.d.ts.map +1 -0
  19. package/dist/git-mutation-node.js +145 -0
  20. package/dist/git-mutation-orchestrator.d.ts +92 -0
  21. package/dist/git-mutation-orchestrator.d.ts.map +1 -0
  22. package/dist/git-mutation-orchestrator.js +321 -0
  23. package/dist/git-mutation-preflight.d.ts +35 -0
  24. package/dist/git-mutation-preflight.d.ts.map +1 -0
  25. package/dist/git-mutation-preflight.js +226 -0
  26. package/dist/git-mutation-taxonomy.d.ts +15 -0
  27. package/dist/git-mutation-taxonomy.d.ts.map +1 -0
  28. package/dist/git-mutation-taxonomy.js +102 -0
  29. package/dist/git-pr-gateway.d.ts +101 -0
  30. package/dist/git-pr-gateway.d.ts.map +1 -0
  31. package/dist/git-pr-gateway.js +487 -0
  32. package/dist/git-pr-node.d.ts +17 -0
  33. package/dist/git-pr-node.d.ts.map +1 -0
  34. package/dist/git-pr-node.js +173 -0
  35. package/dist/git-publish-gateway.d.ts +72 -0
  36. package/dist/git-publish-gateway.d.ts.map +1 -0
  37. package/dist/git-publish-gateway.js +423 -0
  38. package/dist/git-publish-node.d.ts +17 -0
  39. package/dist/git-publish-node.d.ts.map +1 -0
  40. package/dist/git-publish-node.js +107 -0
  41. package/dist/git-worktree-adapter.d.ts +78 -0
  42. package/dist/git-worktree-adapter.d.ts.map +1 -0
  43. package/dist/git-worktree-adapter.js +300 -0
  44. package/dist/git-worktree-snapshot-node.d.ts +31 -0
  45. package/dist/git-worktree-snapshot-node.d.ts.map +1 -0
  46. package/dist/git-worktree-snapshot-node.js +189 -0
  47. package/dist/index.d.ts +9 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +37 -0
  50. package/package.json +9 -5
@@ -0,0 +1,243 @@
1
+ // Node implementation of the narrow governed merge adapter (Issue #478, Epic #470) — AC1/AC4/AC5.
2
+ //
3
+ // This is the ONLY place a governed merge operation actually executes. It builds the governed `gh api`
4
+ // argv from the pure builders (git-merge-gateway.ts) and runs them through the SAME keiko-tools no-shell
5
+ // spawn boundary (`runCommand`, exec.ts) with a DEDICATED merge allowlist (`GIT_MERGE_COMMAND_RULES`)
6
+ // that permits only `gh api` and denies file-input flags. There is no method that accepts an arbitrary
7
+ // command string and no parallel child_process path: the deny-by-default allowlist, env isolation,
8
+ // redaction, and cancellation of the shared boundary apply to the merge operation exactly as to every
9
+ // other tool.
10
+ //
11
+ // `gh` reads its own GitHub token from its keyring or from GH_TOKEN/GITHUB_TOKEN in the inherited
12
+ // process environment; Keiko never reads or handles the token value. The readiness READ maps GitHub's
13
+ // content-free PR facts (state / draft / mergeable_state) and repo merge configuration into the
14
+ // provider-neutral contract interfaces; a non-OK merge status is classified into a typed
15
+ // GitMergeRejectionReason. Raw stdout/stderr never leave this module — only the typed facts, the
16
+ // content-free contract error code, and the typed rejection reason cross out.
17
+ //
18
+ // Lives on the `./internal/git-mutation` subpath (re-exported by git-mutation-node.ts) because it carries
19
+ // the Node execution effect; the pure port, builders, and rules it implements are on the barrel.
20
+ import { GIT_DELIVERY_SCHEMA_VERSION, } from "@oscharko-dev/keiko-contracts";
21
+ import { buildDeleteMergedBranchArgv, buildHeadStatusArgv, buildMergeArgv, buildMergeReadinessArgv, buildRepoMergeConfigArgv, classifyGitMergeRejection, GIT_MERGE_COMMAND_RULES, gitMergeRejectionToErrorCode, mapRawMergeReadiness, } from "./git-merge-gateway.js";
22
+ import { CommandCancelledError, CommandTimeoutError } from "./errors.js";
23
+ import { nodeSpawnFn, runCommand, } from "./exec.js";
24
+ import { DEFAULT_SANDBOX_POLICY } from "./types.js";
25
+ function executionResult(outcome, durationMs, extra) {
26
+ return {
27
+ schemaVersion: GIT_DELIVERY_SCHEMA_VERSION,
28
+ outcome,
29
+ durationMs: Math.max(0, Math.trunc(durationMs)),
30
+ ...extra,
31
+ };
32
+ }
33
+ function buildRunContext(deps) {
34
+ return {
35
+ runDeps: {
36
+ workspace: deps.workspace,
37
+ policy: deps.policy ?? DEFAULT_SANDBOX_POLICY,
38
+ commandRules: GIT_MERGE_COMMAND_RULES,
39
+ spawn: deps.spawn ?? nodeSpawnFn,
40
+ processEnv: deps.processEnv ?? process.env,
41
+ now: deps.now ?? Date.now,
42
+ ...(deps.resolveExecutable !== undefined
43
+ ? { resolveExecutable: deps.resolveExecutable }
44
+ : {}),
45
+ ...(deps.home !== undefined ? { home: deps.home } : {}),
46
+ },
47
+ signal: deps.signal ?? new AbortController().signal,
48
+ timeoutMs: deps.timeoutMs,
49
+ };
50
+ }
51
+ function rejectionFromExit(result) {
52
+ if (result.timedOut) {
53
+ return executionResult("failed", result.durationMs, {
54
+ errorCode: "timeout",
55
+ rejectionReason: "provider-unavailable",
56
+ });
57
+ }
58
+ const reason = classifyGitMergeRejection(`${result.stdout}\n${result.stderr}`);
59
+ return executionResult("failed", result.durationMs, {
60
+ errorCode: gitMergeRejectionToErrorCode(reason),
61
+ rejectionReason: reason,
62
+ });
63
+ }
64
+ function failureFromThrow(error, durationMs) {
65
+ if (error instanceof CommandTimeoutError) {
66
+ return executionResult("failed", durationMs, {
67
+ errorCode: "timeout",
68
+ rejectionReason: "provider-unavailable",
69
+ });
70
+ }
71
+ if (error instanceof CommandCancelledError) {
72
+ return executionResult("aborted", durationMs);
73
+ }
74
+ return executionResult("failed", durationMs, { errorCode: "internal-error" });
75
+ }
76
+ async function runGh(ctx, argv) {
77
+ try {
78
+ return await runCommand({ command: "gh", args: argv, cwd: undefined, timeoutMs: ctx.timeoutMs, signal: ctx.signal }, ctx.runDeps);
79
+ }
80
+ catch (error) {
81
+ return error instanceof Error ? error : new Error("gh invocation failed");
82
+ }
83
+ }
84
+ function parseJsonObject(stdout) {
85
+ try {
86
+ const parsed = JSON.parse(stdout);
87
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
88
+ ? parsed
89
+ : undefined;
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ }
95
+ function optionalString(value) {
96
+ return typeof value === "string" && value.length > 0 ? value : undefined;
97
+ }
98
+ function rawMergeReadinessFrom(facts, req) {
99
+ return {
100
+ prNumber: req.prExternalId,
101
+ headBranchName: optionalString(facts.headRef) ?? "unknown",
102
+ ...(optionalString(facts.state) !== undefined ? { state: optionalString(facts.state) } : {}),
103
+ ...(typeof facts.merged === "boolean" ? { merged: facts.merged } : {}),
104
+ ...(typeof facts.draft === "boolean" ? { draft: facts.draft } : {}),
105
+ ...(optionalString(facts.mergeable_state) !== undefined
106
+ ? { mergeableState: optionalString(facts.mergeable_state) }
107
+ : {}),
108
+ ...(optionalString(facts.base) !== undefined ? { baseRef: optionalString(facts.base) } : {}),
109
+ ...(optionalString(facts.head) !== undefined ? { headSha: optionalString(facts.head) } : {}),
110
+ };
111
+ }
112
+ function capableStrategiesFrom(cfg) {
113
+ const caps = [];
114
+ if (cfg.squash === true)
115
+ caps.push("squash");
116
+ if (cfg.rebase === true)
117
+ caps.push("rebase");
118
+ if (cfg.merge === true)
119
+ caps.push("merge-commit");
120
+ return caps;
121
+ }
122
+ const CHECKS_STATUS_BY_STATE = {
123
+ success: "passing",
124
+ pending: "pending",
125
+ failure: "failing",
126
+ error: "failing",
127
+ };
128
+ // A best-effort combined check-status read. Only the overallStatus drives the contract readiness
129
+ // derivation; the per-bucket counts are coarse (the combined state attributed to the matching bucket).
130
+ function checksStateFrom(state, total) {
131
+ const overallStatus = CHECKS_STATUS_BY_STATE[state] ?? "skipped";
132
+ return {
133
+ total,
134
+ passing: overallStatus === "passing" ? total : 0,
135
+ failing: overallStatus === "failing" ? total : 0,
136
+ pending: overallStatus === "pending" ? total : 0,
137
+ overallStatus,
138
+ };
139
+ }
140
+ function shouldReadHeadStatus(mergeableState) {
141
+ return (mergeableState === "blocked" || mergeableState === "unstable" || mergeableState === "unknown");
142
+ }
143
+ async function readHeadChecks(ctx, ownerAndRepo, headSha) {
144
+ if (headSha === undefined) {
145
+ return undefined;
146
+ }
147
+ let argv;
148
+ try {
149
+ argv = buildHeadStatusArgv(ownerAndRepo, headSha);
150
+ }
151
+ catch {
152
+ return undefined;
153
+ }
154
+ const result = await runGh(ctx, argv);
155
+ if (result instanceof Error || result.exitCode !== 0) {
156
+ return undefined;
157
+ }
158
+ const state = result.stdout.trim();
159
+ if (state.length === 0) {
160
+ return undefined;
161
+ }
162
+ return checksStateFrom(state, 0);
163
+ }
164
+ async function readMergeReadiness(ctx, req) {
165
+ let prArgv;
166
+ let cfgArgv;
167
+ try {
168
+ prArgv = buildMergeReadinessArgv(req);
169
+ cfgArgv = buildRepoMergeConfigArgv(req);
170
+ }
171
+ catch {
172
+ return { providerCapableStrategies: [], providerError: true };
173
+ }
174
+ const prResult = await runGh(ctx, prArgv);
175
+ if (prResult instanceof Error || prResult.exitCode !== 0) {
176
+ return { providerCapableStrategies: [], providerError: true };
177
+ }
178
+ const prObj = parseJsonObject(prResult.stdout);
179
+ if (prObj === undefined) {
180
+ return { providerCapableStrategies: [], providerError: true };
181
+ }
182
+ const raw = rawMergeReadinessFrom(prObj, req);
183
+ const pullRequest = mapRawMergeReadiness(raw);
184
+ const cfgResult = await runGh(ctx, cfgArgv);
185
+ const cfgObj = cfgResult instanceof Error || cfgResult.exitCode !== 0
186
+ ? undefined
187
+ : parseJsonObject(cfgResult.stdout);
188
+ const providerCapableStrategies = cfgObj !== undefined ? capableStrategiesFrom(cfgObj) : [];
189
+ const checks = shouldReadHeadStatus(raw.mergeableState)
190
+ ? await readHeadChecks(ctx, req.ownerAndRepo, raw.headSha)
191
+ : undefined;
192
+ return {
193
+ pullRequest,
194
+ providerCapableStrategies,
195
+ ...(checks !== undefined ? { checks } : {}),
196
+ };
197
+ }
198
+ // ─── Merge execute (+ guarded, non-fatal branch deletion) ─────────────────────────────────────────────
199
+ function parseMerged(stdout) {
200
+ return stdout.trim() === "true";
201
+ }
202
+ // Deletes the merged head branch best-effort. A failed deletion NEVER fails the merge (the merge already
203
+ // succeeded); it only reports branchDeleted=false.
204
+ async function deleteMergedBranch(ctx, req) {
205
+ let argv;
206
+ try {
207
+ argv = buildDeleteMergedBranchArgv(req.ownerAndRepo, req.headBranchName);
208
+ }
209
+ catch {
210
+ return false;
211
+ }
212
+ const result = await runGh(ctx, argv);
213
+ return !(result instanceof Error) && result.exitCode === 0;
214
+ }
215
+ async function mergePullRequest(ctx, req) {
216
+ let argv;
217
+ try {
218
+ argv = buildMergeArgv(req);
219
+ }
220
+ catch {
221
+ return executionResult("failed", 0, { errorCode: "internal-error" });
222
+ }
223
+ const result = await runGh(ctx, argv);
224
+ if (result instanceof Error) {
225
+ return failureFromThrow(result, 0);
226
+ }
227
+ if (result.exitCode !== 0) {
228
+ return rejectionFromExit(result);
229
+ }
230
+ const merged = parseMerged(result.stdout);
231
+ if (!req.deleteBranchAfterMerge || !merged) {
232
+ return executionResult("succeeded", result.durationMs, { merged });
233
+ }
234
+ const branchDeleted = await deleteMergedBranch(ctx, req);
235
+ return executionResult("succeeded", result.durationMs, { merged, branchDeleted });
236
+ }
237
+ export function createNodeGitMergeAdapter(deps) {
238
+ const ctx = buildRunContext(deps);
239
+ return {
240
+ readMergeReadiness: (req) => readMergeReadiness(ctx, req),
241
+ mergePullRequest: (req) => mergePullRequest(ctx, req),
242
+ };
243
+ }
@@ -0,0 +1,51 @@
1
+ import type { GitDeliveryAbortableOperation, GitDeliveryExecutionResult, GitDeliveryRecoveryStrategyHint } from "@oscharko-dev/keiko-contracts";
2
+ import type { CommandRule } from "./types.js";
3
+ export interface GitBranchCreateExecRequest {
4
+ readonly branchName: string;
5
+ readonly startPointRefHash: string;
6
+ }
7
+ export interface GitBranchSwitchExecRequest {
8
+ readonly branchName: string;
9
+ }
10
+ export interface GitStageExecRequest {
11
+ readonly pathspecs: readonly string[];
12
+ }
13
+ export interface GitUnstageExecRequest {
14
+ readonly pathspecs: readonly string[];
15
+ }
16
+ export interface GitCommitExecRequest {
17
+ readonly message: string;
18
+ readonly allowEmpty: boolean;
19
+ }
20
+ export interface GitAbortExecRequest {
21
+ readonly operationToAbort: GitDeliveryAbortableOperation;
22
+ }
23
+ export interface GitRecoveryExecRequest {
24
+ readonly recoveryStrategyHint: GitDeliveryRecoveryStrategyHint;
25
+ readonly targetRefHash: string;
26
+ readonly pathspecs: readonly string[];
27
+ }
28
+ export interface GitLocalMutationAdapter {
29
+ createBranch(req: GitBranchCreateExecRequest): Promise<GitDeliveryExecutionResult>;
30
+ switchBranch(req: GitBranchSwitchExecRequest): Promise<GitDeliveryExecutionResult>;
31
+ stage(req: GitStageExecRequest): Promise<GitDeliveryExecutionResult>;
32
+ unstage(req: GitUnstageExecRequest): Promise<GitDeliveryExecutionResult>;
33
+ commit(req: GitCommitExecRequest): Promise<GitDeliveryExecutionResult>;
34
+ abort(req: GitAbortExecRequest): Promise<GitDeliveryExecutionResult>;
35
+ recover(req: GitRecoveryExecRequest): Promise<GitDeliveryExecutionResult>;
36
+ }
37
+ export declare const GIT_MUTATION_ALLOWED_SUBCOMMANDS: readonly string[];
38
+ export declare const GIT_MUTATION_COMMAND_RULES: readonly CommandRule[];
39
+ export declare class GitMutationArgvError extends Error {
40
+ constructor(message: string);
41
+ }
42
+ export type GitMutationArgvPlan = readonly (readonly string[])[];
43
+ export declare function buildBranchCreateArgv(req: GitBranchCreateExecRequest): GitMutationArgvPlan;
44
+ export declare function buildBranchSwitchArgv(req: GitBranchSwitchExecRequest): GitMutationArgvPlan;
45
+ export declare function buildStageArgv(req: GitStageExecRequest): GitMutationArgvPlan;
46
+ export declare function buildUnstageArgv(req: GitUnstageExecRequest): GitMutationArgvPlan;
47
+ export declare function buildCommitArgv(req: GitCommitExecRequest): GitMutationArgvPlan;
48
+ export declare function buildAbortArgv(req: GitAbortExecRequest): GitMutationArgvPlan;
49
+ export declare function buildRecoveryArgv(req: GitRecoveryExecRequest): GitMutationArgvPlan;
50
+ export declare function gitMutationPlanIsGoverned(plan: GitMutationArgvPlan): boolean;
51
+ //# sourceMappingURL=git-mutation-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-mutation-adapter.d.ts","sourceRoot":"","sources":["../src/git-mutation-adapter.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EACV,6BAA6B,EAC7B,0BAA0B,EAC1B,+BAA+B,EAChC,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM9C,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACpC;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,gBAAgB,EAAE,6BAA6B,CAAC;CAC1D;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,oBAAoB,EAAE,+BAA+B,CAAC;IAC/D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAE/B,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAOD,MAAM,WAAW,uBAAuB;IACtC,YAAY,CAAC,GAAG,EAAE,0BAA0B,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;IACnF,YAAY,CAAC,GAAG,EAAE,0BAA0B,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;IACnF,KAAK,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;IACrE,OAAO,CAAC,GAAG,EAAE,qBAAqB,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;IACzE,MAAM,CAAC,GAAG,EAAE,oBAAoB,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;IACvE,KAAK,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;IACrE,OAAO,CAAC,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;CAC3E;AAOD,eAAO,MAAM,gCAAgC,EAAE,SAAS,MAAM,EAa5D,CAAC;AAMH,eAAO,MAAM,0BAA0B,EAAE,SAAS,WAAW,EA2B3D,CAAC;AAOH,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AA6DD,MAAM,MAAM,mBAAmB,GAAG,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC;AAEjE,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,0BAA0B,GAAG,mBAAmB,CAQ1F;AAMD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,0BAA0B,GAAG,mBAAmB,CAE1F;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,mBAAmB,GAAG,mBAAmB,CAE5E;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,qBAAqB,GAAG,mBAAmB,CAEhF;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,oBAAoB,GAAG,mBAAmB,CAO9E;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,mBAAmB,GAAG,mBAAmB,CAgB5E;AAMD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,sBAAsB,GAAG,mBAAmB,CAmBlF;AASD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAI5E"}
@@ -0,0 +1,207 @@
1
+ // The narrow local Git mutation adapter contract (Issue #472, Epic #470) — AC3.
2
+ //
3
+ // This module defines WHAT the governed local Git adapter may do, as data and pure functions:
4
+ //
5
+ // 1. `GitLocalMutationAdapter` — the port. One typed method per local mutation kind. There is NO
6
+ // method that accepts an arbitrary argument vector, so there is no path from the kernel to
7
+ // "run this git command string". Local Git write authority is reachable only through these
8
+ // typed operations.
9
+ // 2. `GIT_MUTATION_ALLOWED_SUBCOMMANDS` / `GIT_MUTATION_COMMAND_RULES` — the closed allowlist the
10
+ // Node adapter (git-mutation-node.ts) hands to the keiko-tools no-shell spawn boundary. It is a
11
+ // SEPARATE, dedicated rule set from both the read-only terminal policy and the harness default,
12
+ // and it permits only the governed mutation subcommands.
13
+ // 3. The pure argv builders — each maps validated, typed operands to a fixed argument vector whose
14
+ // first token is always one of the allowed subcommands. Operands are validated (no NUL, no
15
+ // flag-injection via a leading "-" on refs) and file operands are literalized before they are
16
+ // placed after a "--" sentinel so a repository-controlled filename cannot be reinterpreted as an
17
+ // option or Git pathspec magic.
18
+ //
19
+ // Pure module: types, frozen tables, and total pure functions. No IO, no spawn, no child_process —
20
+ // the actual execution adapter is git-mutation-node.ts on the `./internal/git-mutation` subpath.
21
+ // ─── Closed subcommand allowlist + dedicated command rules ────────────────────────────────
22
+ // The exact git subcommands the governed adapter may run. No read-only inspection subcommand and no
23
+ // network subcommand (push/fetch/pull/clone) appears here; remote and provider execution is a later
24
+ // slice (#476–#478) behind a separate gateway, never this local adapter.
25
+ export const GIT_MUTATION_ALLOWED_SUBCOMMANDS = Object.freeze([
26
+ "branch",
27
+ "switch",
28
+ "add",
29
+ "restore",
30
+ "commit",
31
+ "reset",
32
+ "stash",
33
+ "merge",
34
+ "rebase",
35
+ "cherry-pick",
36
+ "revert",
37
+ "bisect",
38
+ ]);
39
+ // The dedicated allowlist handed to the keiko-tools spawn boundary by the Node adapter. Mirrors the
40
+ // terminal policy's defense-in-depth flag denials (global config/cwd-shifting/code-execution flags)
41
+ // so that even though the adapter only ever builds argv from typed operands, a smuggled global flag
42
+ // would still be rejected before spawn.
43
+ export const GIT_MUTATION_COMMAND_RULES = Object.freeze([
44
+ {
45
+ executable: "git",
46
+ allowedSubcommands: GIT_MUTATION_ALLOWED_SUBCOMMANDS,
47
+ valueFlags: Object.freeze([
48
+ "-C",
49
+ "-c",
50
+ "--git-dir",
51
+ "--work-tree",
52
+ "--namespace",
53
+ "--exec-path",
54
+ ]),
55
+ denyFlags: Object.freeze([
56
+ "-C",
57
+ "-c",
58
+ "--config-env",
59
+ "--git-dir",
60
+ "--work-tree",
61
+ "--namespace",
62
+ "--exec-path",
63
+ "--ext-diff",
64
+ "--textconv",
65
+ "--no-index",
66
+ "--contents",
67
+ "--output",
68
+ ]),
69
+ },
70
+ ]);
71
+ // ─── Operand validation ───────────────────────────────────────────────────────────────────
72
+ // Thrown when typed operands cannot be turned into a safe argv (empty, NUL-bearing, or a ref/branch
73
+ // that would be read as a flag). The Node adapter maps this to an `internal-error` execution result;
74
+ // it never reaches a spawn.
75
+ export class GitMutationArgvError extends Error {
76
+ constructor(message) {
77
+ super(message);
78
+ this.name = "GitMutationArgvError";
79
+ }
80
+ }
81
+ function hasNul(value) {
82
+ return value.includes("\u0000");
83
+ }
84
+ // A ref/branch operand passed as a bare positional. Rejects empty, NUL, and a leading "-" so it can
85
+ // never be reinterpreted as a git option.
86
+ function assertRef(value, label) {
87
+ if (value.length === 0) {
88
+ throw new GitMutationArgvError(`${label} must not be empty`);
89
+ }
90
+ if (hasNul(value)) {
91
+ throw new GitMutationArgvError(`${label} must not contain a NUL byte`);
92
+ }
93
+ if (value.startsWith("-")) {
94
+ throw new GitMutationArgvError(`${label} must not start with "-" (flag-injection guard)`);
95
+ }
96
+ return value;
97
+ }
98
+ // A commit message passed as the value of `--message`. May start with "-" (git consumes it as the
99
+ // flag's value), but must be non-empty and NUL-free.
100
+ function assertMessage(value) {
101
+ if (value.length === 0) {
102
+ throw new GitMutationArgvError("commit message must not be empty");
103
+ }
104
+ if (hasNul(value)) {
105
+ throw new GitMutationArgvError("commit message must not contain a NUL byte");
106
+ }
107
+ return value;
108
+ }
109
+ function literalPathspec(value) {
110
+ return `:(literal)${value}`;
111
+ }
112
+ // File operands placed after the "--" sentinel. May start with "-" (the sentinel disarms option
113
+ // parsing), but must be non-empty and NUL-free, and the list must be non-empty. Returned argv tokens
114
+ // are literal pathspecs so names such as ":(top)*" cannot expand beyond the selected file.
115
+ function assertPathspecs(values) {
116
+ if (values.length === 0) {
117
+ throw new GitMutationArgvError("at least one pathspec is required");
118
+ }
119
+ for (const value of values) {
120
+ if (value.length === 0) {
121
+ throw new GitMutationArgvError("pathspec must not be empty");
122
+ }
123
+ if (hasNul(value)) {
124
+ throw new GitMutationArgvError("pathspec must not contain a NUL byte");
125
+ }
126
+ }
127
+ return values.map(literalPathspec);
128
+ }
129
+ export function buildBranchCreateArgv(req) {
130
+ return [
131
+ [
132
+ "branch",
133
+ assertRef(req.branchName, "branchName"),
134
+ assertRef(req.startPointRefHash, "startPointRefHash"),
135
+ ],
136
+ ];
137
+ }
138
+ // Switch HEAD to an EXISTING local branch. `git switch` (never `checkout`) is branch-only: it cannot
139
+ // be coerced into a path checkout, so it carries no file-overwrite surface. The branch operand is a
140
+ // bare positional guarded by assertRef (non-empty, NUL-free, no leading "-" flag-injection), mirroring
141
+ // branch-create; switch takes no pathspecs so no "--" sentinel is needed.
142
+ export function buildBranchSwitchArgv(req) {
143
+ return [["switch", assertRef(req.branchName, "branchName")]];
144
+ }
145
+ export function buildStageArgv(req) {
146
+ return [["add", "--", ...assertPathspecs(req.pathspecs)]];
147
+ }
148
+ export function buildUnstageArgv(req) {
149
+ return [["restore", "--staged", "--", ...assertPathspecs(req.pathspecs)]];
150
+ }
151
+ export function buildCommitArgv(req) {
152
+ const argv = ["commit"];
153
+ if (req.allowEmpty) {
154
+ argv.push("--allow-empty");
155
+ }
156
+ argv.push("--message", assertMessage(req.message));
157
+ return [argv];
158
+ }
159
+ export function buildAbortArgv(req) {
160
+ switch (req.operationToAbort) {
161
+ case "merge":
162
+ return [["merge", "--abort"]];
163
+ case "rebase":
164
+ return [["rebase", "--abort"]];
165
+ case "cherry-pick":
166
+ return [["cherry-pick", "--abort"]];
167
+ case "revert":
168
+ return [["revert", "--abort"]];
169
+ case "bisect":
170
+ // A bisect has no `--abort`; `git bisect reset` is its termination form.
171
+ return [["bisect", "reset"]];
172
+ default:
173
+ return assertNeverAbort(req.operationToAbort);
174
+ }
175
+ }
176
+ function assertNeverAbort(operation) {
177
+ throw new GitMutationArgvError(`unhandled abortable operation: ${JSON.stringify(operation)}`);
178
+ }
179
+ export function buildRecoveryArgv(req) {
180
+ const target = assertRef(req.targetRefHash, "targetRefHash");
181
+ switch (req.recoveryStrategyHint) {
182
+ case "soft-reset":
183
+ return [["reset", "--soft", target]];
184
+ case "mixed-reset":
185
+ return [["reset", "--mixed", target]];
186
+ case "stash-and-reset":
187
+ // Stash (including untracked) FIRST so no local work is lost, then hard-reset to the target.
188
+ // The stash is the guided recovery point; the hard reset is safe because the work is preserved.
189
+ return [
190
+ ["stash", "push", "--include-untracked"],
191
+ ["reset", "--hard", target],
192
+ ];
193
+ case "restore-index":
194
+ return [["restore", "--source", target, "--staged", "--", ...assertPathspecs(req.pathspecs)]];
195
+ default:
196
+ return assertNeverRecovery(req.recoveryStrategyHint);
197
+ }
198
+ }
199
+ function assertNeverRecovery(hint) {
200
+ throw new GitMutationArgvError(`unhandled recovery strategy: ${JSON.stringify(hint)}`);
201
+ }
202
+ // True iff every command in the plan begins with an allowed governed mutation subcommand. Used by
203
+ // tests (and available to callers) to prove the no-generic-fallback property structurally: there is
204
+ // no producible plan whose first token is outside GIT_MUTATION_ALLOWED_SUBCOMMANDS.
205
+ export function gitMutationPlanIsGoverned(plan) {
206
+ return plan.every((argv) => argv.length > 0 && GIT_MUTATION_ALLOWED_SUBCOMMANDS.includes(argv[0] ?? ""));
207
+ }
@@ -0,0 +1,23 @@
1
+ import type { GitDeliveryEvidenceRecord, GitDeliveryEvidenceRef } from "@oscharko-dev/keiko-contracts";
2
+ import type { GitMutationLifecycleResult } from "./git-mutation-orchestrator.js";
3
+ export interface GitDeliveryEvidenceBuildInput {
4
+ readonly result: GitMutationLifecycleResult;
5
+ readonly snapshot: GitDeliveryEvidenceSnapshot;
6
+ readonly workflowRunId: string;
7
+ readonly repoId?: string | undefined;
8
+ readonly attemptSequence?: number | undefined;
9
+ readonly evidenceRef?: GitDeliveryEvidenceRef | undefined;
10
+ }
11
+ export interface GitDeliveryEvidenceSnapshot {
12
+ readonly headDetached: boolean;
13
+ readonly currentBranchName?: string | undefined;
14
+ readonly stagedFileCount: number;
15
+ readonly unstagedFileCount: number;
16
+ readonly untrackedFileCount: number;
17
+ }
18
+ export interface GitDeliveryEvidenceBuildDeps {
19
+ readonly now: () => number;
20
+ readonly hash?: ((input: string) => string) | undefined;
21
+ }
22
+ export declare function buildGitDeliveryEvidenceRecord(input: GitDeliveryEvidenceBuildInput, deps: GitDeliveryEvidenceBuildDeps): GitDeliveryEvidenceRecord;
23
+ //# sourceMappingURL=git-mutation-evidence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-mutation-evidence.d.ts","sourceRoot":"","sources":["../src/git-mutation-evidence.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAUV,yBAAyB,EACzB,sBAAsB,EAOvB,MAAM,+BAA+B,CAAC;AASvC,OAAO,KAAK,EACV,0BAA0B,EAE3B,MAAM,gCAAgC,CAAC;AAMxC,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAC;IAE5C,QAAQ,CAAC,QAAQ,EAAE,2BAA2B,CAAC;IAG/C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAE/B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,WAAW,CAAC,EAAE,sBAAsB,GAAG,SAAS,CAAC;CAC3D;AAKD,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAE3B,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,SAAS,CAAC;CACzD;AAwSD,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,6BAA6B,EACpC,IAAI,EAAE,4BAA4B,GACjC,yBAAyB,CAiC3B"}