Home / Briefs / Telemetry from a published vault
Telemetry from a published vault
A build brief, written to be handed to the agent that will build the thing. The problem: a vault is about to be published with a read key, so every visitor can read every file in it — and it still needs to report anonymous usage back to its author. The answer is an append lane, and it works for one reason that is worth understanding before you write any code.
What to read first
| Page | What it gives you |
|---|---|
| Vault messaging | The worked example: two vaults exchanging encrypted messages, end to end, with curl |
| API — append lanes | The six endpoints, the gates, the limits and the status codes |
sg.append | The same transport from inside a vault app, plus the sg.on event push |
| PKI | Keypair lifecycle — sgit pki keygen / export / encrypt / decrypt |
| Sub-vaults | The other cross-vault mechanism — *.link.json, for reading across rather than writing |
One signpost, so you do not go looking. The Private Health Score vault looks like the two-party case study and is not one: it is a single vault with three audiences — patient/, doctor/ and shared/ — split by who is reading rather than by copying, where sharing with the clinician means handing over a derived read key. There is no second vault and no message passing in it.
The mechanism: four capabilities, deliberately split
| Capability | Holder | Can | Cannot |
|---|---|---|---|
append_token | the sender | write | list, fetch, read anything |
enum_key | the vault owner | list, fetch, mark-processed | write, purge |
write_key | the vault owner | configure, purge | — |
| private key | the vault owner | decrypt | never sent to the server |
The server stores SHA-256 of the first three and compares hashes. It never holds a raw capability key and never holds a private key at all. Two consequences decide the whole design:
- The write response is blind — exactly
{"ok": true}. No file ID, no count, no metadata. A sender cannot learn what else is in the lane, or whether theirs was the first write. - No account is needed to send.
POST /api/vault/append/write/{vault_id}requires no access token; theappend_tokenin the body is the entire gate.
The shape of it
PUBLISHED VAULT (read key public) TELEMETRY VAULT (private, yours) ─────────────────────────────── ────────────────────────────────── holds: append_token holds: enum_key, write_key, + telemetry vault_id private key + your PUBLIC key a reader reaches step 4 │ encrypt(event, your_public_key) │ POST /api/vault/append/write/{telemetry_vault_id} ├────────────────────────────────▶ gate: H(append_token) ∈ append_anchors? ◀──────────────────────────────── {"ok": true} ← blind │ you: list (free) → fetch → decrypt locally
Why publishing the token is acceptable — and where it stops being
Every visitor holds the published vault's read key, so every visitor can read the append_token out of it. That is not avoidable, and it is not the mistake it looks like: an append token grants write only. A visitor who extracts it cannot list the lane, cannot fetch anything, cannot read another reader's events — and because the write response is blind, cannot even tell whether the lane holds anything at all.
This is the one credential shape that survives being published inside a public vault. A vault key would not. A read key to a private vault would not: a read key decrypts everything, which is exactly the finding that has held two vaults back from publication here.
Events can be forged. Treat every row as an untrusted claim about what happened, never as fact. Encrypting to your public key gives you confidentiality, not proof of origin — the public key ships in the published vault, so anyone can produce a well-formed payload.
The lane can be flooded. The cap is 1000 pending files per token → 507, at which point your real telemetry stops arriving. This is the failure mode to design against, not the confidentiality one.
Mitigations, in the order they matter: register several append_anchors so one flooded lane does not bury the others and revoking a sender is removing one anchor; drain aggressively (list → fetch → mark-processed, then purge with folder:"processed"); rotate the token by re-configure-ing and shipping a new vault release; and rate-limit client-side so honest readers never approach the cap.
Answered: the two questions this brief told you to verify
sg.append.write survives a read-only session, warned that the bridge path was probably unavailable, and steered you to a direct fetch instead. That was wrong, and it sent the first agent who followed it down the harder road. The SG/API team reviewed the append code against this page and reported back; what follows is their answer, and it reverses this brief's recommendation. Their review is quoted below with permission — it is published CC BY 4.0.- Does
sg.append.writework from a read-only session? — YES. There is no read-only gate on append at all.
The bridge checks exactly one thing, a permission grant:perm.append.write === true. In the reviewer's words, "there is nowritablecheck, no consent gate, and no read-only branch anywhere in the append handler", andsg.app.writable"is irrelevant to append". So the grant is the whole story:{ "permissions": { "append": { "write": true } } }Nothing else is required. TheEREADONLYthis brief cited belongs tosg.vfs.write, which fails at the data-source layer on a different code path — but both deny with the same string,'Permission denied', which is precisely how the misdiagnosis happened. Do not use one to probe the other. - Does the frame's CSP allow
connect-srcto the API host? — NO, not by default. And this is the trap.
The frame is served withconnect-src blob: data:, so a directfetchto the API is blocked before it leaves. The escape hatch is"permissions": {"network": true}, which omits the CSP meta entirely — and which, as the reviewer puts it, appears "zero times" in the authoring guide and zero times in the first version of this page. It is discoverable only by reading the source.
Prefer the bridge anyway.network: trueis all-or-nothing: it reopens every egress from a frame that is holding decrypted vault content, which is a poor trade for a telemetry beacon. The reviewer's verdict on the tempting shortcut is worth keeping: "Not recommended: loosening the default CSP, or adding a read-only bypass for append." - Only
writecrosses vaults. Every other verb is bound to the vault that is open.
The bridge builds one client bound to the currently open vault, with the enum key derived from that vault's read key.list,fetch,markProcessed,purgeandconfiguretherefore always act on the open vault;writeis the sole verb that takes an explicitvault_id, which is exactly what cross-vault telemetry needs and the only thing that makes it possible.
This is a design boundary, not a gap: listing a remote lane would need the recipient's enum key, and putting that in a published app "would give every visitor read access to the whole lane." Drain where the enum key legitimately lives — the receiving vault's own dashboard, or the CLI. - Token format.
^[0-9a-f]{16,128}$— hex only. A prefixed token returns 400, not 403, because it fails input validation before reaching any gate. Do not paste asha256:CLI fingerprint in as a token; that is the most likely way to hit it.
.vault/releases.json) means app.json is read from that commit rather than HEAD, so a pushed manifest is simply not seen — and a pin also forces read-only for everyone, owner included; and a folder-level app.json beside a deep-linked entry page replaces the root manifest wholesale rather than merging with it, so root grants vanish without a warning.And one gap to design around. The intended model is append_token = H(recipient public key), so a sender could compute the lane address from a public bundle with no coordination. No shipped command emits the token — that derivation is PROPOSED, not shipped. Until it lands, treat the token as an opaque 64-hex secret you generate and register yourself.
Checked against the shipped CLI while writing this: sgit pki ships keygen, export, encrypt, decrypt, sign and verify. There is no append-lane command in the CLI at all, so the transport is HTTP or the browser bridge — nothing else.
Setting it up
# once, on the telemetry vault $ sgit pki keygen --label "Telemetry" $ sgit pki export sha256:FINGERPRINT > telemetry-identity.json $ curl -X POST https://send.sgraph.ai/api/vault/append/configure/$TELEMETRY_VAULT_ID \ -H "x-sgraph-vault-write-key: $WRITE_KEY" -H "Content-Type: application/json" \ -d '{"append_anchors":["<sha256 of the append_token>"], "enum_key_hash":"<sha256 of your enum_key>"}' # reading — metadata-only listing reads ZERO payloads, so poll with this $ curl -X POST https://send.sgraph.ai/api/vault/append/list/$TELEMETRY_VAULT_ID \ -H "x-sgraph-vault-enum-key: $ENUM_KEY" -d '{"include_content": false}'
Filenames are server-assigned as {epoch_ms}_{24-hex}.enc, so they sort chronologically — which is what makes cursor pagination stable. Page with after_file_id. mark-processed is idempotent: an already-moved file comes back in missing rather than as an error, so a retried batch is safe.
“Real time”, precisely
On the receiving side the kernel pushes sg.on('append.new-messages', …) on tab focus and app open — a dashboard app does not poll, it declares "host_events": ["append.new-messages", "append.error"] in app.json and reacts.
That means real time while you are looking at it, not a live stream. For anything closer, poll list with include_content: false; it is free.
What you can and cannot learn
Can: that the vault was opened, which screens were reached, how far a reader got, event ordering and timing, and a session journey via a random per-session id the app mints in memory.
Cannot: who. There is no identity in this system and no login. Every reader is anonymous by construction — which is usually the point, but it does mean “who opened the vault” is not an answerable question. Only how many.
Two disclosure points, and neither should be skipped:
- The platform's own default is that opening a vault never phones home. External embeds are click-to-load precisely so that it does not. A telemetry beacon breaks that expectation, so the vault should say so visibly, in the vault — not in a footnote. That is the same house rule every published vault page here follows: publish the work, then say exactly what it is and what it costs.
- The server sees lane activity, size and timing regardless of encryption, and it sees the requesting IP. “The server cannot read the content” is true. “The traffic is invisible” is not.
The prompt to hand the builder agent
Written to be pasted as-is. Substitute the vault ids.
Build a vault app for THE THING. It will be published with a read key, so assume every visitor can read every file in it, including any credential it carries.
Emit anonymous usage events to a separate telemetry vault over an append lane. Read
https://sgit.ai/docs/vault-messaging.htmlandhttps://sgit.ai/api/append-lanes.htmlfirst.Constraints:
- Send through the bridge:
sg.append.write({ vault_id, append_token, payload }), with{"permissions": {"append": {"write": true}}}inapp.json. That grant is the only requirement — a read-only session does not block it, andwriteis the one verb that takes another vault's id.- Do not reach for a direct
fetch()to the endpoint. The frame's CSP isconnect-src blob: data:and will block it silently; the only way round ispermissions.network: true, which reopens every egress from a frame holding decrypted vault content. If a grant appears to be ignored, check for a release pin or a folder-levelapp.jsonbefore you change approach.- Encrypt each payload to the telemetry vault's public key before sending. The public key ships in the published vault; that is fine, public keys are publishable.
- Never put a vault key, a read key to any other vault, or any other credential in this vault. The append token is the only credential that may appear, and only because it is write-only.
- Mint a random per-session id in memory. No names, no emails, no fingerprinting, nothing that identifies a person. Anonymous is the design, not a setting.
- Rate-limit client-side. The lane caps at 1000 pending files per token and returns 507 after that.
- Fail silently and never block on telemetry — if the POST fails, the reader must not notice.
- Put a visible, plain-English notice in the vault saying that anonymous usage events are sent and what they contain. Opening a vault does not normally phone home; this one does, so say so.
- Treat every received event as an untrusted claim. Anyone holding the read key can forge or flood the lane; the data is directional, not evidential.
Deliverables: the app, the event schema, the telemetry vault's
configurecall, and a dashboard app in the telemetry vault that subscribes tosg.on('append.new-messages')and renders the funnel.
The canonical markdown copy lives in the SGit-AI__CLI repo under team/humans/dinis_cruz/claude-code-web/, which is where every brief on this page is filed.