for agents/docs/llms.txtv0.2.75 · 9 Sep 2026

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 a build brief is, and how it differs from the asks below it. The cross-team briefs are requests addressed to another team, with a status, that close when answered. This is the other kind: a durable reference an agent executes. It carries the mechanism, the traps, the things to verify first, and — at the bottom — the prompt to hand the builder, written to be pasted rather than paraphrased.

What to read first

PageWhat it gives you
Vault messagingThe worked example: two vaults exchanging encrypted messages, end to end, with curl
API — append lanesThe six endpoints, the gates, the limits and the status codes
sg.appendThe same transport from inside a vault app, plus the sg.on event push
PKIKeypair lifecycle — sgit pki keygen / export / encrypt / decrypt
Sub-vaultsThe 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

CapabilityHolderCanCannot
append_tokenthe senderwritelist, fetch, read anything
enum_keythe vault ownerlist, fetch, mark-processedwrite, purge
write_keythe vault ownerconfigure, purge
private keythe vault ownerdecryptnever 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 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.

What you do not get is authenticity. Anything the browser holds, the visitor holds.

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 (listfetchmark-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

Corrected 7 September 2026, and the correction matters more than the original. This brief said to check whether 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.
  1. Does sg.append.write work 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 no writable check, no consent gate, and no read-only branch anywhere in the append handler", and sg.app.writable "is irrelevant to append". So the grant is the whole story:
    { "permissions": { "append": { "write": true } } }
    Nothing else is required. The EREADONLY this brief cited belongs to sg.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.
  2. Does the frame's CSP allow connect-src to the API host? — NO, not by default. And this is the trap.
    The frame is served with connect-src blob: data:, so a direct fetch to 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: true is 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."
  3. Only write crosses 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, purge and configure therefore always act on the open vault; write is the sole verb that takes an explicit vault_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.
  4. 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 a sha256: CLI fingerprint in as a token; that is the most likely way to hit it.
If a grant you added seems to be ignored, the manifest is the first suspect, not the gate. Two causes, both silent: a release pin (.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 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.html and https://sgit.ai/api/append-lanes.html first.

Constraints:

Deliverables: the app, the event schema, the telemetry vault's configure call, and a dashboard app in the telemetry vault that subscribes to sg.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.