@ai-sdlc/orchestrator 0.6.0 → 0.10.0
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/__test-helpers/git-env.d.ts +40 -0
- package/dist/__test-helpers/git-env.js +73 -0
- package/dist/admission-composite.d.ts +89 -0
- package/dist/admission-composite.js +241 -0
- package/dist/admission-enrichment.d.ts +142 -0
- package/dist/admission-enrichment.js +341 -0
- package/dist/admission-hc.d.ts +81 -0
- package/dist/admission-hc.js +94 -0
- package/dist/admission-score.d.ts +109 -5
- package/dist/admission-score.js +90 -11
- package/dist/analysis/analyzer.js +3 -2
- package/dist/analysis/convention-detector.d.ts +85 -2
- package/dist/analysis/convention-detector.js +375 -70
- package/dist/analysis/diff-analyzer.d.ts +33 -0
- package/dist/analysis/diff-analyzer.js +122 -0
- package/dist/analysis/hotspot-analyzer.js +3 -1
- package/dist/analysis/index.d.ts +2 -1
- package/dist/analysis/index.js +2 -1
- package/dist/artifacts/index.d.ts +65 -0
- package/dist/artifacts/index.js +142 -0
- package/dist/backlog-adapter.d.ts +121 -0
- package/dist/backlog-adapter.js +438 -0
- package/dist/calibration.d.ts +43 -0
- package/dist/calibration.js +76 -0
- package/dist/cli/commands/agents.d.ts +24 -0
- package/dist/cli/commands/agents.js +66 -1
- package/dist/cli/commands/git-remote.d.ts +74 -0
- package/dist/cli/commands/git-remote.js +170 -0
- package/dist/cli/commands/health.d.ts +4 -0
- package/dist/cli/commands/health.js +13 -1
- package/dist/cli/commands/init-features.d.ts +208 -0
- package/dist/cli/commands/init-features.js +473 -0
- package/dist/cli/commands/init-templates.d.ts +104 -0
- package/dist/cli/commands/init-templates.js +399 -0
- package/dist/cli/commands/init.d.ts +48 -0
- package/dist/cli/commands/init.js +322 -23
- package/dist/cli/commands/mcp-setup.d.ts +31 -0
- package/dist/cli/commands/mcp-setup.js +78 -8
- package/dist/cli/formatters/table.js +15 -2
- package/dist/cli/index.d.ts +14 -1
- package/dist/cli/index.js +81 -20
- package/dist/cli/versions.d.ts +57 -0
- package/dist/cli/versions.js +128 -0
- package/dist/code-area-classifier.d.ts +21 -0
- package/dist/code-area-classifier.js +48 -0
- package/dist/config.d.ts +33 -1
- package/dist/config.js +78 -8
- package/dist/database/adapters/external.d.ts +24 -0
- package/dist/database/adapters/external.js +80 -0
- package/dist/database/adapters/neon.d.ts +41 -0
- package/dist/database/adapters/neon.js +98 -0
- package/dist/database/adapters/pg-snapshot-restore.d.ts +28 -0
- package/dist/database/adapters/pg-snapshot-restore.js +68 -0
- package/dist/database/adapters/sqlite-copy.d.ts +32 -0
- package/dist/database/adapters/sqlite-copy.js +145 -0
- package/dist/database/connection-injection.d.ts +35 -0
- package/dist/database/connection-injection.js +93 -0
- package/dist/database/index.d.ts +12 -0
- package/dist/database/index.js +23 -0
- package/dist/database/registry.d.ts +13 -0
- package/dist/database/registry.js +27 -0
- package/dist/database/topology.d.ts +52 -0
- package/dist/database/topology.js +44 -0
- package/dist/database/types.d.ts +89 -0
- package/dist/database/types.js +26 -0
- package/dist/design-authority.d.ts +53 -0
- package/dist/design-authority.js +84 -0
- package/dist/design-lookahead.d.ts +64 -0
- package/dist/design-lookahead.js +86 -0
- package/dist/design-quality-trend.d.ts +87 -0
- package/dist/design-quality-trend.js +190 -0
- package/dist/design-system-context.d.ts +46 -0
- package/dist/design-system-context.js +80 -0
- package/dist/design-system-correction-loop.d.ts +64 -0
- package/dist/design-system-correction-loop.js +128 -0
- package/dist/design-system-metrics.d.ts +61 -0
- package/dist/design-system-metrics.js +104 -0
- package/dist/design-system-stewardship.d.ts +22 -0
- package/dist/design-system-stewardship.js +85 -0
- package/dist/design-system-validation.d.ts +37 -0
- package/dist/design-system-validation.js +88 -0
- package/dist/dispatch/index.d.ts +4 -0
- package/dist/dispatch/index.js +4 -0
- package/dist/dispatch/merge-gate.d.ts +46 -0
- package/dist/dispatch/merge-gate.js +90 -0
- package/dist/dispatch/requeue.d.ts +57 -0
- package/dist/dispatch/requeue.js +131 -0
- package/dist/dispatch/worker-pool.d.ts +62 -0
- package/dist/dispatch/worker-pool.js +60 -0
- package/dist/execute.d.ts +40 -0
- package/dist/execute.js +188 -15
- package/dist/fix-ci.js +8 -1
- package/dist/fix-review.js +8 -1
- package/dist/harness/adapters/claude-code.d.ts +29 -0
- package/dist/harness/adapters/claude-code.js +191 -0
- package/dist/harness/adapters/codex.d.ts +25 -0
- package/dist/harness/adapters/codex.js +61 -0
- package/dist/harness/independence.d.ts +51 -0
- package/dist/harness/independence.js +67 -0
- package/dist/harness/index.d.ts +14 -0
- package/dist/harness/index.js +20 -0
- package/dist/harness/registry.d.ts +17 -0
- package/dist/harness/registry.js +31 -0
- package/dist/harness/types.d.ts +123 -0
- package/dist/harness/types.js +8 -0
- package/dist/harness/version-probe.d.ts +14 -0
- package/dist/harness/version-probe.js +123 -0
- package/dist/index.d.ts +30 -4
- package/dist/index.js +35 -2
- package/dist/models/classifier.d.ts +78 -0
- package/dist/models/classifier.js +277 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.js +3 -0
- package/dist/models/registry.d.ts +97 -0
- package/dist/models/registry.js +173 -0
- package/dist/pillar-breakdown.d.ts +85 -0
- package/dist/pillar-breakdown.js +162 -0
- package/dist/priority.d.ts +5 -0
- package/dist/priority.js +18 -5
- package/dist/review-meta.d.ts +65 -0
- package/dist/review-meta.js +149 -0
- package/dist/review.d.ts +4 -0
- package/dist/review.js +12 -2
- package/dist/runners/claude-code-sdk.d.ts +31 -0
- package/dist/runners/claude-code-sdk.js +238 -0
- package/dist/runners/claude-code.d.ts +3 -0
- package/dist/runners/claude-code.js +30 -66
- package/dist/runners/codex.js +4 -1
- package/dist/runners/copilot.js +4 -1
- package/dist/runners/cursor.js +4 -1
- package/dist/runners/git-utils.d.ts +81 -0
- package/dist/runners/git-utils.js +201 -0
- package/dist/runners/index.d.ts +3 -0
- package/dist/runners/index.js +3 -0
- package/dist/runners/review-agent.d.ts +20 -0
- package/dist/runners/review-agent.js +171 -41
- package/dist/runners/runner-registry.js +10 -0
- package/dist/runners/sdk-review-runner.d.ts +65 -0
- package/dist/runners/sdk-review-runner.js +185 -0
- package/dist/runners/security-triage.d.ts +20 -4
- package/dist/runners/security-triage.js +39 -15
- package/dist/runners/types.d.ts +6 -0
- package/dist/runtime/attestations.d.ts +766 -0
- package/dist/runtime/attestations.js +1195 -0
- package/dist/runtime/git-env.d.ts +53 -0
- package/dist/runtime/git-env.js +60 -0
- package/dist/runtime/index.d.ts +7 -0
- package/dist/runtime/index.js +7 -0
- package/dist/runtime/parallelism-flag.d.ts +28 -0
- package/dist/runtime/parallelism-flag.js +39 -0
- package/dist/runtime/port-allocator.d.ts +32 -0
- package/dist/runtime/port-allocator.js +96 -0
- package/dist/runtime/worktree-pool.d.ts +86 -0
- package/dist/runtime/worktree-pool.js +204 -0
- package/dist/runtime/worktree.d.ts +25 -0
- package/dist/runtime/worktree.js +111 -0
- package/dist/sa-scoring/auto-calibrate.d.ts +69 -0
- package/dist/sa-scoring/auto-calibrate.js +107 -0
- package/dist/sa-scoring/c1-sa2-computable.d.ts +42 -0
- package/dist/sa-scoring/c1-sa2-computable.js +59 -0
- package/dist/sa-scoring/composite.d.ts +107 -0
- package/dist/sa-scoring/composite.js +139 -0
- package/dist/sa-scoring/depparse-client.d.ts +79 -0
- package/dist/sa-scoring/depparse-client.js +187 -0
- package/dist/sa-scoring/did-compiler.d.ts +122 -0
- package/dist/sa-scoring/did-compiler.js +286 -0
- package/dist/sa-scoring/drift-monitor.d.ts +84 -0
- package/dist/sa-scoring/drift-monitor.js +186 -0
- package/dist/sa-scoring/exemplar-bank.d.ts +78 -0
- package/dist/sa-scoring/exemplar-bank.js +154 -0
- package/dist/sa-scoring/feedback-store.d.ts +100 -0
- package/dist/sa-scoring/feedback-store.js +156 -0
- package/dist/sa-scoring/index.d.ts +71 -0
- package/dist/sa-scoring/index.js +158 -0
- package/dist/sa-scoring/layer1-deterministic.d.ts +115 -0
- package/dist/sa-scoring/layer1-deterministic.js +298 -0
- package/dist/sa-scoring/layer2-structural.d.ts +71 -0
- package/dist/sa-scoring/layer2-structural.js +151 -0
- package/dist/sa-scoring/layer3-llm.d.ts +86 -0
- package/dist/sa-scoring/layer3-llm.js +282 -0
- package/dist/sa-scoring/rescore-orchestrator.d.ts +52 -0
- package/dist/sa-scoring/rescore-orchestrator.js +47 -0
- package/dist/scheduling/burn-down.d.ts +27 -0
- package/dist/scheduling/burn-down.js +43 -0
- package/dist/scheduling/calibration.d.ts +42 -0
- package/dist/scheduling/calibration.js +150 -0
- package/dist/scheduling/index.d.ts +8 -0
- package/dist/scheduling/index.js +8 -0
- package/dist/scheduling/ledger.d.ts +59 -0
- package/dist/scheduling/ledger.js +216 -0
- package/dist/scheduling/off-peak.d.ts +27 -0
- package/dist/scheduling/off-peak.js +112 -0
- package/dist/scheduling/schedule-decision.d.ts +39 -0
- package/dist/scheduling/schedule-decision.js +89 -0
- package/dist/scheduling/tier-analysis.d.ts +47 -0
- package/dist/scheduling/tier-analysis.js +81 -0
- package/dist/scheduling/types.d.ts +140 -0
- package/dist/scheduling/types.js +11 -0
- package/dist/shared.d.ts +13 -0
- package/dist/shared.js +32 -0
- package/dist/state/schema.d.ts +5 -1
- package/dist/state/schema.js +223 -1
- package/dist/state/store.d.ts +55 -1
- package/dist/state/store.js +342 -7
- package/dist/state/types.d.ts +139 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate-agent-output.js +4 -1
- package/dist/watch.js +6 -0
- package/package.json +3 -2
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cryptographic review attestations (AISDLC-74).
|
|
3
|
+
*
|
|
4
|
+
* `/ai-sdlc execute` runs three reviewer subagents (code/test/security) locally
|
|
5
|
+
* before pushing. CI then re-ran the same reviewers via `Post Review Results` —
|
|
6
|
+
* burning tokens on duplicate work.
|
|
7
|
+
*
|
|
8
|
+
* This module provides the primitives `/ai-sdlc execute` and the
|
|
9
|
+
* `verify-attestation.yml` workflow share to skip CI review when a valid local
|
|
10
|
+
* attestation exists. The shape is a DSSE envelope (in-toto / SLSA pattern)
|
|
11
|
+
* carrying a versioned predicate that commits to the commit SHA, diff hash,
|
|
12
|
+
* policy hash, and reviewer agent file hashes — so CI can reject envelopes
|
|
13
|
+
* after force-push, after a policy edit, or after a reviewer agent change.
|
|
14
|
+
*
|
|
15
|
+
* ## Threat model (in-scope)
|
|
16
|
+
*
|
|
17
|
+
* - Lazy contributor faking attestation → signature mismatch
|
|
18
|
+
* - Copy-pasted attestation from another PR → subject digest mismatch
|
|
19
|
+
* - Replay after diff changed (force-push) → diffHash mismatch
|
|
20
|
+
* - Attestation issued before a policy edit → policyHash mismatch
|
|
21
|
+
* - Stale reviewer-agent attestation → agentFileHash mismatch
|
|
22
|
+
* - Schema drift / forward-compat smuggling → schemaVersion enforcement
|
|
23
|
+
*
|
|
24
|
+
* Out of scope: compromised dev machine, compromised CI runner, collusion.
|
|
25
|
+
*
|
|
26
|
+
* ## Why ed25519 + Node's built-in crypto (no Sigstore)
|
|
27
|
+
*
|
|
28
|
+
* The keys are project-controlled, committed in `.ai-sdlc/trusted-reviewers.yaml`,
|
|
29
|
+
* and small (32-byte). Sigstore would add Fulcio + Rekor + transparency log
|
|
30
|
+
* infrastructure for no benefit at this scale. ed25519 is what `ssh-keygen
|
|
31
|
+
* -t ed25519` and `git commit -S` already use; Node's `crypto.sign(null, ...)`
|
|
32
|
+
* supports it natively.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* The currently-accepted predicate schema versions. CI rejects any envelope
|
|
36
|
+
* whose `payload.schemaVersion` is not in this allowlist — this is the
|
|
37
|
+
* forward-compatibility hatch.
|
|
38
|
+
*
|
|
39
|
+
* AISDLC-103 (Verifier Phase 3) narrowed this to `['v3']` only:
|
|
40
|
+
* - `v1` envelopes (pre-AISDLC-94, diffHash-only) are rejected.
|
|
41
|
+
* - `v2` was never landed as a distinct schemaVersion — the AISDLC-94
|
|
42
|
+
* `contentHash` and AISDLC-101 `contentHashV3` shipped under the v1
|
|
43
|
+
* schemaVersion as additive optional fields during the dual- and
|
|
44
|
+
* triple-hash soak windows.
|
|
45
|
+
* - `v3` envelopes carry `contentHashV3` as a required field and DO NOT
|
|
46
|
+
* carry `diffHash` or `contentHash` (the legacy hashes are forbidden;
|
|
47
|
+
* a v3 envelope smuggling either field is rejected by
|
|
48
|
+
* `validatePredicateShape`).
|
|
49
|
+
*
|
|
50
|
+
* Exported so the `verify-attestation` workflow can `import`/inline it.
|
|
51
|
+
*/
|
|
52
|
+
export declare const ACCEPTED_SCHEMA_VERSIONS: readonly ["v3"];
|
|
53
|
+
export type SchemaVersion = (typeof ACCEPTED_SCHEMA_VERSIONS)[number];
|
|
54
|
+
/**
|
|
55
|
+
* The DSSE PAE payload type for our predicate. DSSE spec mandates a payload
|
|
56
|
+
* type URI — we use a project-controlled vendor URI rather than the
|
|
57
|
+
* in-toto Statement format (which would force us to shape the predicate
|
|
58
|
+
* around `_type` + `subject` at the envelope layer instead of the predicate).
|
|
59
|
+
*/
|
|
60
|
+
export declare const DSSE_PAYLOAD_TYPE = "application/vnd.ai-sdlc.attestation+json";
|
|
61
|
+
/** SHA-1 commit digest (40 hex chars) for the subject of an attestation. */
|
|
62
|
+
export interface SubjectDigest {
|
|
63
|
+
/** sha1 of the git commit being attested (40 hex chars). */
|
|
64
|
+
sha1: string;
|
|
65
|
+
}
|
|
66
|
+
/** A single reviewer's contribution to the predicate. */
|
|
67
|
+
export interface ReviewerEntry {
|
|
68
|
+
/** Agent identifier — matches the `name` field of the agent .md file. */
|
|
69
|
+
agentId: string;
|
|
70
|
+
/** sha256 of the reviewer agent's `.md` file at the time of review. */
|
|
71
|
+
agentFileHash: string;
|
|
72
|
+
/** Harness used for the review (e.g. `codex`, `claude-code`). */
|
|
73
|
+
harness: string;
|
|
74
|
+
/** Verdict — true if the reviewer approved, false otherwise. */
|
|
75
|
+
approved: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Findings counts by severity. We commit to *counts only* (not the full
|
|
78
|
+
* verdict JSON) to keep attestations small (~1-2KB). The full verdicts
|
|
79
|
+
* live in the PR body for human review; CI doesn't need them.
|
|
80
|
+
*/
|
|
81
|
+
findings: {
|
|
82
|
+
critical: number;
|
|
83
|
+
major: number;
|
|
84
|
+
minor: number;
|
|
85
|
+
suggestion: number;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** The signed payload — what the predicate actually attests. */
|
|
89
|
+
export interface AttestationPredicate {
|
|
90
|
+
/** Schema version — mandatory, enforced at verify time. */
|
|
91
|
+
schemaVersion: SchemaVersion;
|
|
92
|
+
/** The commit being attested. */
|
|
93
|
+
subject: {
|
|
94
|
+
digest: SubjectDigest;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Per-file-delta content binding (AISDLC-101 — required as of AISDLC-103
|
|
98
|
+
* Phase 3). sha256 over a canonical line-per-file string of the form
|
|
99
|
+
* `<path>\t<fileDeltaHash>\n` (sorted ascending by path), where
|
|
100
|
+
* `fileDeltaHash[path] = sha256(<base_blob_sha> + ' -> ' +
|
|
101
|
+
* <head_blob_sha>)`. The base blob SHA comes from the merge-base of the
|
|
102
|
+
* PR's `<baseRef>` and `<headRef>`; the head blob SHA from the PR's
|
|
103
|
+
* `<headRef>`.
|
|
104
|
+
*
|
|
105
|
+
* Why this is the only content binding in v3:
|
|
106
|
+
* - `diffHash` (legacy v1, sha256 of literal `git diff` text) broke on
|
|
107
|
+
* every rebase because `@@` hunk headers shift even when the
|
|
108
|
+
* post-apply file content doesn't change.
|
|
109
|
+
* - `contentHash` (AISDLC-94, sha256 of `(path, head_blob_sha)` per
|
|
110
|
+
* file) was rebase-tolerant for the no-overlap case but broke in the
|
|
111
|
+
* AISDLC-93 / PR #102 sibling-overlap case (the rebased file's HEAD
|
|
112
|
+
* blob contained the sibling's contributions, so the head blob SHA
|
|
113
|
+
* changed even though OUR contribution was unchanged).
|
|
114
|
+
* - `contentHashV3` commits to the (base, head) blob-pair TRANSITION
|
|
115
|
+
* per file ("we moved file F from blob A to blob B"). Stable when
|
|
116
|
+
* paired with the producer-side pre-sign rebase from AISDLC-102 even
|
|
117
|
+
* in the sibling-overlap case, and a genuine content tampering still
|
|
118
|
+
* flips the head blob SHA → fileDeltaHash flips → reject (threat
|
|
119
|
+
* model preserved).
|
|
120
|
+
*
|
|
121
|
+
* Required for v3 envelopes. The dual-hash (v1 → AISDLC-94) and
|
|
122
|
+
* triple-hash (AISDLC-94 → AISDLC-101) windows kept this optional under
|
|
123
|
+
* schemaVersion `v1`; AISDLC-103 narrows the accepted-schema-versions
|
|
124
|
+
* allowlist to `['v3']` and makes `contentHashV3` mandatory in
|
|
125
|
+
* `validatePredicateShape`. Legacy envelopes carrying only `diffHash`
|
|
126
|
+
* and/or `contentHash` are rejected with a schemaVersion-allowlist reason.
|
|
127
|
+
*/
|
|
128
|
+
contentHashV3: string;
|
|
129
|
+
/**
|
|
130
|
+
* Base-independent per-file head-blob binding (AISDLC-193.1). sha256
|
|
131
|
+
* over `JSON.stringify(sorted([{path, headBlobSha}]))` for every
|
|
132
|
+
* changed file in `<base>...<head>`, EXCLUDING the envelope file
|
|
133
|
+
* itself (`.ai-sdlc/attestations/<sha>.dsse.json` — see
|
|
134
|
+
* `isAttestationEnvelopePath` for the rationale).
|
|
135
|
+
*
|
|
136
|
+
* Why both v3 AND v4 ship side by side during the transition:
|
|
137
|
+
* - In-flight envelopes signed before AISDLC-193.1 carry only `v3`.
|
|
138
|
+
* Verifier accepts those via the existing v3 ancestor walk so the
|
|
139
|
+
* queue doesn't reject envelopes that were valid yesterday.
|
|
140
|
+
* - New envelopes carry BOTH so the verifier prefers v4 (skip the
|
|
141
|
+
* walk, base-independent) but can fall back to v3 if v4 doesn't
|
|
142
|
+
* match (= the rare case where head blobs DID change between
|
|
143
|
+
* signing and verification — e.g. amend-after-sign).
|
|
144
|
+
*
|
|
145
|
+
* After the transition window, `contentHashV3` will be deprecated
|
|
146
|
+
* and the verifier will require `contentHashV4`. For now both are
|
|
147
|
+
* populated by `buildPredicate` so any envelope this code emits is
|
|
148
|
+
* verifiable on both legs. The TypeScript type marks v4 as OPTIONAL
|
|
149
|
+
* because envelopes parsed from disk that pre-date AISDLC-193.1 will
|
|
150
|
+
* not carry the field; `validatePredicateShape` accepts the absence
|
|
151
|
+
* (= legacy v3 envelope) and the verifier falls back to the v3
|
|
152
|
+
* ancestor walk in that case.
|
|
153
|
+
*/
|
|
154
|
+
contentHashV4?: string;
|
|
155
|
+
/** sha256 of `.ai-sdlc/review-policy.md` at attestation time. */
|
|
156
|
+
policyHash: string;
|
|
157
|
+
/** Reviewer entries — typically 3 (code/test/security). */
|
|
158
|
+
reviewers: ReviewerEntry[];
|
|
159
|
+
/** Plugin version from `ai-sdlc-plugin/plugin.json`. */
|
|
160
|
+
pluginVersion: string;
|
|
161
|
+
/**
|
|
162
|
+
* Pipeline-cli version from `pipeline-cli/package.json` (RFC-0012 Phase 6 /
|
|
163
|
+
* AISDLC-100.6). Forensic / audit purpose only — the verifier logs this
|
|
164
|
+
* but does NOT enforce a specific version. Equivalent to AISDLC-87/AISDLC-94's
|
|
165
|
+
* `pluginVersion` field but for the `@ai-sdlc/pipeline-cli` workspace package.
|
|
166
|
+
*
|
|
167
|
+
* Optional in v1 for backward compatibility — envelopes signed BEFORE
|
|
168
|
+
* pipeline-cli existed (and BEFORE this field landed) carry no
|
|
169
|
+
* `pipelineVersion` and the verifier still accepts them, logging
|
|
170
|
+
* `<missing> (legacy envelope)` instead.
|
|
171
|
+
*/
|
|
172
|
+
pipelineVersion?: string;
|
|
173
|
+
/**
|
|
174
|
+
* Harness that produced the developer + reviewer verdicts (AISDLC-202.3).
|
|
175
|
+
* Populated by the calling adapter (e.g. `CodexHarnessAdapter` sets
|
|
176
|
+
* `{ name: 'codex', version: '0.128.0' }` when signing a Codex-run task).
|
|
177
|
+
* Claude Code paths omit this field or set `{ name: 'claude-code' }`.
|
|
178
|
+
*
|
|
179
|
+
* Optional for backward compatibility: envelopes produced before
|
|
180
|
+
* AISDLC-202.3 carry no `harness` field; the verifier accepts them and
|
|
181
|
+
* logs `<unknown>` when the field is absent. Downstream trust decisions
|
|
182
|
+
* (e.g. "require Codex review for Claude-developed PRs") can filter on
|
|
183
|
+
* `harness.name` without failing envelopes that predate this field.
|
|
184
|
+
*
|
|
185
|
+
* `name` is constrained to `SHORT_ID` (letters, digits, dot, dash,
|
|
186
|
+
* underscore) to prevent CR/LF injection into GITHUB_OUTPUT. `version`
|
|
187
|
+
* is constrained to `SEMVER` when present.
|
|
188
|
+
*/
|
|
189
|
+
harness?: {
|
|
190
|
+
name: string;
|
|
191
|
+
version?: string;
|
|
192
|
+
};
|
|
193
|
+
/** Iteration count — how many dev rounds the work went through. */
|
|
194
|
+
iterationCount: number;
|
|
195
|
+
/**
|
|
196
|
+
* Free-form harness note — empty string when independence was enforced,
|
|
197
|
+
* `'⚠ INDEPENDENCE NOT ENFORCED (codex unavailable, fell back to ...)'`
|
|
198
|
+
* when not. Surfaced in PR body so the reviewer-of-the-reviewer sees it.
|
|
199
|
+
*/
|
|
200
|
+
harnessNote: string;
|
|
201
|
+
/** ISO 8601 timestamp at signing. */
|
|
202
|
+
signedAt: string;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* DSSE envelope (https://github.com/secure-systems-lab/dsse).
|
|
206
|
+
*
|
|
207
|
+
* `payload` is base64-encoded JSON of the predicate. `signatures[]` lets us
|
|
208
|
+
* carry multi-sig if we ever need it (today: 1 signer = the dev who ran
|
|
209
|
+
* `/ai-sdlc execute`).
|
|
210
|
+
*/
|
|
211
|
+
export interface DsseEnvelope {
|
|
212
|
+
payloadType: typeof DSSE_PAYLOAD_TYPE;
|
|
213
|
+
/** base64-encoded JSON of the predicate. */
|
|
214
|
+
payload: string;
|
|
215
|
+
signatures: DsseSignature[];
|
|
216
|
+
}
|
|
217
|
+
export interface DsseSignature {
|
|
218
|
+
/**
|
|
219
|
+
* Identifier of the public key that produced this signature. Used to
|
|
220
|
+
* look up the trusted-reviewer entry. Free-form — typically `<identity>:
|
|
221
|
+
* <machine>` (e.g. `dominique@reliablegenius.io:laptop-2025`).
|
|
222
|
+
*/
|
|
223
|
+
keyid: string;
|
|
224
|
+
/** base64-encoded raw ed25519 signature (64 bytes → 88 chars b64). */
|
|
225
|
+
sig: string;
|
|
226
|
+
}
|
|
227
|
+
/** Trusted-reviewers.yaml entry shape. */
|
|
228
|
+
export interface TrustedReviewer {
|
|
229
|
+
/** Free-form contributor identifier (typically email or GitHub handle). */
|
|
230
|
+
identity: string;
|
|
231
|
+
/** Free-form machine label — lets one identity register multiple keys. */
|
|
232
|
+
machine: string;
|
|
233
|
+
/** PEM-encoded ed25519 public key. */
|
|
234
|
+
pubkey: string;
|
|
235
|
+
/** ISO 8601 date the entry was added. */
|
|
236
|
+
addedAt: string;
|
|
237
|
+
/** GitHub handle of the reviewer who approved the entry's PR. */
|
|
238
|
+
addedBy: string;
|
|
239
|
+
}
|
|
240
|
+
/** Result of verifying an attestation. */
|
|
241
|
+
export type VerifyResult = {
|
|
242
|
+
valid: true;
|
|
243
|
+
predicate: AttestationPredicate;
|
|
244
|
+
trustedReviewer: TrustedReviewer;
|
|
245
|
+
} | {
|
|
246
|
+
valid: false;
|
|
247
|
+
reason: string;
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Validate a parsed predicate against the v3 schema regex patterns.
|
|
251
|
+
*
|
|
252
|
+
* Returns `null` when the predicate is shape-valid; otherwise returns
|
|
253
|
+
* a static failure reason that does NOT embed any user-controlled
|
|
254
|
+
* value (just the field path). This is the load-bearing property:
|
|
255
|
+
* the malicious value never reaches the `reason` string, so it can't
|
|
256
|
+
* propagate to GITHUB_OUTPUT or commit-status descriptions.
|
|
257
|
+
*
|
|
258
|
+
* AISDLC-103 (Verifier Phase 3): `contentHashV3` is now required, and the
|
|
259
|
+
* legacy `diffHash` / `contentHash` fields are FORBIDDEN — a predicate
|
|
260
|
+
* carrying either is treated as a v1/v2 envelope smuggling itself into the
|
|
261
|
+
* v3 window and rejected with a static reason.
|
|
262
|
+
*/
|
|
263
|
+
export declare function validatePredicateShape(parsed: unknown): string | null;
|
|
264
|
+
/**
|
|
265
|
+
* The set of reviewer agent IDs the verifier expects to see in every
|
|
266
|
+
* attestation. Exported so callers (verify-attestation.mjs) can
|
|
267
|
+
* cross-check that all three reviewers are present + match.
|
|
268
|
+
*
|
|
269
|
+
* Frozen to discourage callers from mutating it.
|
|
270
|
+
*/
|
|
271
|
+
export declare const REQUIRED_REVIEWER_AGENT_IDS: readonly string[];
|
|
272
|
+
/**
|
|
273
|
+
* Name-equivalence map for the reviewer-set completeness check (AISDLC-252).
|
|
274
|
+
*
|
|
275
|
+
* A "role" is satisfied when any of the listed agentIds is present in the
|
|
276
|
+
* envelope's reviewer set. This lets codex-harness variants (`code-reviewer-codex`,
|
|
277
|
+
* `test-reviewer-codex`) satisfy the same role as their Claude counterparts,
|
|
278
|
+
* enabling the bidirectional cross-harness review goal without requiring a
|
|
279
|
+
* redundant Claude review on Codex-reviewed PRs.
|
|
280
|
+
*
|
|
281
|
+
* Security stays Claude-only: `security-reviewer` has no codex variant per
|
|
282
|
+
* `feedback_subagent_model_selection.md` (Claude Opus for security reasoning
|
|
283
|
+
* depth is not yet validated for Codex o4-mini).
|
|
284
|
+
*
|
|
285
|
+
* The map is keyed by role name (= the canonical agentId), each value is the
|
|
286
|
+
* set of ALL agentIds that satisfy the role (including the canonical one).
|
|
287
|
+
*
|
|
288
|
+
* Frozen to discourage callers from mutating it.
|
|
289
|
+
*/
|
|
290
|
+
export declare const REVIEWER_ROLE_EQUIVALENCES: Readonly<Record<string, readonly string[]>>;
|
|
291
|
+
/**
|
|
292
|
+
* When the implementer ran in Codex (`predicate.harness.name === 'codex'`),
|
|
293
|
+
* these reviewer roles MUST be satisfied by a reviewer whose `harness` field
|
|
294
|
+
* differs from `codex`. Per RFC-0010 §13.10 `requiresIndependentHarnessFrom`:
|
|
295
|
+
* code and test reviewers must come from a different harness than the
|
|
296
|
+
* implementer to preserve cross-harness independence.
|
|
297
|
+
*
|
|
298
|
+
* Security is excluded — it is always Claude-only regardless.
|
|
299
|
+
*
|
|
300
|
+
* Frozen to discourage callers from mutating it.
|
|
301
|
+
*/
|
|
302
|
+
export declare const INDEPENDENCE_REQUIRED_ROLES: readonly string[];
|
|
303
|
+
/**
|
|
304
|
+
* One entry in the changed-file set used to compute `contentHash`
|
|
305
|
+
* (AISDLC-94). `path` is the repo-relative forward-slash path; `blobSha`
|
|
306
|
+
* is the git blob SHA-1 (40 lowercase hex chars) of the file's CURRENT
|
|
307
|
+
* post-apply content at the attested commit.
|
|
308
|
+
*
|
|
309
|
+
* For deleted files, set `blobSha` to the empty string — the canonical
|
|
310
|
+
* line still includes the path so a delete-vs-keep difference between
|
|
311
|
+
* two PRs produces different hashes.
|
|
312
|
+
*/
|
|
313
|
+
export interface ChangedFileEntry {
|
|
314
|
+
path: string;
|
|
315
|
+
blobSha: string;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* One entry in the per-file-delta set used to compute `contentHashV3`
|
|
319
|
+
* (AISDLC-101). `path` is the repo-relative forward-slash path;
|
|
320
|
+
* `baseBlobSha` is the git blob SHA-1 of the file at the merge-base of
|
|
321
|
+
* `<baseRef>` and `<headRef>` (= the file's content BEFORE the PR's
|
|
322
|
+
* commits replayed); `headBlobSha` is the git blob SHA-1 of the file at
|
|
323
|
+
* `<headRef>` (= AFTER the PR's commits).
|
|
324
|
+
*
|
|
325
|
+
* For files that don't exist at one of the endpoints (newly added or
|
|
326
|
+
* deleted), the corresponding `*BlobSha` is the empty string. The
|
|
327
|
+
* canonical line still includes the path so:
|
|
328
|
+
* - "added file" (`base=''`, `head=<sha>`) → distinct from "kept file"
|
|
329
|
+
* (`base=<old>`, `head=<new>`)
|
|
330
|
+
* - "deleted file" (`base=<old>`, `head=''`) → distinct from "added file"
|
|
331
|
+
*/
|
|
332
|
+
export interface ChangedFileDeltaEntry {
|
|
333
|
+
path: string;
|
|
334
|
+
baseBlobSha: string;
|
|
335
|
+
headBlobSha: string;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Regex matching the envelope self-exclusion path pattern
|
|
339
|
+
* `.ai-sdlc/attestations/<sha>.dsse.json`. Used to filter out the
|
|
340
|
+
* envelope file itself from the file collector for AISDLC-193.1
|
|
341
|
+
* `contentHashV4` and AISDLC-101 `contentHashV3` purposes.
|
|
342
|
+
*
|
|
343
|
+
* The chore-commit pattern signs the predicate at the dev-commit (HEAD
|
|
344
|
+
* BEFORE the envelope file exists), then the chore commit on top adds
|
|
345
|
+
* the envelope file at `.ai-sdlc/attestations/<sha>.dsse.json`. If the
|
|
346
|
+
* collector includes the envelope file in the hashed file set, the
|
|
347
|
+
* verifier (which runs against PR HEAD = dev-commit + chore commit)
|
|
348
|
+
* will see an EXTRA entry for the envelope that the signer never saw
|
|
349
|
+
* → mismatch even on direct PR HEAD without any rebase.
|
|
350
|
+
*
|
|
351
|
+
* The exclusion applies to the file COLLECTOR for HASHING purposes
|
|
352
|
+
* only. The verifier's chore-commit allowlist (`scripts/verify-attestation.mjs`
|
|
353
|
+
* `CHORE_COMMIT_PATH_ALLOWLIST`) STILL allows the envelope file in the
|
|
354
|
+
* chore commit's diff — that's a separate concern from "what is in the
|
|
355
|
+
* file set we hash."
|
|
356
|
+
*
|
|
357
|
+
* Anchored with `^...$` against the forward-slash-normalized path so
|
|
358
|
+
* an attacker cannot bypass with `./.ai-sdlc/attestations/x.dsse.json`
|
|
359
|
+
* or `foo/.ai-sdlc/attestations/x.dsse.json`. Note that git's
|
|
360
|
+
* `--name-only` always emits paths relative to the repo root with
|
|
361
|
+
* forward slashes, so the match is straightforward in practice.
|
|
362
|
+
*/
|
|
363
|
+
export declare const ATTESTATION_ENVELOPE_PATH_PATTERN: RegExp;
|
|
364
|
+
/**
|
|
365
|
+
* Predicate to determine whether a file path identifies an attestation
|
|
366
|
+
* envelope and should therefore be excluded from `contentHashV3` /
|
|
367
|
+
* `contentHashV4` file enumeration. Defensive about backslash
|
|
368
|
+
* normalization (Windows callers).
|
|
369
|
+
*/
|
|
370
|
+
export declare function isAttestationEnvelopePath(path: string): boolean;
|
|
371
|
+
/**
|
|
372
|
+
* The "shared churn" exclude list for `contentHashV4` (AISDLC-258).
|
|
373
|
+
*
|
|
374
|
+
* Files in this list are EXCLUDED from the v4 file collector in BOTH the
|
|
375
|
+
* signer (`collectChangedFileDeltaEntries`) and the verifier
|
|
376
|
+
* (`computeHeadContentHashV4` in `scripts/verify-attestation.mjs`). When
|
|
377
|
+
* a file appears in this list, changes to it after signing (e.g. from a
|
|
378
|
+
* merge-queue rebase that regenerated `pnpm-lock.yaml`) do NOT cause
|
|
379
|
+
* `contentHashV4` to mismatch, so the operator is never asked to re-sign
|
|
380
|
+
* just because a shared tooling file was regenerated automatically.
|
|
381
|
+
*
|
|
382
|
+
* **Security trade-off (operator-approved, 2026-05-10):** An attacker
|
|
383
|
+
* COULD slip malicious changes through these files undetected (the
|
|
384
|
+
* attestation would still pass even if the ignore-listed file was
|
|
385
|
+
* tampered). The operator accepted this risk because:
|
|
386
|
+
* - None of these files contain reviewable hand-written code.
|
|
387
|
+
* - `pnpm-lock.yaml` is generated from `package.json` (which IS hashed).
|
|
388
|
+
* - `CHANGELOG.md` variants are auto-generated by release-please from
|
|
389
|
+
* commit history (which IS hashed via the commit-level binding).
|
|
390
|
+
* - `generated-schemas.ts` is generated from spec schemas (reviewed
|
|
391
|
+
* separately in the spec/ PR that changed them).
|
|
392
|
+
*
|
|
393
|
+
* **DO NOT add to this list:** `package.json` (real dep changes are
|
|
394
|
+
* reviewable), source files, test files, configs, RFCs, or anything a
|
|
395
|
+
* human writes by hand. The list is intentionally narrow.
|
|
396
|
+
*
|
|
397
|
+
* Paths are exact matches against the forward-slash-normalized repo-relative
|
|
398
|
+
* path emitted by `git diff --name-only`. Patterns (globs/regex) are NOT
|
|
399
|
+
* supported to keep the list auditable — every entry must be exact.
|
|
400
|
+
*
|
|
401
|
+
* Exported so `scripts/verify-attestation.mjs` can import it from the
|
|
402
|
+
* orchestrator barrel and apply the same exclusions on the verifier side.
|
|
403
|
+
*/
|
|
404
|
+
export declare const CONTENTHASHV4_IGNORE_FILES: readonly string[];
|
|
405
|
+
/**
|
|
406
|
+
* Predicate to determine whether a file path should be excluded from the
|
|
407
|
+
* `contentHashV4` computation because it is a "shared churn" file (see
|
|
408
|
+
* `CONTENTHASHV4_IGNORE_FILES`). Defensive about backslash normalization.
|
|
409
|
+
*
|
|
410
|
+
* Note: this predicate is intentionally separate from
|
|
411
|
+
* `isAttestationEnvelopePath` because the two exclusions serve different
|
|
412
|
+
* purposes and may diverge independently. Merge them only if the list
|
|
413
|
+
* becomes large enough to warrant a single unified predicate.
|
|
414
|
+
*/
|
|
415
|
+
export declare function isIgnoredForContentHash(path: string): boolean;
|
|
416
|
+
/**
|
|
417
|
+
* One entry in the base-independent per-file head-blob set used to
|
|
418
|
+
* compute `contentHashV4` (AISDLC-193.1). Identical in shape to
|
|
419
|
+
* `ChangedFileEntry` (AISDLC-94's `contentHash`) but with `headBlobSha`
|
|
420
|
+
* naming for clarity — v4 binds reviewers' approval to "I approved
|
|
421
|
+
* THESE files at THESE specific head blobs," nothing about the base.
|
|
422
|
+
*
|
|
423
|
+
* Why v4 was added on top of v3:
|
|
424
|
+
* - v3 binds the (base_blob, head_blob) PAIR per file. When the
|
|
425
|
+
* merge queue rebases the PR onto a sibling-merged main, the base
|
|
426
|
+
* blob SHA for shared files changes (the merge-base shifts forward
|
|
427
|
+
* to include the sibling's contributions). The fileDeltaHash flips
|
|
428
|
+
* even though the post-apply file content the reviewers approved
|
|
429
|
+
* hasn't moved → contentHashV3 invalidates → required check fails
|
|
430
|
+
* → queue rejects the PR. Net: every queued code-touching PR
|
|
431
|
+
* deadlocks at the gate.
|
|
432
|
+
* - v4 binds only `{path, headBlobSha}`. The head blob SHA captures
|
|
433
|
+
* "this is the EXACT file content the reviewers signed off on."
|
|
434
|
+
* Whatever the rebase does to the base ref, as long as the head
|
|
435
|
+
* blob SHAs are unchanged, the v4 hash matches.
|
|
436
|
+
* - Threat model preserved: any genuine post-sign content tampering
|
|
437
|
+
* (someone amends the PR to add unreviewed code) flips the head
|
|
438
|
+
* blob SHA → v4 hash flips → verifier rejects.
|
|
439
|
+
*/
|
|
440
|
+
export interface ChangedFileHeadEntry {
|
|
441
|
+
path: string;
|
|
442
|
+
headBlobSha: string;
|
|
443
|
+
}
|
|
444
|
+
/** Inputs for building an attestation predicate. */
|
|
445
|
+
export interface BuildPredicateInputs {
|
|
446
|
+
commitSha: string;
|
|
447
|
+
policy: string | Buffer;
|
|
448
|
+
reviewers: Array<{
|
|
449
|
+
agentId: string;
|
|
450
|
+
agentFileContent: string | Buffer;
|
|
451
|
+
harness: string;
|
|
452
|
+
approved: boolean;
|
|
453
|
+
findings: ReviewerEntry['findings'];
|
|
454
|
+
}>;
|
|
455
|
+
pluginVersion: string;
|
|
456
|
+
/**
|
|
457
|
+
* Pipeline-cli version from `pipeline-cli/package.json` (AISDLC-100.6).
|
|
458
|
+
* Optional — when omitted (e.g. legacy callers, environments where
|
|
459
|
+
* pipeline-cli isn't installed), the predicate's `pipelineVersion` field
|
|
460
|
+
* is also omitted. Forensic / audit purpose only — the verifier logs
|
|
461
|
+
* this but does not enforce.
|
|
462
|
+
*/
|
|
463
|
+
pipelineVersion?: string;
|
|
464
|
+
/**
|
|
465
|
+
* Harness that produced the developer + reviewer verdicts (AISDLC-202.3).
|
|
466
|
+
* Optional — when omitted, the predicate carries no `harness` field
|
|
467
|
+
* (back-compat with pre-202.3 envelopes). When provided, the adapter
|
|
468
|
+
* populates both `name` (required, SHORT_ID) and optionally `version`
|
|
469
|
+
* (SEMVER). Example: `{ name: 'codex', version: '0.128.0' }`.
|
|
470
|
+
*
|
|
471
|
+
* The signing script (`sign-attestation.mjs`) passes this via
|
|
472
|
+
* `--harness-name` + `--harness-version` CLI flags.
|
|
473
|
+
*/
|
|
474
|
+
harness?: {
|
|
475
|
+
name: string;
|
|
476
|
+
version?: string;
|
|
477
|
+
};
|
|
478
|
+
iterationCount: number;
|
|
479
|
+
harnessNote: string;
|
|
480
|
+
/** Override `signedAt` for deterministic tests. */
|
|
481
|
+
signedAt?: string;
|
|
482
|
+
/**
|
|
483
|
+
* Per-file-delta set for `contentHashV3` (AISDLC-101 / AISDLC-103).
|
|
484
|
+
* REQUIRED for v3 envelopes — captures the (base_blob_sha →
|
|
485
|
+
* head_blob_sha) transition per file. Pass `[]` for no-op PRs (the
|
|
486
|
+
* resulting `contentHashV3` is `sha256('')`, which is well-defined and
|
|
487
|
+
* still verifiable).
|
|
488
|
+
*/
|
|
489
|
+
changedFileDeltas: ChangedFileDeltaEntry[];
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Compute a sha256 hex digest. Single source of truth for the hashing
|
|
493
|
+
* algorithm — every predicate field that ends in `Hash` flows through here.
|
|
494
|
+
*/
|
|
495
|
+
export declare function sha256Hex(input: string | Buffer): string;
|
|
496
|
+
/** Compute a sha1 hex digest (used for git commit SHAs in the subject). */
|
|
497
|
+
export declare function sha1Hex(input: string | Buffer): string;
|
|
498
|
+
/**
|
|
499
|
+
* Compute the rebase-tolerant `contentHash` (AISDLC-94) over a changed-file
|
|
500
|
+
* set. The canonical encoding is one line per entry, sorted ascending by
|
|
501
|
+
* path, with `<path>\t<blobSha>\n` per line. The whole string is sha256-ed.
|
|
502
|
+
*
|
|
503
|
+
* Why this beats `diffHash`:
|
|
504
|
+
* - Rebasing PR-X onto a new `main` that already touched the same files
|
|
505
|
+
* does NOT change the post-apply blob SHAs (assuming no conflict),
|
|
506
|
+
* so `contentHash` stays stable across the rebase.
|
|
507
|
+
* - A conflict resolution that picks different content WILL change the
|
|
508
|
+
* blob SHA → `contentHash` changes → attestation correctly invalidated.
|
|
509
|
+
* - Force-pushing a no-op edit (e.g. `git commit --amend --no-edit`) keeps
|
|
510
|
+
* blob SHAs identical → `contentHash` stays stable.
|
|
511
|
+
*
|
|
512
|
+
* The deduplication step makes the function idempotent if a caller
|
|
513
|
+
* accidentally passes the same path twice (last-write-wins per path).
|
|
514
|
+
*
|
|
515
|
+
* Pure function. The caller (sign-attestation script) is responsible for
|
|
516
|
+
* gathering the file set (via `git diff --name-only` + `git ls-tree`).
|
|
517
|
+
*/
|
|
518
|
+
export declare function computeContentHash(entries: ChangedFileEntry[]): string;
|
|
519
|
+
/**
|
|
520
|
+
* Optional injection points for `collectChangedFileEntries`. Defaults are
|
|
521
|
+
* production behaviour; tests pass synthetic `runGit` to avoid spawning git.
|
|
522
|
+
*/
|
|
523
|
+
export interface CollectChangedFileEntriesOptions {
|
|
524
|
+
/**
|
|
525
|
+
* Run `git <args>` in `cwd` and return stdout (utf-8). Defaults to
|
|
526
|
+
* `execFileSync` with the git-context env scrubbed (see `cleanGitEnv`).
|
|
527
|
+
* Tests pass a stub so they don't depend on a real worktree.
|
|
528
|
+
*/
|
|
529
|
+
runGit?: (args: string[], cwd: string) => string;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Collect the changed-file set used to compute `contentHash` (AISDLC-94).
|
|
533
|
+
*
|
|
534
|
+
* Returns one `{ path, blobSha }` entry per file in
|
|
535
|
+
* `git diff --name-only <baseRef>...<headRef>` with the blob SHA from
|
|
536
|
+
* `git ls-tree -r <headRef> -- <path>`. Deleted files get an empty
|
|
537
|
+
* `blobSha` (the path still appears so the canonical encoding distinguishes
|
|
538
|
+
* "deleted" from "kept").
|
|
539
|
+
*
|
|
540
|
+
* `--no-renames` so a rename shows up as add+delete (= two entries) — that
|
|
541
|
+
* way a rebase that resolved a conflict by renaming differently produces a
|
|
542
|
+
* different hash. `-c core.quotepath=false` mirrors the verifier's git
|
|
543
|
+
* helper so unicode paths come back as raw UTF-8.
|
|
544
|
+
*
|
|
545
|
+
* Path entries containing `\t` or `\n` are rejected to keep the canonical
|
|
546
|
+
* encoding injective (mirrors the rejection in `computeContentHash`). Such
|
|
547
|
+
* paths are exceedingly rare in practice — git's default config disallows
|
|
548
|
+
* `\n` in tracked filenames on most platforms — but we defend in depth so
|
|
549
|
+
* malicious or pathological inputs can't smuggle entries past the binding.
|
|
550
|
+
*
|
|
551
|
+
* Extracted from the previously-duplicated helpers in
|
|
552
|
+
* `ai-sdlc-plugin/scripts/sign-attestation.mjs` so a single source of truth
|
|
553
|
+
* applies the same parsing + validation at every signing site.
|
|
554
|
+
*/
|
|
555
|
+
export declare function collectChangedFileEntries(baseRef: string, headRef: string, repoRoot: string, options?: CollectChangedFileEntriesOptions): ChangedFileEntry[];
|
|
556
|
+
/**
|
|
557
|
+
* Compute the per-file-delta `contentHashV3` (AISDLC-101) over a set of
|
|
558
|
+
* `{path, baseBlobSha, headBlobSha}` triples. The canonical encoding is
|
|
559
|
+
* one line per entry, sorted ascending by path, with
|
|
560
|
+
* `<path>\t<fileDeltaHash>\n` per line, where
|
|
561
|
+
* `fileDeltaHash = sha256(baseBlobSha + ' -> ' + headBlobSha)`. The
|
|
562
|
+
* outer `contentHashV3` is the sha256 of the concatenated lines.
|
|
563
|
+
*
|
|
564
|
+
* Why per-file delta hashing — and what it adds vs. AISDLC-94's `contentHash`:
|
|
565
|
+
* - `contentHash` (AISDLC-94) hashes the post-apply blob SHA per file.
|
|
566
|
+
* If a sibling PR landed between OUR sign + OUR merge AND modified
|
|
567
|
+
* the SAME file, the rebased file's HEAD blob SHA contains both the
|
|
568
|
+
* sibling contribution AND ours → contentHash diverges (false reject).
|
|
569
|
+
* - `contentHashV3` (AISDLC-101) hashes the (base, head) blob-pair
|
|
570
|
+
* transition per file. Provides a stricter "we moved file F from blob
|
|
571
|
+
* A to blob B" binding than just "we ended up at blob B". Any genuine
|
|
572
|
+
* content change still flips the head blob SHA → fileDeltaHash flips
|
|
573
|
+
* → contentHashV3 flips → reject (threat model preserved).
|
|
574
|
+
*
|
|
575
|
+
* This is the SECOND line of defense in the 3-layer rebase-tolerance
|
|
576
|
+
* plan (AISDLC-94 = Phase 1 verifier-side dual-hash, AISDLC-102 = Phase 1.5
|
|
577
|
+
* producer-side pre-sign rebase, AISDLC-101 = Phase 2 per-file delta).
|
|
578
|
+
* The verifier OR's all three legs during the triple-hash window.
|
|
579
|
+
*
|
|
580
|
+
* Path-delimiter rejection (\t / \n) mirrors `computeContentHash` so the
|
|
581
|
+
* canonical encoding stays injective regardless of caller input.
|
|
582
|
+
*
|
|
583
|
+
* Pure function. Idempotent against double-enumeration via dedup-by-path
|
|
584
|
+
* (last-write-wins per path), same as `computeContentHash`.
|
|
585
|
+
*/
|
|
586
|
+
export declare function computeContentHashV3(entries: ChangedFileDeltaEntry[]): string;
|
|
587
|
+
/**
|
|
588
|
+
* Compute the BASE-INDEPENDENT per-file head-blob `contentHashV4`
|
|
589
|
+
* (AISDLC-193.1) over a set of `{path, headBlobSha}` pairs. The
|
|
590
|
+
* canonical encoding is `JSON.stringify(sorted-by-path-array-of-{path,
|
|
591
|
+
* headBlobSha}-objects)`, hashed with sha256.
|
|
592
|
+
*
|
|
593
|
+
* Why JSON-of-sorted-array (and not the v3 `<path>\t<fileDeltaHash>\n`
|
|
594
|
+
* canonical) for v4:
|
|
595
|
+
* - JSON's quoting rules already cover delimiter injection
|
|
596
|
+
* (a malicious path containing tab/newline can't smuggle through
|
|
597
|
+
* because they round-trip as escape sequences). We still reject
|
|
598
|
+
* such paths defensively so the canonical stays injective and
|
|
599
|
+
* the on-the-wire representation is what readers expect.
|
|
600
|
+
* - JSON is unambiguous about field ordering (stringify of a
|
|
601
|
+
* `{path, headBlobSha}` literal always emits `path` first,
|
|
602
|
+
* `headBlobSha` second — V8's object-key ordering is insertion
|
|
603
|
+
* order, and we insert in this order in the .map() below).
|
|
604
|
+
* - Easier to extend: future hash versions can add fields
|
|
605
|
+
* (`mode`, `executable bit`, etc) to the entry objects without
|
|
606
|
+
* breaking the canonical encoding scheme.
|
|
607
|
+
*
|
|
608
|
+
* Why this is BASE-INDEPENDENT (= the whole point):
|
|
609
|
+
* - v3's per-file delta hashes the (base_blob, head_blob) pair.
|
|
610
|
+
* When the merge queue rebases the PR onto current main (which
|
|
611
|
+
* advanced past the merge-base the producer signed against), the
|
|
612
|
+
* base blob SHA for any shared file changes → v3 invalidates.
|
|
613
|
+
* - v4 hashes only `{path, headBlobSha}`. Whatever the rebase does
|
|
614
|
+
* to the base ref or the merge-base, as long as the head blob SHA
|
|
615
|
+
* (= the actual reviewed file content) is unchanged, v4 matches.
|
|
616
|
+
* - The reviewer never approved "base_blob X → head_blob Y"; they
|
|
617
|
+
* approved "the file contents at head_blob Y." v4 binds to that
|
|
618
|
+
* directly.
|
|
619
|
+
*
|
|
620
|
+
* Threat model preserved:
|
|
621
|
+
* - Genuine post-sign content tampering (someone amends the PR to
|
|
622
|
+
* add unreviewed code) flips the head blob SHA → v4 hash flips →
|
|
623
|
+
* verifier rejects. Same threat-model surface as v3.
|
|
624
|
+
* - The signing key still has to be a trusted reviewer's; v4
|
|
625
|
+
* doesn't change the signature/key flow, just what the predicate
|
|
626
|
+
* binds to.
|
|
627
|
+
*
|
|
628
|
+
* Pure function. Idempotent against double-enumeration via dedup-by-path
|
|
629
|
+
* (last-write-wins per path), same as `computeContentHash` and
|
|
630
|
+
* `computeContentHashV3`.
|
|
631
|
+
*/
|
|
632
|
+
export declare function computeContentHashV4(entries: ChangedFileHeadEntry[]): string;
|
|
633
|
+
/**
|
|
634
|
+
* Collect the per-file-delta set used to compute `contentHashV3` (AISDLC-101).
|
|
635
|
+
*
|
|
636
|
+
* Returns one `{ path, baseBlobSha, headBlobSha }` entry per file in
|
|
637
|
+
* `git diff --name-only <baseRef>...<headRef>`. The base blob SHA is read
|
|
638
|
+
* from the *merge-base* of `<baseRef>` and `<headRef>` (which the `...`
|
|
639
|
+
* 3-dot diff range already targets — `A...B` diffs against
|
|
640
|
+
* `merge-base(A,B)`); the head blob SHA from `<headRef>`. Files newly
|
|
641
|
+
* added in the PR have empty `baseBlobSha`; deleted files have empty
|
|
642
|
+
* `headBlobSha`.
|
|
643
|
+
*
|
|
644
|
+
* Mirrors `collectChangedFileEntries`'s flag set (`--no-renames`,
|
|
645
|
+
* `core.quotepath=false`) for consistency with the other binding's file
|
|
646
|
+
* enumeration.
|
|
647
|
+
*
|
|
648
|
+
* Extracted so a single source of truth handles the two ls-tree lookups
|
|
649
|
+
* (one per endpoint) at every signing site (`sign-attestation.mjs`).
|
|
650
|
+
*/
|
|
651
|
+
export declare function collectChangedFileDeltaEntries(baseRef: string, headRef: string, repoRoot: string, options?: CollectChangedFileEntriesOptions): ChangedFileDeltaEntry[];
|
|
652
|
+
/**
|
|
653
|
+
* Project a v3 `ChangedFileDeltaEntry` set down to the v4
|
|
654
|
+
* `ChangedFileHeadEntry` shape (`{path, headBlobSha}`). Convenience
|
|
655
|
+
* for callers that already collected v3 deltas and want to dual-emit
|
|
656
|
+
* both hashes from the same file enumeration. Pure function.
|
|
657
|
+
*
|
|
658
|
+
* The envelope self-exclusion is enforced upstream by
|
|
659
|
+
* `collectChangedFileDeltaEntries`, so this projection is a simple
|
|
660
|
+
* field-pick — no path filtering needed here.
|
|
661
|
+
*/
|
|
662
|
+
export declare function projectDeltaEntriesToHeadEntries(deltas: ChangedFileDeltaEntry[]): ChangedFileHeadEntry[];
|
|
663
|
+
/**
|
|
664
|
+
* Build the predicate payload from raw inputs. Pure function — no I/O,
|
|
665
|
+
* no signing. The caller (`/ai-sdlc execute` Step 10) reads files and git
|
|
666
|
+
* output, then hands them here.
|
|
667
|
+
*
|
|
668
|
+
* AISDLC-103 (Verifier Phase 3): always emits a v3 envelope. The caller
|
|
669
|
+
* MUST provide `changedFileDeltas` (use `[]` for no-op PRs); the legacy
|
|
670
|
+
* `diff` + `changedFiles` inputs were dropped along with the legacy
|
|
671
|
+
* `diffHash` + `contentHash` fields.
|
|
672
|
+
*/
|
|
673
|
+
export declare function buildPredicate(inputs: BuildPredicateInputs): AttestationPredicate;
|
|
674
|
+
/**
|
|
675
|
+
* DSSE Pre-Authentication Encoding. Per the spec
|
|
676
|
+
* (https://github.com/secure-systems-lab/dsse/blob/master/protocol.md):
|
|
677
|
+
*
|
|
678
|
+
* PAE(type, body) = "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body
|
|
679
|
+
*
|
|
680
|
+
* Lengths are decimal ASCII byte-counts of the UTF-8 encoding. Signing the
|
|
681
|
+
* PAE — not the raw payload — is what gives DSSE its domain separation:
|
|
682
|
+
* a signature over a payload of one `payloadType` cannot be replayed onto
|
|
683
|
+
* a payload of a different type.
|
|
684
|
+
*/
|
|
685
|
+
export declare function paeEncode(payloadType: string, payload: Buffer): Buffer;
|
|
686
|
+
/** Generate a fresh ed25519 keypair as PEM strings (for `/ai-sdlc init-signing-key`). */
|
|
687
|
+
export declare function generateSigningKeyPair(): {
|
|
688
|
+
privateKeyPem: string;
|
|
689
|
+
publicKeyPem: string;
|
|
690
|
+
};
|
|
691
|
+
/** Sign options. `keyid` is required — verifiers use it to look up the pubkey. */
|
|
692
|
+
export interface SignOptions {
|
|
693
|
+
predicate: AttestationPredicate;
|
|
694
|
+
privateKeyPem: string;
|
|
695
|
+
keyid: string;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Sign a predicate, producing a DSSE envelope.
|
|
699
|
+
*
|
|
700
|
+
* Throws if `predicate.schemaVersion` is not in `ACCEPTED_SCHEMA_VERSIONS` —
|
|
701
|
+
* we don't want to issue an envelope that we'd reject ourselves.
|
|
702
|
+
*/
|
|
703
|
+
export declare function signAttestation(opts: SignOptions): DsseEnvelope;
|
|
704
|
+
/** Verify options. `expected` lets the caller bind verification to a specific PR state. */
|
|
705
|
+
export interface VerifyOptions {
|
|
706
|
+
envelope: DsseEnvelope;
|
|
707
|
+
/**
|
|
708
|
+
* Trusted reviewers from `.ai-sdlc/trusted-reviewers.yaml`. The verifier
|
|
709
|
+
* tries each pubkey against each signature ("any-of-N") and accepts on
|
|
710
|
+
* the first match.
|
|
711
|
+
*/
|
|
712
|
+
trustedReviewers: TrustedReviewer[];
|
|
713
|
+
/**
|
|
714
|
+
* What the predicate's `subject.digest.sha1`, `contentHashV3`,
|
|
715
|
+
* `policyHash`, and `reviewers[].agentFileHash` MUST equal. Mismatch =
|
|
716
|
+
* invalid.
|
|
717
|
+
*
|
|
718
|
+
* `expectedAgentFileHashes` is a map from agentId to its sha256 — we
|
|
719
|
+
* tolerate the predicate listing fewer or more reviewers than the map,
|
|
720
|
+
* but every reviewer entry whose agentId IS in the map must hash-match.
|
|
721
|
+
*/
|
|
722
|
+
expected: {
|
|
723
|
+
commitSha: string;
|
|
724
|
+
contentHashV3: string;
|
|
725
|
+
/**
|
|
726
|
+
* AISDLC-193.1 base-independent per-file head-blob binding.
|
|
727
|
+
* Optional — callers that want v4-prefer behavior pass it; callers
|
|
728
|
+
* that only have v3 (legacy) leave it undefined.
|
|
729
|
+
*
|
|
730
|
+
* When BOTH this AND the envelope's `contentHashV4` are present,
|
|
731
|
+
* the verifier prefers v4 (base-independent → survives queue
|
|
732
|
+
* rebases). When v4 matches, v3 is NOT consulted (this is the
|
|
733
|
+
* whole point — v3 will mismatch on a queue rebase even though
|
|
734
|
+
* the reviewed content is unchanged).
|
|
735
|
+
*
|
|
736
|
+
* When the envelope is legacy v3-only (no `contentHashV4`), the
|
|
737
|
+
* verifier falls back to the v3 check unconditionally regardless
|
|
738
|
+
* of whether `expected.contentHashV4` is supplied.
|
|
739
|
+
*/
|
|
740
|
+
contentHashV4?: string;
|
|
741
|
+
policyHash: string;
|
|
742
|
+
expectedAgentFileHashes: Record<string, string>;
|
|
743
|
+
};
|
|
744
|
+
/**
|
|
745
|
+
* Override the accepted-schema-versions allowlist (for tests). Defaults
|
|
746
|
+
* to `ACCEPTED_SCHEMA_VERSIONS`.
|
|
747
|
+
*/
|
|
748
|
+
acceptedSchemaVersions?: readonly string[];
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Verify a DSSE envelope. Returns a discriminated union — `{ valid: true }`
|
|
752
|
+
* with the parsed predicate + matched trusted reviewer, or `{ valid: false }`
|
|
753
|
+
* with a single human-readable reason string.
|
|
754
|
+
*
|
|
755
|
+
* The reason string is what gets posted to the commit status
|
|
756
|
+
* (`ai-sdlc/attestation: invalid (<reason>)`), so keep it short and specific.
|
|
757
|
+
*/
|
|
758
|
+
export declare function verifyAttestation(opts: VerifyOptions): VerifyResult;
|
|
759
|
+
/**
|
|
760
|
+
* Validate the shape of a parsed `.ai-sdlc/trusted-reviewers.yaml` document.
|
|
761
|
+
* Throws on malformed input with a specific reason. Acceptance criterion #4.
|
|
762
|
+
*
|
|
763
|
+
* Accepts the parsed YAML (as `unknown`) and returns the typed array.
|
|
764
|
+
*/
|
|
765
|
+
export declare function validateTrustedReviewers(parsed: unknown): TrustedReviewer[];
|
|
766
|
+
//# sourceMappingURL=attestations.d.ts.map
|