Skip to content

Sign attestations with a key held elsewhere; render a view's tree without touching disk - #242

Merged
geekgonecrazy merged 12 commits into
feat/libatomic-contractfrom
feat/external-signing-and-diskless-render
Oct 8, 2026
Merged

geekgonecrazy merged 12 commits into
feat/libatomic-contractfrom
feat/external-signing-and-diskless-render

Conversation

@geekgonecrazy

Copy link
Copy Markdown
Contributor

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_value splits into prepare_attestation (the document and the exact bytes to sign) and attach_proof, so a signer that keeps its key outside this process produces the same eddsa-jcs-2022 attestation the CLI does. atomic intent attest --prepare prints what to sign; --signed <file> records the result.
  • atomic-wasm exposes 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.
  • Tokens name the server they are for (aud), so a token leaked from one server can't be replayed at another within its lifetime.
  • Service-layer wiring (the stack's record_attestation previously loaded a secret key and signed server-side): PrepareAttestation returns the signing bytes under a read capability; RecordAttestation with 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_entries streams 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.
  • Another view's tree is a projection (its deferred tree journal applied in a thrown-away write transaction); a read-only repository refuses rather than answering with the checked-out view's tree.
  • sandbox stage/seal render through the same kernel, so a view's own added files appear in its image.

Smaller keeps

  • A sandbox's pointer is never treated as untracked (record --all could pick up a credential).
  • The storage layer's endianness rule (BE keys that range-scan, LE opaque values) written down at the encoders and in AGENTS.md, with the decode-with-your-own-from_bytes lesson.
  • A test pinning whether the pristine's file lock is exclusive across processes.
  • intent|memory validate --json over 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 --check clean; 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.

`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).
@geekgonecrazy
geekgonecrazy merged commit cc01851 into feat/libatomic-contract Oct 8, 2026
@geekgonecrazy
geekgonecrazy deleted the feat/external-signing-and-diskless-render branch October 8, 2026 19:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant