Repository navigation
Remote sandboxes over the owner protocol, and signing with a key held elsewhere - #223
Closed
geekgonecrazy wants to merge 16 commits into
Closed
geekgonecrazy wants to merge 16 commits into
geekgonecrazy wants to merge 16 commits into
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. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`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. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When an agent identity was named (option or ATOMIC_AGENT_IDENTITY) but couldn't be used — missing, not an agent, unreadable — recording fell back to the plus-tag author on the human's default key, putting a person's key on work an agent did, exactly when the caller said it was an agent's. It now records unkeyed instead, and says so in the log. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`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. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`.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`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Remote sandboxes use the database owner's protocol rather than a second one: iroh is another transport beside the local socket, answered by the same handler. Each remote frame carries a view-scoped sandbox token (in memory only, hashed); a remote caller gets Ping, the provenance requests and Materialize, each checked against its token — its own view, the sessions and turns it started, checkpoints only for changes its view can see. Opening, renewing and closing sandboxes, and shutdown, stay local. The client picks its route from .atomic-sandbox, so provenance from a remote sandbox goes through the same OwnerJournalSink as a local hook. `atomic sandbox create --remote` mints the token and writes the pointer (the owner's iroh address, the view, the token; 0600); `materialize` writes the view's tree inside the sandbox; `renew` and `close` manage the token. The owner's iroh key persists in .atomic so pointers survive a restart. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A change carries the repository's internal node ids and inode numbers,
so a sandbox that records without the repository must read the
repository's own rows. atomic-core's pristine::slice exports and imports
them byte for byte: a skeleton (the view's tree, inodes and positions,
directories, ids of every visible change) and, per record, a graph slice
for the inodes it touches (each file's content and name chain, one hop of
neighbours so find_block resolves as it does on the repository, its CRDT
rows and conflicts). The view is imported flattened, with the
repository's own Merkle state. Inodes the cache adds itself start at
2^62, clear of the repository's.
The change store can hold content spans without their change files, and
consults them first. Repository::{export,import}_sandbox_{skeleton,slice}
wrap both sides; sandbox_slice_inodes is what a cache asks for — changed
files and the directories on the way to changed or new paths.
status: a tracked file missing from disk is "already deleted" only when
the graph holds its vertex; a cache has the tree before the graph.
The parity test records the same edit on a repository and on a cache
built from it — modify, deletions spanning changes, delete, add (existing
and new directory), move, move and edit, and a second record after the
first lands — and requires identical hashes and bytes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two more requests on the owner protocol, checked against the sandbox's token like the rest: FileStates hands the cache the repository's rows and content for the inodes record will read (only inodes on the view), and SubmitChange takes the V3 bytes the cache recorded. The owner applies a submitted change only if it is what it claims and names nothing outside the view: the bytes hash to the claimed hash; the view is still at the state it was recorded against; every change it depends on or refers to that the repository knows is visible on the view; every node its file operations name is a visible change's (FileOps::referenced_node_ids); no path touches .atomic, .atomic-sandbox or .atomic-sandbox.d; and it is new. Submissions go in one at a time. Refusals write nothing. In the sandbox, materialize also builds the cache: an ordinary repository under .atomic-sandbox.d/cache whose working tree is the sandbox, so a remote pointer opens it and status, log and diff work locally. record hydrates first, records without saving or applying, submits, then takes the view's new skeleton and marks the tree clean. When a change lands, a path the sandbox added gets the repository's inode. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The cache holds a view's rows, not its content. diff and restore now load the content they compare against first (FileStates, as record does), and log fetches the change files the view has and the cache lacks — a new Changes request, answered only for changes on the token's view, each file kept only if its bytes hash to what it claims. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e lands The remote path moves out of individual commands into Repository, so every caller gets it — the atomic CLI and an agent's turn hooks alike. A process installs one RemoteSandboxLink (the atomic binary installs the owner client at startup); in a remote sandbox's cache, record hydrates first, write_recorded submits instead of applying (a change that didn't land fails the record), diff and restore hydrate, log fetches change files, and publish_provenance_checkpoint publishes the checkpoint in the repository. PublishProvenance, the owner's side: the sandbox's own session, and every change the provenance explains on its view; the graph arrives serialized, so the repository publishes what the sandbox hashed. Two authorization fixes the agent flow showed: asking after a session nobody has written to is allowed (the store answers "no such turn"), and BindCheckpointHash names a provenance graph, not a change. An agent's session-start, turn-start, turn-end and session-end in a remote sandbox now leave its change on the view and its session ledger and provenance in the repository. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The vault arrives as files, as after a pull, so building the cache bootstraps the vault tables from them; intents written in the sandbox are then recorded and land on the view like any other change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…vault 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 Materialize and the skeleton after a submit — now projects that view's journal in a write transaction that is thrown away, and the skeleton's tree is the view as rendered (implicit directories, inode 0, left out). Before, a file added on an expedition's draft never appeared on it. When a sandbox's change lands, the vault files it touched are indexed into the repository's vault from the view's content, as a pull does from the files it writes (vault_record_files, factored out of the working-copy deflate). An intent created in an expedition is in `intent list` again. Tests: record parity and a submitted change on a draft, a second record building on the first, and the agent flow on a draft view off dev. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ATOMIC_IROH_IPV4_ONLY=1 makes the owner endpoint and the client dialer open no IPv6 socket. Under some microVM network backends (smolvm TSI, in a fork) creating one takes the process down, so a sandbox inside such a VM could not materialize or submit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Superseded by #224. |
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.
What if you want to run an agent's work in a VM that mounts nothing from the host, and to keep the agent's keys where they belong.
Remote sandboxes
A VM works in a remote sandbox of a view: a tree materialized from the repository's owner over iroh, speaking the same database-owner protocol (no second protocol). Changes recorded in the sandbox land in the repository through the owner.
902ca4f), with the owner's iroh endpoints able to bind IPv4 only (d4c0d8d).9e8d77c,ae0537a). Remote sandboxes live insideRepository, so any caller records and provenance lands (b9a63e3).93e8af3). Never treat a sandbox's pointer file as untracked (83f319f).9aa898f). Expeditions' draft views render as themselves and have their vault indexed (3001ba7).e3cb25a), and stage and seal a view as it renders (555a7d2).Signing with a key held elsewhere
e95b122):atomic intent attest --prepareprints the document and the bytes to sign, and--signed <file>records an attestation made from them, checked against--identity's key and the intent as it is now (5c1dc77).aud(6875cd2).Merged
devdevis merged in, including Ed25519 change signing (#214). Inatomic-agent/src/identity.rsthe merge takesdev's version as-is. That supersedesb5ef87b("never record agent work under the human's key"):devfalls back to the default signer on purpose (a_human_identity_selected_falls_back_to_the_default_signer).Testing
cargo test -p atomic-agent --libpasses after the merge.