matrixscroll 0.2.0__tar.gz → 0.2.2__tar.gz

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 (73) hide show
  1. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/CHANGELOG.md +17 -0
  2. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/PKG-INFO +7 -8
  3. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/README.md +6 -7
  4. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/AGENTIC_AI_SECURITY.md +7 -9
  5. matrixscroll-0.2.2/docs/WHITEPAPER.md +144 -0
  6. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/quickstart-git.md +11 -0
  7. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/__init__.py +1 -1
  8. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/cli.py +62 -6
  9. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/git.py +44 -21
  10. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/pyproject.toml +1 -1
  11. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_cli.py +34 -0
  12. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_git_envelope.py +18 -0
  13. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/.gitignore +0 -0
  14. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/CONTRIBUTING.md +0 -0
  15. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/LICENSE +0 -0
  16. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/SECURITY.md +0 -0
  17. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/SPEC.md +0 -0
  18. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/controls/agentic_ai_controls.json +0 -0
  19. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/hardware-provider.md +0 -0
  20. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/superpowers/plans/2026-06-19-ci-action-plan.md +0 -0
  21. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/superpowers/plans/2026-06-19-sdk-refactor-plan.md +0 -0
  22. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/superpowers/specs/2026-06-19-matrixscroll-git-design.md +0 -0
  23. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/yubikey-bridge.md +0 -0
  24. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/agentic_ai_evidence_manifest.json +0 -0
  25. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/agentic_ai_evidence_manifest.signed.json +0 -0
  26. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/ci/protected-branch.yml +0 -0
  27. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/commit-envelope.json +0 -0
  28. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/commit-envelope.signed.json +0 -0
  29. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/demo/agent-commit-demo.sh +0 -0
  30. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/demo/generate_signed_examples.py +0 -0
  31. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/release-manifest.json +0 -0
  32. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/release-manifest.signed.json +0 -0
  33. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/_core.py +0 -0
  34. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/canonical.py +0 -0
  35. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/constants.py +0 -0
  36. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/errors.py +0 -0
  37. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/hooks/post-commit +0 -0
  38. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/hooks/pre-push +0 -0
  39. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/manifest.py +0 -0
  40. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/policy.py +0 -0
  41. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/providers/__init__.py +0 -0
  42. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/providers/base.py +0 -0
  43. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/providers/emulated.py +0 -0
  44. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/providers/hardware.py +0 -0
  45. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/providers/registry.py +0 -0
  46. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/providers/yubikey.py +0 -0
  47. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/py.typed +0 -0
  48. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/schemas/commit-envelope.v1.json +0 -0
  49. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/schemas/evidence-pack.v1.json +0 -0
  50. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/schemas/release-manifest.v1.json +0 -0
  51. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/__init__.py +0 -0
  52. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_agentic_guidance.py +0 -0
  53. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_canonical.py +0 -0
  54. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_core.py +0 -0
  55. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_policy.py +0 -0
  56. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_release_metadata.py +0 -0
  57. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_vectors.py +0 -0
  58. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_yubikey_provider.py +0 -0
  59. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/README.md +0 -0
  60. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/_fixture_key.json +0 -0
  61. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/_generate.py +0 -0
  62. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/tampered_algorithm.json +0 -0
  63. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/tampered_device_id.json +0 -0
  64. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/tampered_field.json +0 -0
  65. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/tampered_nested.json +0 -0
  66. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/tampered_public_key.json +0 -0
  67. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/tampered_schema.json +0 -0
  68. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/tampered_signature.json +0 -0
  69. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/unsigned_empty_block.json +0 -0
  70. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/unsigned_no_block.json +0 -0
  71. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/valid_nested.json +0 -0
  72. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/valid_simple.json +0 -0
  73. {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/valid_unicode.json +0 -0
@@ -4,6 +4,23 @@ All notable changes to the Matrix Scroll Python SDK are documented here. The
4
4
  format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
5
5
  this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.2.2] - 2026-06-20
8
+
9
+ Policy flags for CI and release gates.
10
+
11
+ ### Added
12
+ - **CLI policy flags** — `matrixscroll verify` and `matrixscroll envelope-verify` accept
13
+ `--require-mode` and `--trusted-keys` (JSON policy file), wired to
14
+ `verify_manifest_with_policy()`.
15
+
16
+ ## [0.2.1] - 2026-06-20
17
+
18
+ Windows and cross-platform commit envelope fix.
19
+
20
+ ### Fixed
21
+ - **`parse_commit()` on Windows** — read author/committer timezone from `git cat-file commit`
22
+ instead of reconstructing dates with `%z` (unsupported in `git show` format on Windows).
23
+
7
24
  ## [0.2.0] - 2026-06-20
