Skip to content

Commit 2c7791b

Browse files
committed
feat: repair assurance roots of trust
1 parent 3f353cf commit 2c7791b

17 files changed

Lines changed: 421 additions & 33 deletions

README.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,15 @@ Or supply an immutable released revision explicitly with `--revision <40-hex-sha
3434

3535
```bash
3636
engineering-process classify \
37-
--sha "$GITHUB_SHA" \
38-
--path src/payments/refund.py \
37+
--base "$BASE_SHA" --head "$GITHUB_SHA" \
3938
--declared '{"observable_behavior":true}'
4039

4140
engineering-process evaluate \
42-
--sha "$GITHUB_SHA" \
43-
--path src/payments/refund.py \
41+
--base "$BASE_SHA" --head "$GITHUB_SHA" \
4442
--declared '{"observable_behavior":true}'
4543

4644
engineering-process explain \
47-
--sha "$GITHUB_SHA" \
48-
--path src/payments/refund.py \
45+
--base "$BASE_SHA" --head "$GITHUB_SHA" \
4946
--declared '{"observable_behavior":true}'
5047

5148
engineering-process attest --predicate test-result/v1 \
@@ -58,7 +55,7 @@ engineering-process validate
5855
engineering-process metrics --obligations .engineering/effective-obligations.json
5956
```
6057

61-
`classify` is monotonic: declarations may strengthen routing but may not defeat deterministic Protected signals. Unknown Protected characteristics fail closed. `readiness` rejects malformed, failed, contradictory, wrong-SHA, wrong-process-revision, and invalid fresh-context review evidence.
58+
Trusted classification derives the complete Git change set from exact `--base` and `--head` commits, including deletions and both rename paths. `--path` is untrusted and diagnostic-only. Classification is monotonic: declarations may strengthen routing but may not defeat deterministic Protected signals. Unknown Protected characteristics fail closed. `readiness` rejects malformed, failed, contradictory, wrong-repository, wrong-SHA, wrong-process-revision, unauthorized-producer, and invalid fresh-context review evidence. See the authoritative [evidence lifecycle](docs/evidence-lifecycle.md).
6259

6360
## Policy model
6461

docs/adoption.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@
99

1010
Repository-local technology is deliberately opaque to policy. Put commands under `overrides.validation.commands`; add sensitive paths/characteristics and domain triggers when they strengthen classification. Never copy the canonical policy into the manifest.
1111

12+
## Locked process upgrades
13+
14+
CI executes the immutable Git SHA in `.engineering/process.lock`, never a floating
15+
package, tag, or branch. Change it only through a reviewed
16+
`engineering-process upgrade --version VERSION --revision 40_HEX_SHA`. The
17+
upgrade refreshes the manifest, lock, generated workflow, and policy snapshot
18+
together. Missing, unavailable, floating, or inconsistent revisions fail closed.
19+
The initial checkout and runner are the bootstrap trust boundary. Evidence
20+
transport and v1 compatibility are defined in the [exact-revision evidence
21+
lifecycle](evidence-lifecycle.md).
22+
1223
## Production authority and independent review
1324

1425
The human production boundary and independent review are separate controls. Every change still requires fresh-context, implementation-independent review evidence. Production authorization and promotion remain manual human actions, and automation or agents may not perform them. For an owner-controlled repository, the owner's explicit authorization and manual merge or promotion satisfy the human boundary; a second human GitHub reviewer is not implicitly required. Repositories with multiple authorized maintainers may configure a stronger branch or Environment reviewer gate.

docs/evidence-lifecycle.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Exact-revision evidence lifecycle
2+
3+
This is the authoritative operational contract for process evidence. Evidence
4+
JSON is data, never proof of its own producer. Canonical policy defines required
5+
predicates and producer classes; a platform adapter verifies transport identity
6+
and supplies a separate trust index.
7+
8+
## Sequence and storage
9+
10+
```text
11+
push H -> trusted diff(base,H) -> classification(H, process P)
12+
-> CI/authenticated verdicts(H,P,repository) -> immutable artifact store
13+
-> readiness joins artifacts + verified provenance -> merge status
14+
-> merge creates M (H is stale) -> deployment-release(M,P) in release store
15+
```
16+
17+
The manifest, lock, and policy are source controlled. Pre-merge classification,
18+
test, specialist, review, approval, and readiness evidence are not committed:
19+
GitHub stores them as immutable workflow artifacts or attestations keyed by
20+
repository, head SHA, process revision, workflow run and attempt. Equivalent
21+
platforms may use an external store with the same keys. Deployment evidence is
22+
post-merge evidence and is retained separately from the readiness join.
23+
24+
`readiness` reads predicates from downloaded artifacts. Authenticated/trusted
25+
status comes only from a verified transport index outside the checkout. It maps
26+
each evidence content digest to repository, target revision, capability,
27+
producer class, and platform identity. GitHub records also carry workflow,
28+
run/attempt, and job identity. Repository-authored producer fields cannot elevate
29+
evidence.
30+
31+
## Ordering, invalidation, and recovery
32+
33+
1. Resolve immutable base/head commits and classify their complete Git diff.
34+
2. Publish classification before dependent jobs; every producer uses that exact
35+
head and locked process revision.
36+
3. Collect all predicates and provenance, then run the readiness join.
37+
4. Any new head is a different key and immediately invalidates prior readiness.
38+
Merge and post-merge release commits require their own evidence.
39+
40+
A rerun creates a new immutable attempt. Any failing or conflicting verdict in a
41+
selected collection fails the join; success does not erase failure. Resolve a
42+
contradiction with a clean run and an explicitly selected, auditable attempt. A
43+
missing, expired, malformed, inaccessible, or unverifiable artifact fails closed:
44+
restore/fetch it or rerun; never copy evidence to a new SHA. Retention follows
45+
the platform policy and for Protected work should cover merge plus the audit
46+
period. Expired evidence requires rerun.
47+
48+
Fork runs may create asserted diagnostics, but receive neither secrets nor trusted
49+
status. Protected readiness runs in the base repository after authorization,
50+
against the fork head SHA, with read-only source permissions. Human and agent
51+
verdicts enter through authenticated reviews, protected workflow dispatch, or an
52+
external service preserving actor/event audit identity; committed JSON remains
53+
an assertion.
54+
55+
## Threat model
56+
57+
- Contributors can create, replace, and replay repository JSON, but cannot create
58+
authenticated/trusted provenance that way.
59+
- Workflow authors can alter repository workflows. Protected trusted CI is
60+
restricted to approved workflow identities/refs and environment or ruleset;
61+
workflow changes themselves classify Protected.
62+
- Platform administrators are inside the platform trust boundary. Audit logs and
63+
artifact attestations reduce replacement risk.
64+
- Compromised credentials may impersonate their holder. Adapters require least
65+
privilege, OIDC issuer/audience checks, and short-lived credentials.
66+
- Repository, exact SHA, process revision, content digest and run identity prevent
67+
replay. Wrong repository/workflow/job and unauthorized capability fail.
68+
69+
Lean v1 evidence remains proportionate and may be `asserted` where policy allows.
70+
Standard and Protected progressively require authenticated/trusted provenance per
71+
`policy/evidence/producer-authorization.yaml`. No self-declared v1 field is ever
72+
upgraded to trusted status.
73+
74+
## Process upgrades and bootstrap trust
75+
76+
The executable revision changes only through `engineering-process upgrade`, which
77+
updates the manifest, lock, generated workflow and snapshot together. Generated
78+
and reusable workflows install the exact 40-character lock SHA and pass it back
79+
to validation. Missing, floating, unavailable, or mismatched revisions fail.
80+
The initial checkout, Git host, and action runner are bootstrap trust assumptions.

enforcement/github/reusable-workflows/process-readiness.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ jobs:
1313
with: {ref: "${{ inputs.target-sha }}", fetch-depth: 0}
1414
- uses: actions/setup-python@v5
1515
with: {python-version: "3.12"}
16-
- run: pip install software-engineering-process
17-
- run: engineering-process validate
16+
- name: Resolve locked process revision
17+
id: process
18+
shell: bash
19+
run: |
20+
revision="$(python -c 'import json; print(json.load(open(".engineering/process.lock"))["revision"])')"
21+
[[ "$revision" =~ ^[0-9a-f]{40}$ ]] || { echo 'invalid locked process revision' >&2; exit 2; }
22+
echo "revision=$revision" >> "$GITHUB_OUTPUT"
23+
- name: Install exact locked process revision
24+
run: pip install 'git+https://github.com/suyog19/software-engineering-process@${{ steps.process.outputs.revision }}'
25+
- run: engineering-process validate --runtime-revision '${{ steps.process.outputs.revision }}'
1826
- run: engineering-process readiness --sha "${{ inputs.target-sha }}"
19-
Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
lean_hints: [typo, documentation_only, metadata_only, mechanical_test_only, established_css_only]
2-
standard: [observable_behavior, public_contract, user_journey, information_architecture, architecture, cross_service, state_transition, validation_or_error, performance, retry, concurrency]
2+
standard: [observable_behavior, public_contract, user_journey, information_architecture, architecture, cross_service, state_transition, validation_or_error, performance, retry, concurrency, dependency_change, generated_change]
33
protected: [financial_consequence, payment, payment_provider_trust, authentication, authorization, iam, privilege_or_trust, secrets, sensitive_data, production_controls, irreversible_data, infrastructure, refund_or_cancellation, learner_entitlement, sensitive_admin_action]
44
deterministic_files:
55
- {pattern: "**/auth/**", characteristic: authentication}
@@ -8,5 +8,15 @@ deterministic_files:
88
- {pattern: "**/secrets/**", characteristic: secrets}
99
- {pattern: "**/migrations/**", characteristic: irreversible_data}
1010
- {pattern: "**/infra/**", characteristic: infrastructure}
11-
- {pattern: ".github/workflows/deploy*.yml", characteristic: production_controls}
12-
11+
- {pattern: ".github/workflows/**", characteristic: production_controls}
12+
- {pattern: "**/package-lock.json", characteristic: dependency_change}
13+
- {pattern: "**/requirements*.txt", characteristic: dependency_change}
14+
- {pattern: "**/poetry.lock", characteristic: dependency_change}
15+
- {pattern: "**/uv.lock", characteristic: dependency_change}
16+
- {pattern: "**/Cargo.lock", characteristic: dependency_change}
17+
- {pattern: "**/go.sum", characteristic: dependency_change}
18+
- {pattern: "**/generated/**", characteristic: generated_change}
19+
- {pattern: ".engineering/process.yaml", characteristic: privilege_or_trust}
20+
- {pattern: ".engineering/process.lock", characteristic: privilege_or_trust}
21+
- {pattern: "AGENTS.md", characteristic: privilege_or_trust}
22+
- {pattern: "CLAUDE.md", characteristic: privilege_or_trust}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
trust_levels: {asserted: 0, authenticated: 1, trusted: 2}
2+
predicates:
3+
classification/v1: {producer_classes: [trusted-ci], minimum: {Lean: asserted, Standard: authenticated, Protected: trusted}}
4+
test-result/v1: {producer_classes: [trusted-ci], minimum: {Lean: asserted, Standard: authenticated, Protected: trusted}}
5+
ux-verdict/v1: {producer_classes: [authorized-human, authorized-agent], minimum: {Standard: authenticated, Protected: authenticated}}
6+
architecture-decision/v1: {producer_classes: [authorized-human, authorized-agent], minimum: {Protected: authenticated}}
7+
functional-qa-verdict/v1: {producer_classes: [authorized-human, authorized-agent, trusted-ci], minimum: {Standard: authenticated, Protected: authenticated}}
8+
non-functional-qa-verdict/v1: {producer_classes: [authorized-human, authorized-agent, trusted-ci], minimum: {Protected: authenticated}}
9+
independent-review/v1: {producer_classes: [authorized-human, authorized-agent], minimum: {Lean: asserted, Standard: authenticated, Protected: authenticated}}
10+
solution-sufficiency/v1: {producer_classes: [authorized-human, authorized-agent], minimum: {Protected: authenticated}}
11+
human-approval/v1: {producer_classes: [product-owner], minimum: {Protected: authenticated}}
12+
deployment-release/v1: {producer_classes: [trusted-ci], minimum: {Protected: trusted}}

src/engineering_process/classification.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@ class Signal:
2020
@dataclass
2121
class Classification:
2222
target_revision: str
23+
base_revision: str
2324
delivery_profile: str
2425
characteristics: dict[str, bool | None]
2526
deterministic_signals: list[dict]
2627
semantic_rationale: str
2728
reasons: list[str]
2829
protected_ambiguity: bool = False
30+
change_set: list[dict] | None = None
31+
input_trust: str = "untrusted-manual"
32+
path_explanations: list[dict] | None = None
2933

3034
def to_dict(self) -> dict:
3135
return asdict(self)
@@ -44,8 +48,12 @@ def classify(
4448
semantic: dict[str, bool | None] | None = None,
4549
semantic_rationale: str = "",
4650
target_revision: str = "",
51+
base_revision: str = "",
52+
change_set: list[dict] | None = None,
53+
input_trust: str = "untrusted-manual",
4754
) -> Classification:
4855
cfg = load_yaml(policy_root / "classification" / "characteristics.yaml")
56+
changed_paths = list(changed_paths)
4957
declared, semantic = declared or {}, semantic or {}
5058
characteristics: dict[str, bool | None] = {**declared, **semantic}
5159
rules = list(cfg["deterministic_files"])
@@ -92,4 +100,9 @@ def classify(
92100
reasons.append(f"Ignored requested downgrade to {explicit_profile}; classification is monotonic")
93101
if any(s.characteristic in protected for s in signals) and delivery != "Protected":
94102
raise ProcessError("internal classification integrity failure: Protected signal was downgraded")
95-
return Classification(target_revision, delivery, characteristics, [asdict(s) for s in signals], semantic_rationale, reasons, bool(unresolved))
103+
explanations = []
104+
for path in changed_paths:
105+
matched = sorted({signal.characteristic for signal in signals if signal.value == path})
106+
explanations.append({"path": path, "deterministic_characteristics": matched,
107+
"effect": "matched deterministic rule(s)" if matched else "no deterministic rule matched"})
108+
return Classification(target_revision, base_revision, delivery, characteristics, [asdict(s) for s in signals], semantic_rationale, reasons, bool(unresolved), change_set or [], input_trust, explanations)

0 commit comments

Comments
 (0)