Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>`. 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/<name>`. 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 <checkout> show <pin>:bitty-plugin.toml | sha256sum`), write
`manifest_hash = "sha256:<hex>"` into `registry/official/<name>.toml`, then
run `just registry-generate` and `just registry-validate` before committing
Expand Down
9 changes: 5 additions & 4 deletions registry/official/activity.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 5 additions & 4 deletions registry/official/file-manager.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 5 additions & 4 deletions registry/official/git-panel.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 5 additions & 4 deletions registry/official/palette.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 5 additions & 4 deletions registry/official/statusline.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 5 additions & 4 deletions registry/official/wheel.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion registry/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
72 changes: 72 additions & 0 deletions tests/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading