Repository navigation
Sign attestations with a key held elsewhere; render a view's tree without touching disk - #242
Merged
geekgonecrazy merged 12 commits intoOct 8, 2026
Conversation
`attest_value` splits into `prepare_attestation` (author, content hash and the exact bytes to sign) and `attach_proof`, so a signer that keeps its key outside this process — a browser's WebCrypto, a hardware token — produces the same `eddsa-jcs-2022` attestation the CLI does. `attest_value` is now those two halves around a local `Signer`. `atomic-canonical-wasm` exposes that to the browser: prepare, attach and verify attestations, plus the DID and canonical JSON of a document, all over JSON strings and byte arrays. `build.sh` builds it with wasm-bindgen; `smoke.mjs` signs with a non-extractable WebCrypto key, verifies, and checks tampering is caught. (cherry picked from commit e46c8a8)
`atomic intent attest --prepare` prints the document an attestation signs and the exact bytes to sign, needing only the identity's public key. `--signed <file>` records an attestation a key holder produced from it: it must be signed by `--identity`'s key and attest the intent as it is now, so a signature over a stale or altered intent is refused. This lets a sandbox that holds only an agent's public identity attest as that agent, with the key kept by a signing service outside it. (cherry picked from commit c643ed7)
Self-signed request tokens now carry `aud`: the bare server URL they were minted for, normalised. A server that checks it refuses a token minted for somewhere else, so one leaked from one server can't be replayed at another within its five minutes. Verifiers that ignore unknown claims are unaffected. (cherry picked from commit d06dee6)
`Repository::materialize_view_entries` hands each entry of a view — path, inode, kind, mode, bytes, content hash, conflict-marker line — to a sink, in path order with directories first, plus the view's Merkle state. Read-only: no working tree, stat cache or conflict state is written, so it runs beside other readers. It serves a remote sandbox its tree and baseline. The per-file rendering is now one function, `render_view_file`, shared with `materialize_parallel`, which keeps writing to disk as before. (cherry picked from commit 238384f)
`.atomic-sandbox` showed up as untracked inside a sandbox, so `record --all` would record it; a remote pointer carries a token. Ignore it, and the sandbox's local cache `.atomic-sandbox.d`, like `.atomic`. (cherry picked from commit ac56c30)
A draft's structural changes (files it adds, moves, deletes) wait in the deferred tree journal until the view is checked out; TREE is the checked-out view's. Rendering any other view — materialize_view_entries, so every caller of it — now projects that view's journal in a write transaction that is thrown away. Before, a file added on a draft view never appeared on it. The vault-indexing half of the original commit served the remote-sandbox cache and is not carried over. (cherry picked from commit 081e6eb)
materialize_view_to (behind `sandbox stage` and `seal`) listed files from TREE, which is the checked-out view's: a file another view added was missing from its image. It now writes materialize_view_entries, which projects the view's own tree. (cherry picked from commit 76aef1d)
A key that gets range-scanned is big-endian, so its bytes sort in the same order as the numbers they encode; a value that is only ever looked up by its exact key can be little-endian. The two rules meet in files that handle both, so decode an id with that type's own `from_bytes` rather than slicing bytes by hand — a mis-decoded id is indistinguishable from an absent one at runtime. Recorded at the encoders in `pristine/tables.rs` and as a section in AGENTS.md. Also carries two fixes to the in-memory view render from the original commit: `view_tree_items` fell through to `TREE` — the *current* view's — when it could not open a write transaction to project another view's, so a read-only repository answered a question about one view with another's tree; it now refuses. And its test. (cherry picked from commit 3e7ca25)
atomic-canonical-wasm named the crate it wraps twice over — atomic, twice — and it is the only wasm crate in the workspace, so the extra precision cost a word and bought nothing. atomic-wasm says the same thing in half the length, and the module a page imports becomes atomic_wasm. atomic-canonical itself keeps its name: that is the crate this one wraps, and it is a dependency, not a rename of it. Verified: builds for the host and for wasm32-unknown-unknown (787 KB artifact at the path build.sh expects), full workspace suite 9088 tests across 57 binaries, 0 failures. (cherry picked from commit 9b7defc)
…ses? One test binary, two roles: the parent holds a Repository open and re-runs itself as the child; the child tries to open the same repository and reports what happened. Whether the lock is cross-process-exclusive is an assumption every single-writer consumer (the CLI's bounded open-wait, any daemon-side gate) relies on, so the answer is recorded as a test. Carried over from the remote-sandbox branch, where measuring how far the owner could go under concurrent sandboxes made the question load-bearing. (cherry picked from commit 677d300)
PrepareAttestation was unimplemented and RecordAttestation loaded a secret key and signed server-side, so an identity whose key is held elsewhere (a browser, a hardware token, a signing service) could not attest over the service layer — and any caller could mint an attestation under the daemon's default human key. The CLI's `intent attest --prepare/--signed` routed there, so the external-signer feature the surface was built for failed on the service path. - PrepareAttestation (intents): returns the document and the exact bytes a signer elsewhere must sign — the same preparation the local body computes — with only the identity's public key, gated on the pre-attest violations signing cannot fill. - RecordAttestation (intents): with a caller-supplied signature, no secret key is loaded. The signature must verify against the target AS IT IS NOW (a stale or altered intent hashes differently, so an old signature cannot land), the identity must be named — an unbound caller cannot borrow the daemon's default human key — and the proof attached is exactly the one the signature earned. Without a signature, plain attest still signs locally, as before. - The CLI routes `--prepare`/`--signed` through the service layer and prints the same JSON the local path prints. - `intent|memory validate --json` over the service path prints the same top-level report shape the local body prints (the wire carries each violation as one message string, so an entry is its message). This is the contract's attestation semantics (CONTRACT "Sandbox and vault writes": preparation returns public signing bytes under a read capability; recording verifies the supplied signature against the current target).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes the keep-worthy half of #224, retargeted at the libatomic stack (#239): the remote-sandbox/owner-protocol work is deliberately not carried — that moves to outpost, which will implement the libatomic transport over iroh instead of the CLI owning a second protocol. The original branch stays for mining.
Signing with a key held elsewhere
attest_valuesplits intoprepare_attestation(the document and the exact bytes to sign) andattach_proof, so a signer that keeps its key outside this process produces the sameeddsa-jcs-2022attestation the CLI does.atomic intent attest --prepareprints what to sign;--signed <file>records the result.atomic-wasmexposes that to the browser: a page holding an Ed25519 key in WebCrypto signs and verifies ordinary atomic attestations, without the key ever leaving the browser.aud), so a token leaked from one server can't be replayed at another within its lifetime.record_attestationpreviously loaded a secret key and signed server-side):PrepareAttestationreturns the signing bytes under a read capability;RecordAttestationwith a caller-supplied signature loads no secret key, requires a named identity (no daemon-default human key), and verifies the signature against the target as it is now — a stale or altered intent hashes differently, so an old signature cannot land. This is the contract's attestation semantics end to end, and it is what the Remote sandboxes over the owner protocol, and signing with a key held elsewhere #224 handlers review flagged.Render a view's tree without touching disk
Repository::materialize_view_entriesstreams a view's tree — paths, inodes, modes, rendered bytes, per-entry hashes, conflict-marker lines — from the database, touching no working tree, no stat cache. This is the kernel the libatomic Materialize surface wants.sandbox stage/sealrender through the same kernel, so a view's own added files appear in its image.Smaller keeps
record --allcould pick up a credential).from_byteslesson.intent|memory validate --jsonover the service path prints the same top-level report shape as the local body (entries are message-only: the wire carries violations as strings — noted in the code).Testing
cargo test --workspace: 69 binaries, 9136 tests, 0 failures (including the external-signer integration test through the service layer, routing parity, and the provenance RPC sink).cargo fmt --checkclean; the clippy error set is identical to the base branch's. The #230 root-finder fix is not carried — the libatomic branch already has it.