for agents/docs/llms.txtv0.6.75 · 6 Oct 2026

Home / Briefs / Draining the subscribe lane

Surface: a page on a *.sgit.ai site writing into a vault's append lane. The other surfaces →

For agent@riskmandate.ai: draining the subscribe lane

The “subscribe” form on the articles page and at the foot of every article does not send a form to a server and does not use a mailing-list service. It encrypts the reader's address in their browser to your public key and drops it into a write-only lane on a vault. This page is everything you need to read that lane and manage the list. Everything on it is public. The one thing that is not on it is the vault key.

Why this is public. A lane's append token can only write: it cannot list, fetch or read anything, and the server answers a write with {"ok": true} and nothing else. The message is encrypted to a key only you hold. So the vault id, the endpoint, the lane token and the public key can all sit on a public page, the same reasoning as telemetry from a published vault and the agent contact file. The only secret is the vault key, from which both the write key and the enum key are derived. It is handed to you privately, never committed here.

The public facts

WhatValue
Contact file (the form reads this)/.well-known/sgit-subscribe.json
Vault idy9j3nc60
Endpointhttps://dev.send.sgraph.ai, the only host with the append routes
Lanesubscribe, append token 0158853058a43736c30f54d48d10f54bfe45330eae30817c79fdb3d67123bb75 (public on purpose)
Encrypted toyour published key, sha256:9314437063df3bb6 (RSA-OAEP 4096), the one in riskmandate.ai's contact file. The form recomputes the fingerprint from the PEM before it encrypts
Signed?No. A person filling in a form holds no key. Treat every message as untrusted input from the open internet
The secretThe subscribe vault's key. Not here, not in the repository, not in any log

What arrives

One single-part .eml per request, UTF-8, encrypted with sgit's hybrid envelope (the output of sgit pki encrypt).

From: web form <site@sgit.ai>
To: agent <agent@riskmandate.ai>
Subject: Subscribe: sgit.ai articles
Date: Sun, 04 Oct 2026 14:51:37 GMT
Message-ID: <sgit-subscribe-1791125498...-4b1c9e07@sgit.ai>
X-EmailFS-Kind: notification
X-SGit-Form: subscribe
X-SGit-Reply-To: reader@example.com
X-SGit-Page: /articles/index.html
Content-Type: text/plain; charset=utf-8

Subscribe to new sgit.ai articles
Email: reader@example.com
Name: (optional)

Consent: yes, keep this address in the subscribe vault and send new articles by email
Sent from: https://sgit.ai/articles/index.html

The address to reply to is X-SGit-Reply-To, not From. The consent line is only present because the form refuses to send without the box ticked. A honeypot field filled in by a bot is dropped in the browser and never reaches the lane.

Draining it

With the vault key and your key's passphrase in the environment, the script below lists the lane, fetches the pending files, keeps each ciphertext before it does anything else, decrypts with sgit pki decrypt, writes the .eml and a line in log.jsonl, and only then marks the files processed. A file that will not decrypt goes to quarantine/ and is never retried silently. It was run end to end on 4 October 2026 against this lane, from a real browser submission, and a second run found nothing to do.

pip install sgit-ai
export SUBSCRIBE_VAULT_KEY=<the vault key>     # the one secret
export SG_SEND_PASSPHRASE=<your pki key's passphrase>
python3 drain_subscribe.py sha256:9314437063df3bb6 ./subscribe-inbox
drain_subscribe.py
#!/usr/bin/env python3
"""Drain the sgit.ai subscribe lane. Needs: pip install sgit-ai; the subscribe vault key in
SUBSCRIBE_VAULT_KEY; the passphrase of the recipient's pki key in SG_SEND_PASSPHRASE.
Usage: drain_subscribe.py <encryption-key-fingerprint> <out-dir>
Writes <out-dir>/accepted/<id>.eml (+ .enc, kept for re-verification) and appends one line per
file to <out-dir>/log.jsonl; marks files processed only after they are safely written."""
import base64, email, hashlib, hmac, json, os, subprocess, sys, urllib.request, urllib.error
from email import policy
from sgit_ai.crypto.Vault__Crypto import Vault__Crypto

EP, VAULT = 'https://dev.send.sgraph.ai', 'y9j3nc60'
fingerprint, out = sys.argv[1], sys.argv[2]
keys = Vault__Crypto().derive_keys_from_vault_key(os.environ['SUBSCRIBE_VAULT_KEY'])   # strips the sgit_private_vault_ prefix
enum_key = hmac.new(bytes.fromhex(keys['write_key']), b'agent-contact/enum-key/v1', hashlib.sha256).hexdigest()

def post(route, body):
    req = urllib.request.Request(f'{EP}/api/vault/append/{route}/{VAULT}', data=json.dumps(body).encode(), method='POST',
                                 headers={'Content-Type': 'application/json', 'x-sgraph-vault-enum-key': enum_key})
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.load(r)

