# geth `geth` is a personal, local-first mesh runtime for scripts, devices, databases, documents, blobs, pipes, and future multi-user collaboration. This project is not the Ethereum `geth` client. The project and executable are still named `geth`. ## One Binary There is one executable: `geth`. It has daemon mode and control mode: ```sh geth init geth daemon run geth daemon service install geth status geth node id geth resource list geth cas add ./file ``` The daemon owns local identity, the Iroh endpoint, trust state, resource registry, module router, local metadata store, and synchronized data structures. Most non-daemon commands talk to the daemon through a local Unix socket at `$GETH_HOME/run/geth.sock`. `geth init --admin-key --signing-key --node-name ` records an owner/admin keychain, the local user/device/node binding, and signs canonical keychain payloads through `ssh-keygen -Y sign` using the `geth.keychain.v1@geth.local` namespace. This is the bootstrap path for admin/YubiKey-rooted trust. `geth keychain sync ` pulls the signed keychain operation log from an imported peer and imports only operations with valid OpenSSH signatures from currently trusted admin keys. `geth node list` shows the active reduced node view, and `geth node rename/revoke` require `--signing-key` so device-management changes can replicate as verified admin statements. `geth node grant/revoke-grant` and `geth auth grant/revoke` also require `--signing-key` in the CLI and store signed auth operations for replication. SSH certificate-flow and revocation records carry agent-key signed provenance over canonical payloads, and sync import rejects new unsigned or invalidly signed records. The daemon can also install itself as a user service: ```sh geth daemon service install geth daemon service status geth daemon service uninstall ``` The bootstrap service managers are systemd user units on Linux, launchd user agents on macOS, and per-user scheduled tasks on Windows. These are user-level services, not system services. ## Transport And SSH All remote node-to-node geth communication is designed to happen over Iroh only. SSH is not a geth transport backend, and there is no SSH fallback transport. The default node config uses Iroh's default relay policy for practical connectivity; set `[iroh].relay_mode = "disabled"` for local-only/offline development. Named custom relay maps can be selected with `relay_mode = "custom"` and `relay_map = ""`. Iroh local-network discovery is enabled by default with `[iroh].local_discovery = true`. SSH keys are used as admin trust anchors and ecosystem integration points. OpenSSH, FIDO, and YubiKey-backed keys can sign geth trust objects through canonical geth envelopes with explicit namespaces such as `geth.keychain.v1@geth.local`. SSH proxying carries SSH protocol bytes over an authorized Iroh stream, but SSH is still not a geth transport backend. SSH certificate request and renewal flows are managed as geth metadata. A node can create a certificate request, another machine can approve it and receive an explicit `ssh-keygen -s ...` command suitable for a CA key or YubiKey-backed CA, or pass `--sign` to run `ssh-keygen` immediately and import the resulting `-cert.pub` for distribution. Certificate and key revocation entries are tracked locally and can be exported as JSONL or as an OpenSSH KRL specification file or a binary OpenSSH KRL generated through `ssh-keygen -k`. `geth ssh cert sync ` and `geth ssh revocation sync ` pull certificate-flow and revocation metadata from an authorized peer over Iroh. ## MVP Features The bootstrap implementation provides: - `geth guide [init|owner-setup|enrollment|keys|overlay|service|completions|smoke-test]` for embedded workflow help, including `--admin-key` / `--signing-key` setup examples - `geth completions ` for shell completion scripts generated from the live CLI command tree - `geth init` - `geth init --admin-key --signing-key --node-name ` - `geth daemon run` - `geth daemon service install|uninstall|start|stop|status|print` - `geth status` - `geth node id` - `geth node list` - `geth node enroll request --node-name --capability [--out ]` - `geth node enroll submit [--request-id |--path ]` - `geth node enroll import ` - `geth node enroll list [--status pending|approved|rejected]` - `geth node enroll approve --signing-key ` - `geth node enroll sync ` - `geth node rename --signing-key ` - `geth node revoke --signing-key ` - `geth node endpoint-add --signing-key ` - `geth node endpoint-revoke --signing-key ` - `geth node grant --signing-key [--grant-id ]` - `geth node revoke-grant --signing-key ` - `geth peer export [--out ]` - `geth peer import ` - `geth peer list` - `geth peer ping ` - `geth peer auth-check ` - optional overlay-network planning: `geth overlay status`, `geth overlay plan [--cidr 172.22.0.0/24]`, `geth overlay join --secret [--cidr 172.22.0.0/24]`, `geth overlay interface-plan [--platform linux|macos|windows]`, `geth overlay up [--bearer-secret ] [--mtu 1280]`, `geth overlay down `, `geth overlay peers `, `geth overlay send --packet-base64 `, `geth overlay recv `, and `geth overlay leave `. Join persists local overlay membership, creates the overlay resource when needed, assigns a deterministic virtual IP, and stores only a BLAKE3 fingerprint of the supplied secret. If the overlay resource already has bearer invites, join requires a bearer token with `overlay.join`. Packet send validates IPv4 packets and carries them over the dedicated `/geth/overlay/1` Iroh ALPN after `overlay.route` authorization. `overlay up` creates a real L3 TUN/Wintun-style interface through `tun-rs`, reads IPv4 packets from that interface, maps destination overlay IPs to imported peer cards, and routes packets over `/geth/overlay/1`. Creating the interface is explicit opt-in and may require `CAP_NET_ADMIN`, sudo, or platform-specific network entitlements. - `geth resource list` - `geth resource create ` - `geth keychain init [--admin-key ] [--signing-key ]` - `geth keychain status` - `geth keychain admin-add --admin-key --signing-key [--principal ]` - `geth keychain admin-revoke --signing-key ` - `geth keychain allowed-signers` - `geth keychain verify` - `geth keychain sync ` - `geth auth sync ` - `geth sync status` - `geth sync now [node-id-or-name]` - `geth secret status` - `geth secret create ` - `geth secret rotate ` - `geth secret bearer create --capability ` - `geth secret bearer list` - `geth secret bearer challenge --capability ` - `geth secret bearer prove --nonce --capability ` - `geth secret bearer verify --nonce --response --capability ` - `geth secret bearer revoke ` - `geth auth explain ` - `geth auth grant --signing-key [--grant-id ]` - `geth auth revoke --signing-key ` - local filesystem CAS commands: `add`, `get`, `fetch`, `hash`, `has`, `pin`, `unpin`, `cleanup`, `providers`, `list`; remote fetch accepts `--bearer-secret ` - private CAS envelope commands: `geth cas add-private ` and `geth cas get-private --out `. These use local resource secret epochs and are a prototype envelope, not audited AEAD. - local CAS tree objects describe file trees and are stored as CAS blobs - local file-root commands: `geth cas root add/list/scan/sync/apply`; root sync pulls authorized remote tree metadata and CAS tree bytes into a peer-qualified remote root, and apply materializes a tree without deleting files or overwriting local edits. Repeated syncs keep the previous imported remote tree as the base and record durable conflicts when local and remote roots both changed. - local file conflict metadata commands: `geth cas conflict record/list/resolve` - local DB resource registration: `geth db add ` and `geth db status ` with schema and `crsql_changes` metadata; the DB crate and daemon can extract typed local `crsql_changes` batches through `geth db changes ` and exchange authorized remote batches with `geth db sync ` - local SQLite-backed KV commands: `geth kv create/set/get`; `kv set` accepts `--subject ` to exercise local capability checks for non-local callers. The daemon mirrors named KV stores into Iroh Documents and `geth kv sync [--bearer-secret ]` pulls authorized remote updates after receiving a read-only docs ticket through geth control. - local Automerge document commands: `geth document create/status/set/get`; CLI input and output are JSON views, while the store keeps durable Automerge save bytes. `geth document sync [--bearer-secret ]` pulls authorized remote Automerge state. - lossy pubsub wakeups: `geth pubsub pub/sub`; local messages are retained in a daemon-lifetime ring buffer, while authorized remote publish/subscribe joins deterministic native `iroh-gossip` topics after geth control authorization - SSH certificate flow metadata: - `geth ssh cert request --public-key --principal [--subject ]` - `geth ssh cert requests [--subject ]` - `geth ssh cert approve --ca-key [--sign] [--subject ]` - `geth ssh cert import --cert [--subject ]` - `geth ssh cert list [--subject ]` - `geth ssh cert sync [--bearer-secret ]` - `geth ssh revocation add [--subject ]` - `geth ssh revocation list [--subject ]` - `geth ssh revocation export --out [--format jsonl|openssh-krl-spec|openssh-krl] [--subject ]` - `geth ssh revocation import [--format jsonl|openssh-krl-spec] [--subject ]` - `geth ssh revocation sync [--bearer-secret ]` - SSH proxy/admin over Iroh: `geth ssh proxy [--bearer-secret ]` and `geth ssh admin-shell [--bearer-secret ]` - pipe registry/message commands: `geth pipe listen [--node ] [--bearer-secret ]`, `geth pipe connect [--node ] [--bearer-secret ]`, `geth pipe send [message|--in |--in -] [--node ] [--bearer-secret ]`, `geth pipe recv [--peek]`, and `geth pipe forward-tcp --listen 127.0.0.1: --node --target 127.0.0.1:` or `geth pipe forward-unix --listen /tmp/local.sock --node --target /tmp/remote.sock` `geth peer export/import/list` is for untrusted peer-card exchange. Peer cards include the Iroh EndpointID plus currently known relay/direct addresses. `geth peer ping ` uses the local daemon's Iroh endpoint to dial an imported peer card and exchange a signed candidate-only peer-card ping. `geth peer auth-check ` sends a protected Iroh control request: the remote daemon verifies that the caller's signed peer card binds the actual Iroh EndpointID before reducing resource-local auth ops. `geth cas fetch ` uses the same protected Iroh control path as an authorization preflight. The remote daemon verifies the caller's signed peer card against the observed Iroh EndpointID and requires `cas.fetch` on `resource:cas:local`. After that preflight succeeds, the requester fetches the blob payload over native `iroh-blobs` (`/iroh-bytes/4`) on the same daemon-owned Iroh endpoint, verifies the BLAKE3 hash, stores it in local CAS, and records the serving peer as a provider visible with `geth cas providers `. `geth-iroh` is pinned to `iroh 0.95.1` and compiles the native backend libraries `iroh-blobs 0.97.0`, `iroh-docs 0.95.0`, and `iroh-gossip 0.95.0` against the same daemon-owned endpoint generation. KV stores are mirrored into native `iroh-docs` namespaces and peers receive read-only document tickets only after geth authorization succeeds. Pubsub joins native `iroh-gossip` topics only after the geth control path has authenticated the peer-card endpoint binding and checked the topic capability. Remote resource commands that accept `--bearer-secret` can also authorize with a resource-scoped bearer proof generated from the private bearer token returned at creation time. The persisted auth log stores a public bearer id and token verifier, not the private token. This does not enroll the caller as a trusted node; it only unlocks the requested capability on that one resource. `geth ssh cert sync ` requires `ssh_cert.sync` on `resource:ssh:certs` at the peer. `geth ssh revocation sync ` requires `ssh_revocation.sync` on `resource:ssh:revocations`. Both commands merge authorized peer SSH distribution log entries into the local store for offline listing and later approval/signing workflows. The current log is materialized from signed certificate requests, signed certificate imports, and signed revocation records, then reduced locally; it is not a mutable remote ACL blob. While the daemon is running, it also performs a background live-sync tick for known peers. The default interval is 30 seconds and can be changed in `config.toml` with `[sync] live_sync_enabled` and `live_sync_interval_ms`. Live-sync stores per-peer high-water cursors in local metadata so repeated ticks request only newer SSH certificate-flow and revocation log entries. Sync import preserves local metadata by rejecting conflicting records with ids that already exist locally. Before probing individual modules, the daemon asks the peer for an authorized sync-status summary over Iroh. The peer only returns stream watermarks for resources where the caller already has the matching capability, letting the local daemon skip unchanged or unauthorized streams. File roots advertise `cas-tree:` watermarks when the caller has `cas.fetch`; background live-sync imports updated tree metadata and CAS tree bytes into peer-qualified remote roots without writing files. When a previous imported remote tree is available, sync compares that base against the current local same-named root and the newly imported remote tree. Concurrent edit, delete/edit, and divergent rename conflicts are recorded in the local conflict table for later resolution. `geth cas root apply --to ` can then materialize that tree locally: without a registered local root base it creates missing directories/files, never deletes extra files, never overwrites differing local files, and records conflicts for manual resolution. When the target path is a registered local file root with a previous scan, apply uses a three-way base/local/remote check and can safely apply non-conflicting remote creates, updates, deletes, and renames only where local state still matches the recorded base. Named KV stores participate in the same live-sync loop once they exist locally: manual `geth kv sync ` and background ticks require `kv.read` on the remote `resource:kv:`. Authorized sync imports from the remote Iroh Documents namespace where available, keeps SQLite as the durable local index, and imports only remote entries that are not older than the local value. Remote pubsub publish uses the protected Iroh control path as an authorization preflight. The remote peer requires `pubsub.publish` on `resource:pubsub:` before recording the message and broadcasting it on a deterministic native `iroh-gossip` topic. Pubsub remains lossy and is not durable storage; facts that must survive restart or reconcile offline belong in CAS, KV, document, or DB resources. Remote pubsub subscribe uses the same protected path, requires `pubsub.subscribe` on `resource:pubsub:`, joins the gossip topic, and returns the peer's current daemon-lifetime snapshot for that topic. Remote pipe connect uses the same protected Iroh control path and requires `pipe.connect` on `resource:pipe:`. The current prototype records a remote connection attempt and whether a listener exists. `geth pipe send [message|--in |--in -] --node ` uses the dedicated `/geth/pipe/1` Iroh ALPN to write a byte message to an authorized peer listener, and `geth pipe recv ` drains local daemon-lifetime messages. Remote pipe listen uses the same protected path: `geth pipe listen --node ` requires `pipe.listen` on `resource:pipe:` before registering a daemon-lifetime listener on the peer. `geth pipe forward-tcp --listen 127.0.0.1: --node --target 127.0.0.1:` starts a local loopback TCP listener. Each accepted connection asks the local daemon to open an authorized `/geth/pipe/1` byte stream to the peer. The remote daemon validates the signed endpoint/card binding and requires `pipe.forward` on `resource:pipe-tcp:` before connecting to the remote loopback TCP target. This is loopback-only in the prototype to avoid turning geth into an accidental open proxy. `geth pipe forward-unix --listen --node --target ` uses the same `/geth/pipe/1` byte stream and requires `pipe.forward` on `resource:pipe-unix:` before connecting to the remote Unix socket. Unix socket paths must be absolute. `geth ssh proxy ` is usable as an OpenSSH `ProxyCommand`: the CLI opens a local daemon stream, the daemon opens the dedicated `/geth/ssh-proxy/1` Iroh ALPN, the remote daemon validates the caller's endpoint/card binding and requires `ssh_proxy.connect` on `resource:ssh-proxy:local`, and only then connects the stream to `127.0.0.1:22`. SSH remains normal OpenSSH on top of that byte stream; SSH is not a geth transport backend. `geth ssh admin-shell ` is a restricted geth admin workflow over the protected Iroh control path. It requires `ssh_proxy.admin_shell` on the same resource and supports only built-in commands (`help`, `status`, `node-id`); it does not execute host shell commands. Document sync exchanges durable Automerge state with a JSON view for CLI output: manual `geth document sync ` and background live-sync require `document.read` on `resource:document:` and merge authorized remote state when the peer advertises a state timestamp at or after the local document. DB sync is a staged cr-sqlite path: manual `geth db sync ` and background live-sync require `db.sync` on `resource:db:`, exchange typed `crsql_changes` batches over the protected Iroh control path, and check remote schema metadata against the local DB before applying and advancing the per-peer cursor. Compatible batches are inserted into the local `crsql_changes` table or view; for real cr-sqlite databases, loading/configuring cr-sqlite remains the database owner's responsibility. The prototype does not use CAS-backed DB snapshots or change-batch blobs; that is deferred until large initial catch-up needs it. The automated tests use deterministic `crsql_changes` fixtures because this dev environment does not provide a `sqlite3` CLI or cr-sqlite extension artifact for a real extension-backed integration test. Importing or pinging a peer card never grants capabilities by itself. When `[iroh].local_discovery = true`, the daemon also advertises and discovers signed peer cards on LAN using a geth-specific mDNS TXT payload. That payload is candidate metadata only; all geth node-to-node requests still run over Iroh. ## Resource Modules Everything meaningful is modeled as a resource. Planned resource kinds are: - `db`: SQLite/cr-sqlite synchronization - `kv`: Iroh Documents backed key-value stores - `pipe`: dumbpipe-like byte streams over Iroh; the bootstrap has a local daemon registry only - `document`: Automerge documents over Iroh streams - `pubsub`: lossy notifications over native `iroh-gossip` after geth authorization, with only an in-memory daemon-lifetime ring buffer - `cas`: content-addressed blob storage and distribution - `ssh-proxy`: authorized SSH proxy/admin access over Iroh Authorization is resource-scoped and capability-based. Bearer secrets may grant specific resource capabilities but do not create trusted node identity. The auth evaluator supports scoped KV write grants such as `kv.write_prefix:apps/foo/` for `kv.write_key:apps/foo/config` explain checks. `geth kv set --subject ` enforces those local grants for test callers; the local node/agent still has owner access for local administration. SSH certificate and revocation commands also accept `--subject ` on local metadata operations to exercise the same capability checks: certificate requests/read/approval/import use `ssh_cert.request`, `ssh_cert.read`, `ssh_cert.approve`, and `ssh_cert.import` on `resource:ssh:certs`, while revocation publish/read/import use `ssh_revocation.publish`, `ssh_revocation.read`, and `ssh_revocation.import` on `resource:ssh:revocations`. `geth auth explain ` is the operator-facing debug path for those decisions. Human output includes the allow/deny result, the reason, evaluated auth-op count, and compact diagnostics. JSON output includes the same diagnostics so scripts can distinguish discovered-only peers, unknown subjects, missing or matched endpoint bindings, missing grants, revoked grants, and bearer-secret access without scraping prose. ## Local State If `GETH_HOME` is set, geth uses it. Otherwise it uses an OS-specific data directory. The bootstrap layout is: ```text $GETH_HOME/ geth.sqlite config.toml identity/agent.ed25519 identity/iroh.ed25519 cas/blobs/ run/geth.sock ``` ## Quick Start In one shell: ```sh export GETH_HOME="$(mktemp -d)" cargo run -p geth -- init cargo run -p geth -- daemon run ``` In another shell: ```sh export GETH_HOME="" cargo run -p geth -- status cargo run -p geth -- node id echo "hello geth" > /tmp/hello-geth.txt cargo run -p geth -- cas add /tmp/hello-geth.txt cargo run -p geth -- cas list ``` ## Owner And Node Management The intended owner setup is SSH-admin-rooted: ```sh geth init \ --admin-key ~/.ssh/id_ed25519_sk.pub \ --signing-key ~/.ssh/id_ed25519_sk \ --node-name laptop \ --capability resource:ssh-proxy:local=ssh_proxy.admin_shell ``` When any owner setup option is used, both `--admin-key` and `--signing-key` are required. This prevents accidentally creating an unsigned owner/device/node statement that cannot be accepted by another node during keychain sync. This records signed keychain operations for `KeychainInit`, `AdminKeyAdd`, `UserAdd`, `DeviceAdd`, `NodeAdd`, and `AgentBind`. The current node identity is stable above endpoint rotation: future endpoint bindings should attach to the node, not replace it. Node management is done through the reduced keychain view: ```sh geth node list geth node rename laptop work-laptop --signing-key ~/.ssh/id_ed25519_sk geth node endpoint-add work-laptop --signing-key ~/.ssh/id_ed25519_sk geth node grant work-laptop resource:ssh-proxy:local ssh_proxy.connect \ --signing-key ~/.ssh/id_ed25519_sk geth node revoke-grant resource:ssh-proxy:local \ --signing-key ~/.ssh/id_ed25519_sk geth node revoke work-laptop --signing-key ~/.ssh/id_ed25519_sk ``` Admin SSH keys are managed through the same signed keychain log: ```sh geth keychain admin-add \ --admin-key ~/.ssh/new_admin.pub \ --signing-key ~/.ssh/id_ed25519_sk \ --principal admin geth keychain allowed-signers > /tmp/geth.allowed_signers geth keychain allowed-signers --out /tmp/geth.allowed_signers geth keychain sign-file \ --in /tmp/authorized_keys \ --out /tmp/authorized_keys.sig \ --signing-key ~/.ssh/id_ed25519_sk geth keychain verify-file \ --in /tmp/authorized_keys \ --signature /tmp/authorized_keys.sig geth keychain sigchain --out /tmp/geth.sigchain.jsonl geth keychain publish-bundle \ --out ./public/.well-known/sshsigchain \ --signing-key ~/.ssh/id_ed25519_sk \ --snapshot authorized_keys=/tmp/authorized_keys geth keychain verify-checkpoint \ --checkpoint /tmp/geth.sigchain.checkpoint.json \ --signature /tmp/geth.sigchain.checkpoint.json.sig \ --sigchain /tmp/geth.sigchain.jsonl \ --allowed-signers /tmp/geth.allowed_signers geth keychain fetch --url https://example.com/.well-known/sshsigchain/ --import geth keychain verify-sigchain --in /tmp/geth.sigchain.jsonl geth keychain import-sigchain --in /tmp/geth.sigchain.jsonl geth keychain explain geth keychain explain-signer geth keychain verify ``` The keychain follows a sigchain model documented in `docs/sigchain-keychain.md`: each keychain operation is accepted only if it is signed by an admin key from the previously accepted reduced view. This is the geth analogue of verifying `git-skm` allowed-signers changes from a prior trusted state. The reusable mechanics live in the `geth-keychain` crate, including application-specific signature profiles, allowed-signers projection, replay verification through a caller-provided verifier trait, and an appendable JSONL sigchain file format suitable for static hosting with HTTP caching/range requests. The default discovery/publication base is `https://example.com/.well-known/sshsigchain/`; publish bundles contain `allowed_signers`, `geth.sigchain.jsonl`, `geth.sigchain.checkpoint.json`, and `geth.sigchain.checkpoint.json.sig`. Clients can verify checkpoints, fetch bundles, import verified sigchains, and remember the last accepted checkpoint to reject older static bundles from the same source. `keychain fetch --url` is the retrieval location, so local `file://` mirrors work for testing; the signed checkpoint still records the advertised publication base URL, and `verify-checkpoint --base-url` can pin that value when needed. Signing is mediated by OpenSSH. `--signing-key` may point at a private key file, a FIDO/YubiKey OpenSSH security-key stub, or a public key whose private half is available in `ssh-agent`. For encrypted private keys, the recommended workflow is to unlock the key with `ssh-add` and pass the public key path. For PKCS#11 tokens, load the key into `ssh-agent` with `ssh-add -s ` and use the exported public key path; direct PKCS#11 signing is not exposed by `ssh-keygen -Y sign` in a portable way. The enrollment flow for a new node is: ```sh # On the new node, initialize local state and trust the owner's admin public key: geth init geth daemon run geth keychain init --admin-key ~/.ssh/id_ed25519_sk.pub # Then create a signed enrollment request: geth node enroll request \ --node-name workstation \ --capability resource:ssh-proxy:local=ssh_proxy.connect \ --out /tmp/workstation-enrollment.json # Either submit over Iroh to an imported owner peer: geth node enroll submit owner-laptop --path /tmp/workstation-enrollment.json # Or import the JSON on the owner/YubiKey machine: geth node enroll import /tmp/workstation-enrollment.json geth node enroll list --status pending geth node enroll approve --signing-key ~/.ssh/id_ed25519_sk # Back on the new node, pull signed identity and authorization state: geth sync now owner-laptop geth sync status ``` Enrollment requests are signed by the requesting agent key. Approval records signed keychain operations for the new device/node/agent binding and signed auth operations for requested resource capabilities. The requesting node must already know the owner's admin public key so it can verify the signed operation logs before importing them. `geth sync now` pulls both signed logs from the owner node through the same path used by background live sync. `geth keychain sync ` pulls signed keychain operations from an imported peer over Iroh and rejects operations that do not have a valid OpenSSH signature from a currently trusted admin key over the canonical keychain payload. This is the current replicated device-management substrate. It is still a pull-based operation log, not yet a CRDT or Keyhive-style convergent authority. The daemon also runs best-effort live sync for imported peers. `geth sync now [node]` triggers the same sync pass immediately, and `geth sync status` reports the last local attempt, success, cursor, import count, rejection count, and error per peer stream. `geth sync status --json` also includes per-stream `state`, `stale`, `stale_after_ms`, and `next_action` fields so smoke tests can fail on stale or failed streams. Keychain and auth sync now use per-peer high-water cursors, while receivers still verify every imported signed operation before it can affect the reduced keychain or authorization views. ## Two-Machine Smoke Test Use two terminals or machines with different `GETH_HOME` values. Owner machine: ```sh export GETH_HOME=/tmp/geth-owner geth init --admin-key ~/.ssh/id_ed25519_sk.pub \ --signing-key ~/.ssh/id_ed25519_sk \ --node-name owner geth daemon run geth peer export --out /tmp/owner.peer.json ``` New node: ```sh export GETH_HOME=/tmp/geth-node geth init geth daemon run geth keychain init --admin-key ~/.ssh/id_ed25519_sk.pub geth peer import /tmp/owner.peer.json geth node enroll request --node-name workstation \ --capability resource:cas:local=cas.fetch \ --capability resource:kv:notes=kv.read \ --capability resource:document:notes=document.read \ --capability resource:db:notes=db.sync \ --capability resource:ssh-proxy:local=ssh_proxy.connect \ --out /tmp/workstation-enrollment.json geth node enroll submit owner --path /tmp/workstation-enrollment.json ``` Owner machine: ```sh geth node enroll list --status pending geth node enroll approve --signing-key ~/.ssh/id_ed25519_sk geth node grant workstation resource:ssh-proxy:local ssh_proxy.connect \ --signing-key ~/.ssh/id_ed25519_sk echo "hello geth" > /tmp/hello-geth.txt geth cas add /tmp/hello-geth.txt geth kv create notes geth kv set notes greeting "hello geth" geth document create notes geth document set notes '{"greeting":"hello geth"}' geth ssh cert requests ``` New node: ```sh geth sync now owner geth sync status --json geth peer ping owner geth cas fetch owner geth kv sync owner notes geth kv get notes greeting geth document sync owner notes geth document get notes geth db add notes /path/to/crsqlite-notes.sqlite geth db sync owner notes geth ssh cert request --public-key ~/.ssh/id_ed25519.pub --principal "$USER" geth ssh cert sync owner geth ssh proxy owner ``` If a command fails, the daemon error includes a `next:` line for common recovery paths such as importing a peer card, running `auth explain`, granting a missing capability, or creating/registering a missing resource. ## Authorization Direction The MVP defines the split between: - keychain: SSH-rooted users, devices, nodes, agents, and endpoint bindings - auth: resource-local signed authorization operations and capability grants - secrets: resource master secrets, epochs, envelopes, and bearer access The current code does not implement Keyhive, BeeKEM, strong forward secrecy, or post-compromise security. It leaves room for future local-first, replicated auth logs and BeeKEM/CGKA-style group key evolution.