Hi!
Hi Yves - thanks for publishing ctxpkg. I am aligned with the goal: a vendor-neutral, portable, verifiable context package format for agents.
Small context: I recently contributed Augment integration work to LeanCTX, which is how I got pulled deeper into this ecosystem. I admire the work you are doing and agree with the direction you are taking with portable context packages. I wanted to make sure I understand the spec correctly before opening an RFC or example PR.
I tried to follow the Discussion-first path in GOVERNANCE.md / CONTRIBUTING.md, but hit a 404 (permission? too early to the party?), so I am opening this as an issue instead.
I wanted to start with the purpose question before sending a PR, because this feels like a spec decision rather than a code cleanup.
The short
The trust mechanism is brittle because v1 asks verifiers to hash a re-serialized JSON value. Different runtimes produce different bytes for the same logical content, so an honest non-Rust producer can create a package that fails verification. For v1, the simplest fix is to make content_hash seal the literal content bytes the producer wrote, and define exactly how a verifier identifies those bytes. That makes verification boring: read bytes, SHA-256 bytes, compare. Semantic equivalence across producers is a separate registry/dedup problem and can get its own later hash/RFC.
For transparency and simplicity here is the recipe to run the proof script:
git clone -b canonical-content-hash https://github.com/parker-brown-family/ctxpkg.git
cd ctxpkg/tools/canonical-json-proof
./prove.sh
Or if you prefer to have it run internally for context building and agent learning something like 🤖 :
Write a tiny Python script that loads ctxpkg's public security-review example, extracts `content`, serializes it with compact original order, compact sorted keys, top-level reordered keys, and default spaced JSON, then prints byte length + SHA-256 for each. The point is to prove same logical JSON content gives different hashes when canonicalization is not specified.
Why I am raising this
The governance doc says CTXPKG is:
- vendor-neutral: "designed to be implemented by any agent, editor, runtime, or workflow"
- minimal: "the spec stays small and focused"
- governed through Discussion -> RFC -> Review -> Decision -> Implementation
The spec's integrity section says:
- the integrity block is required
- it is verified on every load
- a package is rejected if any integrity check fails
That reads to me like the v1 integrity block is primarily about artifact integrity / tamper detection: can a loader prove this package's content bytes are the same bytes the producer sealed?
But the current v1 wording defines:
content_hash = SHA-256 of serde_json::to_string(content) (canonical compact JSON)
---> PROBLEM <----
That creates an interoperability problem for non-Rust producers. It also quietly makes content_hash do a second job: not only "was this artifact changed?" but also "can independent producers normalize equivalent content to the same semantic representation?"
Those are related, but they are not the same requirement.
What I found
Using the public examples/security-review/security-review-1.2.0.ctxpkg content, the same logical content object produces different hashes under reasonable serializer choices:
| Serialization |
Bytes |
SHA-256 |
| JS/Python compact, preserve order |
2545 |
7fbc53711d5bf083048cf0c8d7644b5f08c622fef3f9a522cecf2a0200413e1f |
| Deep sorted keys |
2545 |
8a7f5b8fe53b00c6c8853a8d2e98b89786269f721117af74c3612fbc50a1bb1d |
| Same top-level content reordered |
2545 |
b26e948c34a7216f31f64e060a8effddd2c1ef5838fda0c7e91bd31ae872111b |
| Python default with spaces |
2667 |
2dd7568dff3e823d36c60f5ad8a485b2a8c533e9d56c929ca80572cba8e3c2ff |
I also tested a minimal session package against the LeanCTX reference implementation:
session fields in the Rust schema order, with the hash computed over that order: imports successfully.
- The same logical
session object with fields reordered, with the hash computed over the reordered compact JSON: rejected with content_hash mismatch.
- The same reordered file as #2, but with the hash computed over the LeanCTX schema-order bytes: imports successfully.
So the current verifier appears to have a stable de facto canonical form, but it is not fully specified in language-neutral terms. A non-Rust producer has to reverse-engineer serializer behavior rather than implement a clear format rule.
I put together a small proof harness for this, but I am not including shell code inline in the issue. I do not expect anyone to run generated proof scripts without review. The useful part is the method and result table; if helpful, I can share the harness separately with a read-first README and static fixtures so an agent or maintainer can reproduce the core test safely in a throwaway directory.
Proof harness: available if useful; omitted here to keep the issue focused.
The purpose split
I think the key question is:
What is content_hash for in v1?
If the v1 goal is verify this package on load, then the simplest and most portable rule may be:
content_hash = SHA-256 of the literal serialized content bytes as written by the producer
In other words: the producer serializes content once, hashes those exact bytes, and the loader hashes the same bytes. No parser re-serialization, no Rust-specific behavior, no JSON number-formatting dispute.
That lines up with the spec's "verified on every load" language and with the governance principle that v1 should stay minimal.
I tested this direction against an adversarial fixture set. Hashing literal content bytes avoids the canonicalization failure modes entirely because numbers, Unicode, key order, whitespace, and null/default reshaping are never re-serialized by the verifier.
The real cost is also worth saying plainly: in the current single-file JSON shape, byte-preservation needs an unambiguous way to identify the exact content byte range. A verifier cannot parse content into a normal JSON value and later recover the original bytes. So v1 would need one precise byte-isolation rule, such as:
- define the hashed byte range for the
content value in the package file, or
- store
content as an opaque encoded payload, or
- split the sealed content bytes from the manifest in a future package layout.
I am not assuming which of those is best. I am saying the byte-preservation path seems more aligned with v1's stated integrity goal than trying to make every producer imitate one library's serializer.
Why I would not lead with a dual hash in v1
There is a clean larger design here:
{
"integrity": {
"content_bytes_sha256": "...",
"semantic_sha256": "..."
}
}
One hash would seal the actual artifact bytes. Another would identify semantically equivalent content across producers for registries, deduplication, package comparison, or agent-to-agent exchange.
That separation feels architecturally right.
But for v1, it may be too much. Your governance doc explicitly says the spec should stay "small and focused" and that features belonging in implementations should stay in implementations. A second semantic hash requires choosing a canonicalization standard, shipping test vectors, deciding number/string/null/default rules, and defining what equivalence means.
That seems like a good future RFC once registry/dedup requirements are concrete, not something v1 needs in order to verify packages on load.
Possible v1 direction
A small v1 clarification could be:
- State that v1
content_hash is for artifact integrity / tamper detection, not semantic equivalence.
- Define exactly which bytes are hashed.
- Avoid requiring
serde_json::to_string(content) as the cross-language rule.
- Reserve semantic canonicalization for a future RFC, possibly as an optional
semantic_hash or registry-level field.
If the intended v1 goal is instead cross-producer semantic identity, then a language-neutral canonicalization standard is required. JCS / RFC 8785 is one option, but it brings number-formatting, Unicode, safe-integer, and null/default-shape decisions that v1 may not need if the current goal is simply "verify on load."
Let's make it award winning!
Hi!
Hi Yves - thanks for publishing ctxpkg. I am aligned with the goal: a vendor-neutral, portable, verifiable context package format for agents.
Small context: I recently contributed Augment integration work to LeanCTX, which is how I got pulled deeper into this ecosystem. I admire the work you are doing and agree with the direction you are taking with portable context packages. I wanted to make sure I understand the spec correctly before opening an RFC or example PR.
I tried to follow the Discussion-first path in
GOVERNANCE.md/CONTRIBUTING.md, but hit a 404 (permission? too early to the party?), so I am opening this as an issue instead.I wanted to start with the purpose question before sending a PR, because this feels like a spec decision rather than a code cleanup.
The short
The trust mechanism is brittle because v1 asks verifiers to hash a re-serialized JSON value. Different runtimes produce different bytes for the same logical content, so an honest non-Rust producer can create a package that fails verification. For v1, the simplest fix is to make
content_hashseal the literal content bytes the producer wrote, and define exactly how a verifier identifies those bytes. That makes verification boring: read bytes, SHA-256 bytes, compare. Semantic equivalence across producers is a separate registry/dedup problem and can get its own later hash/RFC.For transparency and simplicity here is the recipe to run the proof script:
Or if you prefer to have it run internally for context building and agent learning something like 🤖 :
Why I am raising this
The governance doc says CTXPKG is:
The spec's integrity section says:
That reads to me like the v1 integrity block is primarily about artifact integrity / tamper detection: can a loader prove this package's content bytes are the same bytes the producer sealed?
But the current v1 wording defines:
---> PROBLEM <----
That creates an interoperability problem for non-Rust producers. It also quietly makes
content_hashdo a second job: not only "was this artifact changed?" but also "can independent producers normalize equivalent content to the same semantic representation?"Those are related, but they are not the same requirement.
What I found
Using the public
examples/security-review/security-review-1.2.0.ctxpkgcontent, the same logicalcontentobject produces different hashes under reasonable serializer choices:7fbc53711d5bf083048cf0c8d7644b5f08c622fef3f9a522cecf2a0200413e1f8a7f5b8fe53b00c6c8853a8d2e98b89786269f721117af74c3612fbc50a1bb1db26e948c34a7216f31f64e060a8effddd2c1ef5838fda0c7e91bd31ae872111b2dd7568dff3e823d36c60f5ad8a485b2a8c533e9d56c929ca80572cba8e3c2ffI also tested a minimal
sessionpackage against the LeanCTX reference implementation:sessionfields in the Rust schema order, with the hash computed over that order: imports successfully.sessionobject with fields reordered, with the hash computed over the reordered compact JSON: rejected withcontent_hash mismatch.So the current verifier appears to have a stable de facto canonical form, but it is not fully specified in language-neutral terms. A non-Rust producer has to reverse-engineer serializer behavior rather than implement a clear format rule.
I put together a small proof harness for this, but I am not including shell code inline in the issue. I do not expect anyone to run generated proof scripts without review. The useful part is the method and result table; if helpful, I can share the harness separately with a read-first README and static fixtures so an agent or maintainer can reproduce the core test safely in a throwaway directory.
Proof harness: available if useful; omitted here to keep the issue focused.
The purpose split
I think the key question is:
If the v1 goal is verify this package on load, then the simplest and most portable rule may be:
In other words: the producer serializes
contentonce, hashes those exact bytes, and the loader hashes the same bytes. No parser re-serialization, no Rust-specific behavior, no JSON number-formatting dispute.That lines up with the spec's "verified on every load" language and with the governance principle that v1 should stay minimal.
I tested this direction against an adversarial fixture set. Hashing literal content bytes avoids the canonicalization failure modes entirely because numbers, Unicode, key order, whitespace, and null/default reshaping are never re-serialized by the verifier.
The real cost is also worth saying plainly: in the current single-file JSON shape, byte-preservation needs an unambiguous way to identify the exact
contentbyte range. A verifier cannot parsecontentinto a normal JSON value and later recover the original bytes. So v1 would need one precise byte-isolation rule, such as:contentvalue in the package file, orcontentas an opaque encoded payload, orI am not assuming which of those is best. I am saying the byte-preservation path seems more aligned with v1's stated integrity goal than trying to make every producer imitate one library's serializer.
Why I would not lead with a dual hash in v1
There is a clean larger design here:
{ "integrity": { "content_bytes_sha256": "...", "semantic_sha256": "..." } }One hash would seal the actual artifact bytes. Another would identify semantically equivalent content across producers for registries, deduplication, package comparison, or agent-to-agent exchange.
That separation feels architecturally right.
But for v1, it may be too much. Your governance doc explicitly says the spec should stay "small and focused" and that features belonging in implementations should stay in implementations. A second semantic hash requires choosing a canonicalization standard, shipping test vectors, deciding number/string/null/default rules, and defining what equivalence means.
That seems like a good future RFC once registry/dedup requirements are concrete, not something v1 needs in order to verify packages on load.
Possible v1 direction
A small v1 clarification could be:
content_hashis for artifact integrity / tamper detection, not semantic equivalence.serde_json::to_string(content)as the cross-language rule.semantic_hashor registry-level field.If the intended v1 goal is instead cross-producer semantic identity, then a language-neutral canonicalization standard is required. JCS / RFC 8785 is one option, but it brings number-formatting, Unicode, safe-integer, and null/default-shape decisions that v1 may not need if the current goal is simply "verify on load."
Let's make it award winning!