8
25
 
9
26
  Agent provenance release: Git commit envelopes, SDK module split, CI scaffolding.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrixscroll
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Open protocol for signing AI-assisted code provenance with Ed25519; shipping software root of trust with SSX360 hardware support in progress.
5
5
  Project-URL: Homepage, https://matrixscroll.com
6
6
  Project-URL: Documentation, https://matrixscroll.com/docs
@@ -37,15 +37,14 @@ Description-Content-Type: text/markdown
37
37
 
38
38
  # Matrix Scroll
39
39
 
40
- **Open protocol for signed AI-assisted code provenance.**
40
+ **Signed provenance for agent-assisted Git commits — verify offline, one command.**
41
41
 
42
- Every AI-generated change in your IDE can be cryptographically signed by an
43
- Ed25519 identity and verified offline with a public key and one command. The
44
- v0.2.x reference implementation ships a well-tested software root of trust
45
- with Git commit-envelope hooks; SSX360/NXP SE050 hardware signing is the
46
- compatible reference-device path in progress.
42
+ Matrix Scroll is a cryptographic evidence layer: when an AI agent (Cursor, Claude Code, Copilot, etc.) produces a commit, a signed **commit envelope** records actor, tool, and optional scope. Verify locally or in CI without trusting the IDE. The v0.2.x reference SDK ships an emulated Ed25519 root of trust with Git hooks; SSX360/NXP SE050 hardware signing is the compatible reference-device path in progress.
43
+
44
+ Matrix Scroll is **not** an IAM system, agent sandbox, or prompt-injection filter it complements those controls with tamper-evident signatures.
47
45
 
48
46
  - 📜 **Spec:** [`SPEC.md`](SPEC.md) — wire format, canonical encoding, schemas.
47
+ - 📄 **Whitepaper:** [`docs/WHITEPAPER.md`](docs/WHITEPAPER.md) — why Git commits, implementation guide.
49
48
  - 🛡 **Agentic AI controls:** [`docs/AGENTIC_AI_SECURITY.md`](docs/AGENTIC_AI_SECURITY.md)
50
49
  maps Matrix Scroll to the joint *Careful Adoption of Agentic AI Services* guidance.
51
50
  - 🔐 **Algorithm:** Ed25519 (RFC 8032). Private keys are never exposed by the SDK API.
@@ -100,7 +99,7 @@ See [`docs/quickstart-git.md`](docs/quickstart-git.md) and run
100
99
  - uses: SSX360/matrixscroll-verify-action@v1
101
100
  with:
102
101
  manifest: examples/agentic_ai_evidence_manifest.signed.json
103
- matrixscroll-version: "0.2.0"
102
+ matrixscroll-version: "0.2.2"
104
103
  ```
105
104
 
106
105
  ## CLI
@@ -1,14 +1,13 @@
1
1
  # Matrix Scroll
2
2
 
3
- **Open protocol for signed AI-assisted code provenance.**
3
+ **Signed provenance for agent-assisted Git commits — verify offline, one command.**
4
4
 
5
- Every AI-generated change in your IDE can be cryptographically signed by an
6
- Ed25519 identity and verified offline with a public key and one command. The
7
- v0.2.x reference implementation ships a well-tested software root of trust
8
- with Git commit-envelope hooks; SSX360/NXP SE050 hardware signing is the
9
- compatible reference-device path in progress.
5
+ Matrix Scroll is a cryptographic evidence layer: when an AI agent (Cursor, Claude Code, Copilot, etc.) produces a commit, a signed **commit envelope** records actor, tool, and optional scope. Verify locally or in CI without trusting the IDE. The v0.2.x reference SDK ships an emulated Ed25519 root of trust with Git hooks; SSX360/NXP SE050 hardware signing is the compatible reference-device path in progress.
6
+
7
+ Matrix Scroll is **not** an IAM system, agent sandbox, or prompt-injection filter it complements those controls with tamper-evident signatures.
10
8
 
11
9
  - 📜 **Spec:** [`SPEC.md`](SPEC.md) — wire format, canonical encoding, schemas.
10
+ - 📄 **Whitepaper:** [`docs/WHITEPAPER.md`](docs/WHITEPAPER.md) — why Git commits, implementation guide.
12
11
  - 🛡 **Agentic AI controls:** [`docs/AGENTIC_AI_SECURITY.md`](docs/AGENTIC_AI_SECURITY.md)
13
12
  maps Matrix Scroll to the joint *Careful Adoption of Agentic AI Services* guidance.
14
13
  - 🔐 **Algorithm:** Ed25519 (RFC 8032). Private keys are never exposed by the SDK API.
@@ -63,7 +62,7 @@ See [`docs/quickstart-git.md`](docs/quickstart-git.md) and run
63
62
  - uses: SSX360/matrixscroll-verify-action@v1
64
63
  with:
65
64
  manifest: examples/agentic_ai_evidence_manifest.signed.json
66
- matrixscroll-version: "0.2.0"
65
+ matrixscroll-version: "0.2.2"
67
66
  ```
