diff --git a/README.md b/README.md index d61b828..a438d23 100644 --- a/README.md +++ b/README.md @@ -145,11 +145,14 @@ sdk = "^0.1" # optional plugin API range (manifest `compat.plugin-api`) records a per-entry `signature_status` (`verified` | `unverified` | `unsigned`); no entry can be `verified` until a key-configured verification phase lands, so the current output is `unsigned`. -- Official `manifest_hash` values pin the owning repository's - `bitty-plugin.toml` bytes at the submodule revision recorded by - `git ls-tree HEAD plugins/`. When an official pin moves (submodule bump - or upstream manifest change), re-pin the hash in the same reviewed change: - read the manifest bytes at the new pin (for example +- Official `manifest_hash` values (Phase 1, H-A: raw-bytes digest) pin the + owning repository's `bitty-plugin.toml` bytes at the submodule revision + recorded by `git ls-tree HEAD plugins/`. This is a SHA-256 digest over + the fetched transport bytes (H-A), not semantic canonical hashing (H-B). + Future phases will distinguish H-A from H-B via explicit version tagging. + When an official pin moves (submodule bump or upstream manifest change), + re-pin the hash in the same reviewed change: read the manifest bytes at the + new pin (for example `git -C show :bitty-plugin.toml | sha256sum`), write `manifest_hash = "sha256:"` into `registry/official/.toml`, then run `just registry-generate` and `just registry-validate` before committing diff --git a/registry/official/activity.toml b/registry/official/activity.toml index 900b56d..ec044c5 100644 --- a/registry/official/activity.toml +++ b/registry/official/activity.toml @@ -4,10 +4,11 @@ # downloads) is intentionally absent; `just registry-sync` mirrors the plugin's # bitty-plugin.toml into the optional `metadata` object of the generated index. # -# Integrity (Phase 1, advisory): `manifest_hash` is the sha256 digest of the -# owning repository's `bitty-plugin.toml` at the pinned submodule revision -# recorded by `git ls-tree HEAD plugins/activity`. Refresh it when the pin -# moves (see README "Registry model" refresh procedure). +# Integrity (Phase 1, advisory): `manifest_hash` is the sha256 digest (H-A: raw +# bytes) of the owning repository's `bitty-plugin.toml` at the pinned submodule +# revision recorded by `git ls-tree HEAD plugins/activity`. This is a digest +# over fetched transport bytes (H-A), not semantic canonical hashing (H-B). +# Refresh it when the pin moves (see README "Registry model" refresh procedure). id = "bitty-featured.activity" name = "Bitty Activity" diff --git a/registry/official/file-manager.toml b/registry/official/file-manager.toml index f721618..829d413 100644 --- a/registry/official/file-manager.toml +++ b/registry/official/file-manager.toml @@ -4,10 +4,11 @@ # downloads) is intentionally absent; `just registry-sync` mirrors the plugin's # bitty-plugin.toml into the optional `metadata` object of the generated index. # -# Integrity (Phase 1, advisory): `manifest_hash` is the sha256 digest of the -# owning repository's `bitty-plugin.toml` at the pinned submodule revision -# recorded by `git ls-tree HEAD plugins/file-manager`. Refresh it when the pin -# moves (see README "Registry model" refresh procedure). +# Integrity (Phase 1, advisory): `manifest_hash` is the sha256 digest (H-A: raw +# bytes) of the owning repository's `bitty-plugin.toml` at the pinned submodule +# revision recorded by `git ls-tree HEAD plugins/file-manager`. This is a digest +# over fetched transport bytes (H-A), not semantic canonical hashing (H-B). +# Refresh it when the pin moves (see README "Registry model" refresh procedure). id = "bitty-terminal.file-manager" name = "File Manager" diff --git a/registry/official/git-panel.toml b/registry/official/git-panel.toml index 80f6c15..afd5e74 100644 --- a/registry/official/git-panel.toml +++ b/registry/official/git-panel.toml @@ -4,10 +4,11 @@ # downloads) is intentionally absent; `just registry-sync` mirrors the plugin's # bitty-plugin.toml into the optional `metadata` object of the generated index. # -# Integrity (Phase 1, advisory): `manifest_hash` is the sha256 digest of the -# owning repository's `bitty-plugin.toml` at the pinned submodule revision -# recorded by `git ls-tree HEAD plugins/git-panel`. Refresh it when the pin -# moves (see README "Registry model" refresh procedure). +# Integrity (Phase 1, advisory): `manifest_hash` is the sha256 digest (H-A: raw +# bytes) of the owning repository's `bitty-plugin.toml` at the pinned submodule +# revision recorded by `git ls-tree HEAD plugins/git-panel`. This is a digest +# over fetched transport bytes (H-A), not semantic canonical hashing (H-B). +# Refresh it when the pin moves (see README "Registry model" refresh procedure). id = "bitty-terminal.git-panel" name = "Git Panel" diff --git a/registry/official/palette.toml b/registry/official/palette.toml index 60fa3dd..6b66e35 100644 --- a/registry/official/palette.toml +++ b/registry/official/palette.toml @@ -4,10 +4,11 @@ # downloads) is intentionally absent; `just registry-sync` mirrors the plugin's # bitty-plugin.toml into the optional `metadata` object of the generated index. # -# Integrity (Phase 1, advisory): `manifest_hash` is the sha256 digest of the -# owning repository's `bitty-plugin.toml` at the pinned submodule revision -# recorded by `git ls-tree HEAD plugins/palette`. Refresh it when the pin -# moves (see README "Registry model" refresh procedure). +# Integrity (Phase 1, advisory): `manifest_hash` is the sha256 digest (H-A: raw +# bytes) of the owning repository's `bitty-plugin.toml` at the pinned submodule +# revision recorded by `git ls-tree HEAD plugins/palette`. This is a digest +# over fetched transport bytes (H-A), not semantic canonical hashing (H-B). +# Refresh it when the pin moves (see README "Registry model" refresh procedure). id = "bitty-terminal.palette" name = "Palette" diff --git a/registry/official/statusline.toml b/registry/official/statusline.toml index e78b2a4..68f47c6 100644 --- a/registry/official/statusline.toml +++ b/registry/official/statusline.toml @@ -4,10 +4,11 @@ # downloads) is intentionally absent; `just registry-sync` mirrors the plugin's # bitty-plugin.toml into the optional `metadata` object of the generated index. # -# Integrity (Phase 1, advisory): `manifest_hash` is the sha256 digest of the -# owning repository's `bitty-plugin.toml` at the pinned submodule revision -# recorded by `git ls-tree HEAD plugins/statusline`. Refresh it when the pin -# moves (see README "Registry model" refresh procedure). +# Integrity (Phase 1, advisory): `manifest_hash` is the sha256 digest (H-A: raw +# bytes) of the owning repository's `bitty-plugin.toml` at the pinned submodule +# revision recorded by `git ls-tree HEAD plugins/statusline`. This is a digest +# over fetched transport bytes (H-A), not semantic canonical hashing (H-B). +# Refresh it when the pin moves (see README "Registry model" refresh procedure). id = "bitty-terminal.statusline" name = "Statusline" diff --git a/registry/official/wheel.toml b/registry/official/wheel.toml index 22c03f9..f84d058 100644 --- a/registry/official/wheel.toml +++ b/registry/official/wheel.toml @@ -4,10 +4,11 @@ # downloads) is intentionally absent; `just registry-sync` mirrors the plugin's # bitty-plugin.toml into the optional `metadata` object of the generated index. # -# Integrity (Phase 1, advisory): `manifest_hash` is the sha256 digest of the -# owning repository's `bitty-plugin.toml` at the pinned submodule revision -# recorded by `git ls-tree HEAD plugins/wheel`. Refresh it when the pin -# moves (see README "Registry model" refresh procedure). +# Integrity (Phase 1, advisory): `manifest_hash` is the sha256 digest (H-A: raw +# bytes) of the owning repository's `bitty-plugin.toml` at the pinned submodule +# revision recorded by `git ls-tree HEAD plugins/wheel`. This is a digest +# over fetched transport bytes (H-A), not semantic canonical hashing (H-B). +# Refresh it when the pin moves (see README "Registry model" refresh procedure). id = "bitty-terminal.wheel" name = "Wheel" diff --git a/registry/schema.json b/registry/schema.json index 27bd444..4a16dc5 100644 --- a/registry/schema.json +++ b/registry/schema.json @@ -27,7 +27,7 @@ }, "manifest_hash": { "type": "string", - "description": "Optional canonical-form manifest digest (H-B): an algorithm-prefixed lowercase hex digest. Advisory in this phase; shape-checked, recorded in the index, and not cryptographically verified.", + "description": "Optional raw-manifest digest (H-A, Phase 1): an algorithm-prefixed lowercase hex digest over the fetched bitty-plugin.toml bytes at the pinned revision. Advisory in this phase; shape-checked, recorded in the index, and not cryptographically verified. Future phases will distinguish raw-bytes (H-A) from semantic canonical hashing (H-B) via explicit version tagging.", "pattern": "^[a-z0-9]+:[0-9a-f]{32,128}$", "maxLength": 160 }, diff --git a/tests/registry.test.ts b/tests/registry.test.ts index fda24b7..fd5c08d 100644 --- a/tests/registry.test.ts +++ b/tests/registry.test.ts @@ -688,6 +688,78 @@ describe("optional integrity fields", () => { }); expect(errors.some((message) => message.includes("signature"))).toBe(false); }); + + test("manifest_hash in Phase 1 is H-A (raw bytes), not H-B (canonical)", () => { + // Phase 1: manifest_hash is SHA-256 over the fetched bitty-plugin.toml bytes + // at the pinned revision. This is H-A (raw transport bytes), not H-B (semantic + // canonical hashing). Two semantically identical manifests with different + // formatting will have different H-A digests. + const manifest1 = ` +[plugin] +id = "example.plugin" +version = "1.0.0" +`; + const manifest2 = ` +[plugin] +id="example.plugin" +version="1.0.0" +`; + + // Same semantic content, different formatting -> different H-A digests + const hash1 = require("crypto") + .createHash("sha256") + .update(manifest1) + .digest("hex"); + const hash2 = require("crypto") + .createHash("sha256") + .update(manifest2) + .digest("hex"); + + expect(hash1).not.toBe(hash2); + + // Both are valid manifest_hash values in Phase 1 (H-A) + const errors1 = errorsOf({ + ...baseEntry, + manifest_hash: `sha256:${hash1}`, + }); + const errors2 = errorsOf({ + ...baseEntry, + manifest_hash: `sha256:${hash2}`, + }); + + expect(errors1.some((message) => message.includes("manifest_hash"))).toBe( + false, + ); + expect(errors2.some((message) => message.includes("manifest_hash"))).toBe( + false, + ); + }); + + test("manifest_hash digest algorithm is versioned for future H-B migration", () => { + // The algorithm prefix (e.g., "sha256:") allows future H-B canonical + // hashing to use a different prefix (e.g., "sha256-canonical-v1:") to + // distinguish from H-A without breaking existing entries. + const errors = errorsOf({ + ...baseEntry, + manifest_hash: + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + }); + expect(errors.some((message) => message.includes("manifest_hash"))).toBe( + false, + ); + + // Future canonical digest would use a different prefix + // (not yet implemented, so this would fail validation in Phase 1) + const futureErrors = errorsOf({ + ...baseEntry, + manifest_hash: + "sha256-canonical-v1:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + }); + // Phase 1 only accepts simple algorithm names without version suffixes + expect( + futureErrors.some((message) => message.includes("manifest_hash")), + ).toBe(true); + }); }); describe("duplicate detection", () => {