for d in ('accepted', 'quarantine'): os.makedirs(f'{out}/{d}', exist_ok=True)
listing = post('list', {'include_content': False})
by_lane = {}
for e in listing['entries']: by_lane.setdefault(e['inbox'], []).append(e['file_id'])
for lane, ids in by_lane.items():
    for i in range(0, len(ids), 100):                                   # 100 file ids per batch
        batch = ids[i:i+100]
        files = post('fetch', {'inbox': lane, 'file_ids': batch})['files']
        done = []
        for f in files:
            fid = f['file_id']                                           # server ids look like 0000000000000_<24hex>.enc
            base = fid[:-4] if fid.endswith('.enc') else fid
            enc_path, eml_path = f'{out}/accepted/{base}.enc', f'{out}/accepted/{base}.eml'
            raw = base64.b64decode(f['content'])                         # payload was base64 of the .enc text
            open(enc_path, 'wb').write(raw)                              # keep the ciphertext before anything else
            try:
                r = subprocess.run(['sgit', 'pki', 'decrypt', enc_path, '--fingerprint', fingerprint],
                                   capture_output=True, text=True, check=True)
                plain = enc_path[:-4]                                    # sgit writes the plaintext beside the .enc, minus the suffix
                if not os.path.exists(plain): raise RuntimeError('decrypt wrote no file: ' + r.stdout[-200:])
                os.replace(plain, eml_path)
                m = email.message_from_bytes(open(eml_path, 'rb').read(), policy=policy.default)
                rec = {'file_id': fid, 'result': 'ok', 'form': m['X-SGit-Form'], 'reply_to': m['X-SGit-Reply-To'],
                       'subject': m['Subject'], 'page': m['X-SGit-Page'], 'date': m['Date']}
            except Exception as ex:
                os.replace(enc_path, f'{out}/quarantine/{base}.enc')
                rec = {'file_id': fid, 'result': 'quarantined', 'error': str(ex)[:200]}
            open(f'{out}/log.jsonl', 'a').write(json.dumps(rec) + '\n')
            done.append(fid)
        if done: post('mark-processed', {'inbox': lane, 'file_ids': done})
print('drained', sum(len(v) for v in by_lane.values()), 'file(s)')

The derivation, so you can use your own tooling. Strip the sgit_private_vault_ prefix and derive the keys with sgit's Vault__Crypto().derive_keys_from_vault_key(vault_key). The write key comes out of that. The enum key is hex(HMAC-SHA256(bytes.fromhex(write_key), b"agent-contact/enum-key/v1")), the convention in the contact-file spec, never stored. The lane registered sha256(append token) as its anchor and sha256(enum key) as enum_key_hash.

Four things that cost time

What to do with a message

  1. Validate X-SGit-Reply-To as a single plausible address. Header injection through a form field is the obvious attack on a list, so refuse any value with a newline or a second address. (The page strips newlines before it builds the headers. Do not rely on that, the lane is open to anyone with the token.)
  2. Confirm before the first send. The lane is unauthenticated, so anyone can type someone else's address. We recommend a one-line confirmation email from agent@riskmandate.ai that the person has to answer before they are added. That is a decision for you and the owner, not something the page enforces.
  3. De-duplicate on the lower-cased address, and record the consent line, the date and the page it came from.
  4. Unsubscribe is a reply to any message that says so. The form's wording promises exactly that, so the list must honour it.
  5. Never put the addresses on a public page, in a commit, or in a message to anyone outside the list's operators. Personal data belongs in the private vault, not here.

Knowing what is new

The site publishes its articles as data: /updates/updates.json carries an articles array (slug, title, date, version, tags, summary), and /articles/graphs.json carries each article's teaser, topics and links. Compare against the last slug you sent. The page for an article is https://sgit.ai/articles/<slug>.html and its markdown twin is the same path ending .md.

Abuse, and how to take it down

The prompt to paste

You are agent@riskmandate.ai and you manage the subscribe list for new sgit.ai articles.
Read https://sgit.ai/docs/briefs/subscribe-lane-agent-brief.html. The vault key is in
SUBSCRIBE_VAULT_KEY and your key passphrase in SG_SEND_PASSPHRASE; never print either.
1. Drain the subscribe lane with drain_subscribe.py, ciphertext kept, quarantine on failure.
2. For each new address: validate it, send the confirmation email, add it only when confirmed.
3. For each article in https://sgit.ai/updates/updates.json newer than the last one sent,
   email the confirmed list a short note with the title, the summary and the link.
4. Honour any unsubscribe reply on the same day.
5. Report counts only (drained, quarantined, added, removed, sent). Never list addresses.

Written 4 October 2026 with the form, the contact file and the lane, tested together. The reader-facing side is assets/subscribe.js, about 120 lines you can read in a sitting. ← All briefs