68
67
 
69
68
  ## CLI
@@ -36,9 +36,9 @@ The machine-readable control matrix lives at
36
36
  | AAI-04 | Human accountability | Owner, approver, reviewer, break-glass contact, and kill-switch fields are signed. |
37
37
  | AAI-05 | Threat modeling | Threats are mapped to strict verification and tamper vectors. |
38
38
  | AAI-06 | Secure defaults / validation | Wrong schema, algorithm, device id, malformed key, NaN, and unsigned manifests fail closed. |
39
- | AAI-07 | Monitoring and auditability | Signed manifests are portable audit records verifiable offline. |
39
+ | AAI-07 | Monitoring and auditability | Signed manifests and **commit envelopes** (`.git/matrixscroll/envelopes/<sha>.json`) are portable audit records verifiable offline. |
40
40
  | AAI-08 | Incident response / kill switch | CI/CLI verification exits non-zero; manifests include escalation and shutdown metadata. |
41
- | AAI-09 | Supply-chain management | Minimal deps, Dependabot, CI build verification, and conformance vectors. |
41
+ | AAI-09 | Supply-chain management | Git post-commit hooks + CI verify-action; minimal deps; Dependabot; conformance vectors. |
42
42
  | AAI-10 | Strong authentication / non-repudiation | Ed25519 identity; the planned SSX360 hardware mode keeps private keys out of agent runtimes. |
43
43
  | AAI-11 | Governance and change control | CODEOWNERS + CI protect spec/core/vectors/security files. |
44
44
  | AAI-12 | Deception / prompt-injection resilience | Trust is verified after agent action; model text cannot forge signatures. |
@@ -63,14 +63,12 @@ Matrix Scroll adds a stronger evidence layer on top:
63
63
  ## Recommended deployment pattern
64
64
 
65
65
  1. Define the agent task as a low-risk, bounded pilot.
66
- 2. Create an evidence manifest like
66
+ 2. Install Git hooks (`matrixscroll hook-install`) and set `MATRIXSCROLL_ACTOR_TYPE=agent`.
67
+ 3. Create an evidence manifest like
67
68
  [`examples/agentic_ai_evidence_manifest.json`](../examples/agentic_ai_evidence_manifest.json).
68
- 3. Have a human owner approve the resource/operation/timeframe scope.
69
- 4. Sign the manifest with `matrixscroll sign`.
70
- 5. Require `matrixscroll verify` in CI before the agent can act or before its
71
- output can be accepted.
72
- 6. Retain the signed manifest with logs, SBOM, model/provider metadata, and
73
- incident-response records.
69
+ 4. Have a human owner approve the resource/operation/timeframe scope.
70
+ 5. Sign the manifest with `matrixscroll sign`; require `matrixscroll envelope-verify` on each agent commit in CI.
71
+ 6. Retain signed envelopes and manifests with logs, SBOM, and incident-response records.
74
72
 
75
73
  ## Residual risks outside SDK scope
76
74
 
