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.
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/CHANGELOG.md +17 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/PKG-INFO +7 -8
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/README.md +6 -7
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/AGENTIC_AI_SECURITY.md +7 -9
- matrixscroll-0.2.2/docs/WHITEPAPER.md +144 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/quickstart-git.md +11 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/__init__.py +1 -1
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/cli.py +62 -6
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/git.py +44 -21
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/pyproject.toml +1 -1
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_cli.py +34 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_git_envelope.py +18 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/.gitignore +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/CONTRIBUTING.md +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/LICENSE +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/SECURITY.md +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/SPEC.md +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/controls/agentic_ai_controls.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/hardware-provider.md +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/superpowers/plans/2026-06-19-ci-action-plan.md +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/superpowers/plans/2026-06-19-sdk-refactor-plan.md +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/superpowers/specs/2026-06-19-matrixscroll-git-design.md +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/yubikey-bridge.md +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/agentic_ai_evidence_manifest.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/agentic_ai_evidence_manifest.signed.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/ci/protected-branch.yml +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/commit-envelope.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/commit-envelope.signed.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/demo/agent-commit-demo.sh +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/demo/generate_signed_examples.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/release-manifest.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/examples/release-manifest.signed.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/_core.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/canonical.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/constants.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/errors.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/hooks/post-commit +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/hooks/pre-push +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/manifest.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/policy.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/providers/__init__.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/providers/base.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/providers/emulated.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/providers/hardware.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/providers/registry.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/providers/yubikey.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/matrixscroll/py.typed +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/schemas/commit-envelope.v1.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/schemas/evidence-pack.v1.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/schemas/release-manifest.v1.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/__init__.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_agentic_guidance.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_canonical.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_core.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_policy.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_release_metadata.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_vectors.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/tests/test_yubikey_provider.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/README.md +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/_fixture_key.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/_generate.py +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/tampered_algorithm.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/tampered_device_id.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/tampered_field.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/tampered_nested.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/tampered_public_key.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/tampered_schema.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/tampered_signature.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/unsigned_empty_block.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/unsigned_no_block.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/valid_nested.json +0 -0
- {matrixscroll-0.2.0 → matrixscroll-0.2.2}/vectors/valid_simple.json +0 -0
- {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.
|
|
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
|
-
**
|
|
40
|
+
**Signed provenance for agent-assisted Git commits — verify offline, one command.**
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
102
|
+
matrixscroll-version: "0.2.2"
|
|
104
103
|
```
|
|
105
104
|
|
|
106
105
|
## CLI
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# Matrix Scroll
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Signed provenance for agent-assisted Git commits — verify offline, one command.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
|
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 |
|
|
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.
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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:
|
|
@@ -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
|
-
|
|
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":
|
|
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
|
|
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
|
-
|
|
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":
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/superpowers/plans/2026-06-19-ci-action-plan.md
RENAMED
|
File without changes
|
{matrixscroll-0.2.0 → matrixscroll-0.2.2}/docs/superpowers/plans/2026-06-19-sdk-refactor-plan.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|