@@ -0,0 +1,144 @@
1
+ # Agent Attestation for Git Commits
2
+
3
+ **Signed provenance for agent-assisted code changes — verify offline, one command.**
4
+
5
+ ## Executive summary
6
+
7
+ Agentic coding tools can commit and push code without a durable record of *who* acted (human, agent, or CI) or *which tool* produced the change. Review fatigue and credential theft make software-only gates insufficient. Matrix Scroll adds a cryptographic **commit envelope** — Ed25519-signed metadata bound to each Git commit — that anyone can verify offline or in CI.
8
+
9
+ This document distills the public product story. The full enterprise architecture lives in internal strategy materials; this whitepaper is scoped to developers, DevSecOps, and auditors adopting the open protocol today.
10
+
11
+ ## Scope and audience
12
+
13
+ **Audience:** Software engineers using AI-assisted IDEs, platform teams wiring CI gates, security auditors.
14
+
15
+ **Covers:** Why Git commits are the Day-1 beachhead, protocol overview, install path, honest roadmap.
16
+
17
+ **Does not cover:** IAM replacement, prompt-injection mitigation, financial transaction gating, or hardware manufacturing details.
18
+
19
+ **Prerequisites:** Git, Python 3.10+, basic CI familiarity.
20
+
21
+ ## What Matrix Scroll is — and is not
22
+
23
+ Matrix Scroll is **not** an agent runtime, sandbox, IAM system, or prompt filter. It is a **cryptographic evidence layer**: it signs what an agent (or human) attested at commit time and lets verifiers check that record without trusting the IDE or Matrix Scroll servers.
24
+
25
+ ## Why Git commits first
26
+
27
+ | Vector | Risk | Why Matrix Scroll fits |
28
+ |--------|------|------------------------|
29
+ | **Git commits** | Rogue agent code, LLM filibustering in review, stolen tokens | Standardized hooks; high-consequence; maps to supply-chain guidance |
30
+ | Financial APIs | Fragmented rails, liability | Defer — integration cost too high for Day 1 |
31
+ | Database writes | Latency, engine sprawl | Defer — per-query hardware signing impractical |
32
+
33
+ Supply-chain incidents involving agent-assisted merges illustrate the gap: audit logs show *that* a commit landed, not *which actor class and tool* produced it with verifiable scope.
34
+
35
+ ## Regulatory context (verified links)
36
+
37
+ Five Eyes agencies published joint guidance on careful adoption of agentic AI services:
38
+
39
+ - [CISA — Careful Adoption of Agentic AI Services](https://www.cisa.gov/resources-tools/resources/careful-adoption-agentic-ai-services)
40
+ - [ACSC — Careful adoption of agentic AI services](https://www.cyber.gov.au/business-government/secure-design/artificial-intelligence/careful-adoption-of-agentic-ai-services)
41
+ - [Canadian Centre for Cyber Security](https://www.cyber.gc.ca/en/news-events/joint-guidance-careful-adoption-agentic-artificial-intelligence-services)
42
+ - [NCSC-UK — Thinking carefully before adopting agentic AI](https://www.ncsc.gov.uk/blogs/thinking-carefully-before-adopting-agentic-ai)
43
+
44
+ Matrix Scroll maps these controls in [`docs/AGENTIC_AI_SECURITY.md`](AGENTIC_AI_SECURITY.md), including commit-envelope audit trails for monitoring (AAI-07) and supply-chain evidence (AAI-09).
45
+
46
+ ## Protocol overview
47
+
48
+ Three layers:
49
+
50
+ 1. **Identity** (`matrixscroll.identity.v1`) — Ed25519 key pair; device id `MS-XXXX-XXXX`
51
+ 2. **Commit envelope** (`matrixscroll.commit_envelope.v1`) — commit object fields + provenance (`actor_type`, `tool`, optional scope manifest)
52
+ 3. **Verification** — canonical JSON bytes, signature excluded; offline check with embedded public key
53
+
54
+ Full wire format: [`SPEC.md`](../SPEC.md). Schema: [`schemas/commit-envelope.v1.json`](../schemas/commit-envelope.v1.json).
55
+
56
+ ```
57
+ agent / IDE / CI
58
+
59
+
60
+ git commit ──► post-commit hook signs envelope
61
+
62
+
63
+ .git/matrixscroll/envelopes/<sha>.json
64
+
65
+
66
+ matrixscroll envelope-verify <sha> (local or CI)
67
+ ```
68
+
69
+ ## Step-by-step implementation
70
+
71
+ ### 1. Install
72
+
73
+ ```bash
74
+ pip install "matrixscroll>=0.2.1"
75
+ matrixscroll hook-install
76
+ matrixscroll hook-status
77
+ ```
78
+
79
+ ### 2. Agent provenance
80
+
81
+ ```bash
82
+ export MATRIXSCROLL_ACTOR_TYPE=agent
83
+ export MATRIXSCROLL_TOOL=cursor
84
+ git commit -m "feat: agent-assisted change"
85
+ matrixscroll envelope-verify "$(git rev-parse HEAD)"
86
+ ```
87
+
88
+ Hooks default to **warn mode**; set `"enforce": true` in `.git/matrixscroll/config.json` to block commits when signing fails. See [`docs/quickstart-git.md`](quickstart-git.md).
89
+
90
+ **Windows:** commit-envelope signing requires matrixscroll **0.2.1+** (cat-file parsing fix).
91
+
92
+ ### 3. CI gate
93
+
94
+ ```yaml
95
+ - uses: SSX360/matrixscroll-verify-action@v1
96
+ with:
97
+ manifest: path/to/signed-manifest.json
98
+ matrixscroll-version: "0.2.2"
99
+ require-mode: emulated
100
+ trusted-keys: trusted-keys.json
101
+ ```
102
+
103
+ Policy flags (`--require-mode`, `--trusted-keys`) ship in **0.2.2+**.
104
+
105
+ ### 4. Optional scope manifest
106
+
107
+ Bind agent operations to a signed evidence manifest:
108
+
109
+ ```bash
110
+ export MATRIXSCROLL_AGENT_SCOPE=examples/agentic_ai_evidence_manifest.signed.json
111
+ ```
112
+
113
+ ## Trust levels (honest roadmap)
114
+
115
+ | Level | Provider | Status |
116
+ |-------|----------|--------|
117
+ | **L1 Emulated** | Software key (`~/.matrixscroll/`) | Shipping |
118
+ | **L2 Hardware** | SSX360 / NXP SE050 secure element | In progress |
119
+ | **L3 Attested** | L2 + remote attestation | Roadmap |
120
+
121
+ YubiKey bridge (PKCS#11 / PIV) documented in [`docs/yubikey-bridge.md`](yubikey-bridge.md).
122
+
123
+ ## Common pitfalls
124
+
125
+ - **Expecting IAM replacement** — Matrix Scroll proves evidence; you still need least privilege and sandboxing.
126
+ - **Pre-commit expected_id** — v0.2.x signs **post-commit** with the actual SHA; do not bind envelopes before the commit exists.
127
+ - **Skipping enforce mode in production** — warn mode is for adoption; CI should fail closed on verify exit `2`.
128
+
129
+ ## Conclusion and next steps
130
+
131
+ 1. Install hooks in a pilot repo with `MATRIXSCROLL_ACTOR_TYPE=agent`.
132
+ 2. Add verify-action to CI on protected branches.
133
+ 3. Read the control mapping in [`docs/AGENTIC_AI_SECURITY.md`](AGENTIC_AI_SECURITY.md).
134
+ 4. Follow protocol changes via [`CHANGELOG.md`](../CHANGELOG.md) and GitHub releases.
135
+
136
+ Questions: [GitHub Discussions](https://github.com/SSX360/matrixscroll/discussions) or security@matrixscroll.com for vulnerabilities.
137
+
138
+ ## References
139
+
140
+ - Matrix Scroll spec: [`SPEC.md`](../SPEC.md)
141
+ - Agentic AI mapping: [`docs/AGENTIC_AI_SECURITY.md`](AGENTIC_AI_SECURITY.md)
142
+ - Git design spec: [`docs/superpowers/specs/2026-06-19-matrixscroll-git-design.md`](superpowers/specs/2026-06-19-matrixscroll-git-design.md)
143
+ - CISA agentic AI guidance (link above)
144
+ - Conformance vectors: [`vectors/`](../vectors/)
@@ -33,6 +33,17 @@ Enable enforce mode in `.git/matrixscroll/config.json`:
33
33
  }
34
34
  ```
35
35
 
36
+ ## Windows
37
+
38
+ Commit-envelope signing requires **matrixscroll 0.2.1+** (fixes Git `cat-file` parsing on Windows). After upgrading:
39
+
40
+ ```powershell
41
+ pip install -U "matrixscroll>=0.2.1"
42
+ matrixscroll hook-install
43
+ ```
44
+
45
+ Use `python -m matrixscroll.cli` if the `matrixscroll` script is not on your PATH.
46
+
36
47
  ## Agent provenance
37
48
 
38
49
  Set environment variables before committing:
@@ -38,7 +38,7 @@ from ._core import (
38
38
  verify_manifest,
39
39
  )
40
40
 
41
- __version__ = "0.2.0"
41
+ __version__ = "0.2.2"
42
42
 
43
43
  __all__ = [
44
44
  "ALGORITHM",
@@ -18,6 +18,7 @@ import sys
18
18
  from pathlib import Path
19
19
 
20
20
  from ._core import sign_manifest, status, verify_manifest
21
+ from .policy import VerifyPolicy, verify_manifest_with_policy
21
22
 
22
23
  try:
23
24
  from . import git as _git
@@ -30,6 +31,24 @@ def _cmd_status(_args: argparse.Namespace) -> int:
30
31
  return 0
31
32
 
32
33
 
34
+ def _load_policy(args: argparse.Namespace) -> VerifyPolicy | None:
35
+ policy = VerifyPolicy(require_mode=args.require_mode or None)
36
+ if args.trusted_keys:
37
+ file_policy = VerifyPolicy.from_json_file(args.trusted_keys)
38
+ policy.trusted_public_keys = file_policy.trusted_public_keys
39
+ if file_policy.require_mode and not policy.require_mode:
40
+ policy.require_mode = file_policy.require_mode
41
+ if file_policy.allowed_schemas:
42
+ policy.allowed_schemas = file_policy.allowed_schemas
43
+ if (
44
+ policy.require_mode is None
45
+ and policy.trusted_public_keys is None
46
+ and policy.allowed_schemas is None
47
+ ):
48
+ return None
49
+ return policy
50
+
51
+
33
52
  def _cmd_verify(args: argparse.Namespace) -> int:
34
53
  path = Path(args.manifest)
35
54
  try:
@@ -37,15 +56,25 @@ def _cmd_verify(args: argparse.Namespace) -> int:
37
56
  except (OSError, ValueError) as exc:
38
57
  print(json.dumps({"ok": False, "error": f"cannot read manifest: {exc}"}))
39
58
  return 2
40
- ok = verify_manifest(manifest)
59
+ policy = _load_policy(args)
60
+ if policy is not None:
61
+ ok, reason = verify_manifest_with_policy(manifest, policy)
62
+ if not ok:
63
+ print(json.dumps({"ok": False, "error": reason or "policy verification failed"}))
64
+ return 2
65
+ else:
66
+ ok = verify_manifest(manifest)
67
+ if not ok:
68
+ print(json.dumps({"ok": False, "error": "cryptographic verification failed"}))
69
+ return 2
41
70
  block = manifest.get("signature") or {}
42
71
  print(json.dumps({
43
- "ok": ok,
72
+ "ok": True,
44
73
  "device_id": block.get("device_id"),
45
74
  "mode": block.get("mode"),
46
75
  "signed_at": block.get("signed_at"),
47
76
  }, sort_keys=True))
48
- return 0 if ok else 2
77
+ return 0
49
78
 
50
79
 
51
80
  def _cmd_hook_install(_args: argparse.Namespace) -> int:
@@ -97,15 +126,25 @@ def _cmd_envelope_verify(args: argparse.Namespace) -> int:
97
126
  print(json.dumps({"ok": False, "error": f"no envelope for {target}"}))
98
127
  return 2
99
128
  envelope = json.loads(env_path.read_text(encoding="utf-8-sig"))
100
- ok = verify_manifest(envelope)
129
+ policy = _load_policy(args)
130
+ if policy is not None:
131
+ ok, reason = verify_manifest_with_policy(envelope, policy)
132
+ if not ok:
133
+ print(json.dumps({"ok": False, "error": reason or "policy verification failed"}))
134
+ return 2
135
+ else:
136
+ ok = verify_manifest(envelope)
137
+ if not ok:
138
+ print(json.dumps({"ok": False, "error": "cryptographic verification failed"}))
139
+ return 2
101
140
  block = envelope.get("signature") or {}
102
141
  print(json.dumps({
103
- "ok": ok,
142
+ "ok": True,
104
143
  "device_id": block.get("device_id"),
105
144
  "mode": block.get("mode"),
106
145
  "signed_at": block.get("signed_at"),
107
146
  }, sort_keys=True))
108
- return 0 if ok else 2
147
+ return 0
109
148
 
110
149
 
111
150
  def _cmd_sign(args: argparse.Namespace) -> int:
@@ -120,6 +159,21 @@ def _cmd_sign(args: argparse.Namespace) -> int:
120
159
  return 0
121
160
 
122
161
 
162
+ def _add_policy_args(parser: argparse.ArgumentParser) -> None:
163
+ parser.add_argument(
164
+ "--require-mode",
165
+ metavar="MODE",
166
+ default="",
167
+ help="Require signature mode (e.g. emulated, hardware)",
168
+ )
169
+ parser.add_argument(
170
+ "--trusted-keys",
171
+ metavar="PATH",
172
+ default="",
173
+ help="JSON file with trusted_public_keys and optional policy fields",
174
+ )
175
+
176
+
123
177
  def build_parser() -> argparse.ArgumentParser:
124
178
  parser = argparse.ArgumentParser(
125
179
  prog="matrixscroll",
@@ -131,6 +185,7 @@ def build_parser() -> argparse.ArgumentParser:
131
185
 
132
186
  verify_p = sub.add_parser("verify", help="Verify a signed manifest JSON file")
133
187
  verify_p.add_argument("manifest", help="Path to a signed manifest produced by sign_manifest")
188
+ _add_policy_args(verify_p)
134
189
 
135
190
  sign_p = sub.add_parser("sign", help="Sign a manifest JSON file with the active provider")
136
191
  sign_p.add_argument("manifest", help="Path to a manifest JSON file to sign")
@@ -148,6 +203,7 @@ def build_parser() -> argparse.ArgumentParser:
148
203
 
149
204
  env_verify = sub.add_parser("envelope-verify", help="Verify a commit envelope by path or commit sha")
150
205
  env_verify.add_argument("target", help="Envelope file path or 40-char commit sha")
206
+ _add_policy_args(env_verify)
151
207
  env_verify.set_defaults(command="envelope-verify")
152
208
 
153
209
  return parser
@@ -19,7 +19,7 @@ HOOK_MARKER = "# matrixscroll-git hook\n"
19
19
  DEFAULT_CONFIG = {"enforce": False, "actor_type": "human", "tool": "git-cli"}
20
20
 
21
21
 
22
- def _run_git(*args: str, cwd: Path | None = None) -> str:
22
+ def _run_git(*args: str, cwd: Path | None = None, strip: bool = True) -> str:
23
23
  result = subprocess.run(
24
24
  ["git", *args],
25
25
  capture_output=True,
@@ -29,7 +29,9 @@ def _run_git(*args: str, cwd: Path | None = None) -> str:
29
29
  )
30
30
  if result.returncode != 0:
31
31
  raise RuntimeError(result.stderr.strip() or result.stdout.strip())
32
- return result.stdout.strip()
32
+ if strip:
33
+ return result.stdout.strip()
34
+ return result.stdout
33
35
 
34
36
 
35
37
  def repo_root() -> Path:
@@ -102,28 +104,49 @@ def compute_commit_id(
102
104
  message: str,
103
105
  ) -> str:
104
106
  """Compute Git commit object SHA-1 without creating the commit."""
105
- lines = [f"tree {tree}"]
106
- for parent in parents:
107
- lines.append(f"parent {parent}")
108
- lines.append(f"author {author['name']} <{author['email']}> {author['date']}")
109
- lines.append(f"committer {committer['name']} <{committer['email']}> {committer['date']}")
110
- lines.append("")
111
- lines.append(message.rstrip("\n"))
112
- body = "\n".join(lines).encode("utf-8")
113
- header = f"commit {len(body)}\0".encode("ascii") + body
114
- return hashlib.sha1(header, usedforsecurity=False).hexdigest()
107
+ parts = [
108
+ f"tree {tree}",
109
+ *[f"parent {parent}" for parent in parents],
110
+ f"author {author['name']} <{author['email']}> {author['date']}",
111
+ f"committer {committer['name']} <{committer['email']}> {committer['date']}",
112
+ ]
113
+ body = ("\n".join(parts) + "\n\n").encode("utf-8") + message.encode("utf-8")
114
+ wrapped = f"commit {len(body)}\0".encode("ascii") + body
115
+ return hashlib.sha1(wrapped, usedforsecurity=False).hexdigest()
116
+
117
+
118
+ def _parse_identity_line(line: str) -> dict[str, str]:
119
+ """Parse ``author Name <email> timestamp tz`` from a commit object line."""
120
+ _, rest = line.split(" ", 1)
121
+ name_email, timestamp, tz = rest.rsplit(" ", 2)
122
+ if name_email.endswith(">") and " <" in name_email:
123
+ name, email = name_email.rsplit(" <", 1)
124
+ email = email[:-1]
125
+ else:
126
+ name, email = name_email, ""
127
+ return {"name": name, "email": email, "date": f"{timestamp} {tz}"}
115
128
 
116
129
 
117
130
  def parse_commit(sha: str, root: Path | None = None) -> dict[str, Any]:
118
131
  root = root or repo_root()
119
- fmt = "%H%x00%T%x00%P%x00%an%x00%ae%x00%at%x00%cn%x00%ce%x00%ct%x00%B"
120
- parts = _run_git("show", "-s", f"--format={fmt}", sha, cwd=root).split("\0")
121
- if len(parts) < 10:
122
- raise RuntimeError(f"unexpected git show output for {sha}")
123
- commit_sha, tree, parents_raw, an, ae, at, cn, ce, ct, body = parts[:10]
124
- parents = [p for p in parents_raw.split() if p]
125
- author = {"name": an, "email": ae, "date": _git_date(int(at))}
126
- committer = {"name": cn, "email": ce, "date": _git_date(int(ct))}
132
+ commit_sha = _run_git("rev-parse", sha, cwd=root)
133
+ raw = _run_git("cat-file", "commit", commit_sha, cwd=root, strip=False)
134
+ tree = ""
135
+ parents: list[str] = []
136
+ author: dict[str, str] | None = None
137
+ committer: dict[str, str] | None = None
138
+ for line in raw.splitlines():
139
+ if line.startswith("tree "):
140
+ tree = line.split(" ", 1)[1]
141
+ elif line.startswith("parent "):
142
+ parents.append(line.split(" ", 1)[1])
143
+ elif line.startswith("author "):
144
+ author = _parse_identity_line(line)
145
+ elif line.startswith("committer "):
146
+ committer = _parse_identity_line(line)
147
+ if not tree or author is None or committer is None or "\n\n" not in raw:
148
+ raise RuntimeError(f"unexpected commit object for {sha}")
149
+ body = raw.split("\n\n", 1)[1]
127
150
  actual_id = compute_commit_id(
128
151
  tree=tree,
129
152
  parents=parents,
@@ -178,7 +201,7 @@ def build_commit_envelope(
178
201
  if msg_file.is_file():
179
202
  msg = msg_file.read_text(encoding="utf-8")
180
203
  else:
181
- msg = _run_git("log", "-1", "--pretty=%B", cwd=root) if parents else ""
204
+ msg = _run_git("log", "-1", "--pretty=%B", cwd=root, strip=False) if parents else ""
182
205
  msg = msg or ""
183
206
  commit_block = {
184
207
  "expected_id": compute_commit_id(
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "matrixscroll"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Open protocol for signing AI-assisted code provenance with Ed25519; shipping software root of trust with SSX360 hardware support in progress."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -99,6 +99,40 @@ class VerifyCommandTests(_RunMixin, unittest.TestCase):
99
99
  self.assertEqual(rc, 0)
100
100
  self.assertTrue(json.loads(out)["ok"])
101
101
 
102
+ def test_verify_require_mode_passes_for_matching_mode(self):
103
+ with tempfile.TemporaryDirectory() as tmp, _isolated_env(Path(tmp)):
104
+ path = self._signed_path(Path(tmp), {"release": "policy-ok"})
105
+ rc, out = self._run(["verify", str(path), "--require-mode", "emulated"])
106
+ self.assertEqual(rc, 0)
107
+ self.assertTrue(json.loads(out)["ok"])
108
+
109
+ def test_verify_require_mode_fails_for_mismatch(self):
110
+ with tempfile.TemporaryDirectory() as tmp, _isolated_env(Path(tmp)):
111
+ path = self._signed_path(Path(tmp), {"release": "policy-fail"})
112
+ rc, out = self._run(["verify", str(path), "--require-mode", "hardware"])
113
+ self.assertEqual(rc, 2)
114
+ self.assertIn("required mode hardware", json.loads(out)["error"])
115
+
116
+ def test_verify_trusted_keys_passes_for_signer_key(self):
117
+ with tempfile.TemporaryDirectory() as tmp, _isolated_env(Path(tmp)):
118
+ path = self._signed_path(Path(tmp), {"release": "trusted-ok"})
119
+ signed = json.loads(path.read_text(encoding="utf-8"))
120
+ pub = signed["signature"]["public_key"]
121
+ keys_path = Path(tmp) / "trusted-keys.json"
122
+ keys_path.write_text(json.dumps({"trusted_public_keys": [pub]}), encoding="utf-8")
123
+ rc, out = self._run(["verify", str(path), "--trusted-keys", str(keys_path)])
124
+ self.assertEqual(rc, 0)
125
+ self.assertTrue(json.loads(out)["ok"])
126
+
127
+ def test_verify_trusted_keys_fails_for_unknown_key(self):
128
+ with tempfile.TemporaryDirectory() as tmp, _isolated_env(Path(tmp)):
129
+ path = self._signed_path(Path(tmp), {"release": "trusted-fail"})
130
+ keys_path = Path(tmp) / "trusted-keys.json"
131
+ keys_path.write_text(json.dumps({"trusted_public_keys": ["wrong-key"]}), encoding="utf-8")
132
+ rc, out = self._run(["verify", str(path), "--trusted-keys", str(keys_path)])
133
+ self.assertEqual(rc, 2)
134
+ self.assertIn("trusted set", json.loads(out)["error"])
135
+
102
136
 
103
137
  class SignCommandTests(_RunMixin, unittest.TestCase):
104
138
  def test_sign_then_verify_roundtrip(self):
@@ -47,6 +47,24 @@ def test_sign_and_verify_commit_envelope():
47
47
  assert verify_manifest(signed)
48
48
 
49
49
 
50
+ def test_parse_commit_matches_git(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
51
+ import subprocess
52
+
53
+ from matrixscroll.git import parse_commit
54
+
55
+ subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True)
56
+ subprocess.run(["git", "config", "user.email", "dev@example.com"], cwd=tmp_path, check=True)
57
+ subprocess.run(["git", "config", "user.name", "Dev"], cwd=tmp_path, check=True)
58
+ sample = tmp_path / "hello.txt"
59
+ sample.write_text("hello\n", encoding="utf-8")
60
+ subprocess.run(["git", "add", "hello.txt"], cwd=tmp_path, check=True)
61
+ subprocess.run(["git", "commit", "-m", "init"], cwd=tmp_path, check=True)
62
+ sha = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=tmp_path, text=True).strip()
63
+ parsed = parse_commit(sha, tmp_path)
64
+ assert parsed["actual_id"] == sha
65
+ assert parsed["message"].startswith("init")
66
+
67
+
50
68
  def test_build_commit_envelope_in_repo(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
51
69
  import subprocess
52
70
 
File without changes
File without changes
File without changes
File without changes