Skip to content

Iceberg

Angelo keeps everything you learn in two local, git-backed stores — the memory tree and the zettelkasten — plus the synapse links between them. By default all of it is private to your machine. Iceberg is the layer that lets you share a chosen slice of it with specific people, and nothing more.

The name is the mental model. Your full store is the iceberg: a large private mass sitting below the waterline. Sharing raises only a thin tip above the surface — a filtered, per-recipient-encrypted copy of exactly the units you granted. Everything else stays underwater. The waterline is a hard consent boundary: content you did not grant is never written into the shared artifact at all, so there is nothing to leak, misconfigure, or accidentally expose.

The iceberg round-trip, animated: a producer's private store is filtered to the granted units, each unit is encrypted to its recipients as it crosses the encrypt gate, the ciphertext lands in one bundle repo, and Alice, Bob, and Carol each pull the same repo — each key unlocking only its own slice

  • What it shares: memory entries, documents, skills, sessions; zettelkasten notes, sources, projects, citations, editorial overlays, and dataset sidecars; and cross-store synapse edges.
  • How it enforces: owner-authored grants + per-recipient encryption (native age via pyrage) — access is holding a key, so there are no server ACLs to trust.
  • How it travels: git. Iceberg only ever hands git opaque ciphertext; git is a dumb, always-on courier.
waterline = consent boundary shared bundle filtered · scrubbed · per-recipient encrypted your private store .memory · .zettelkasten · .synapse private by default — never shared
Iceberg shares only the tip: a filtered, scrubbed, per-recipient-encrypted copy of the units you granted. The large private mass — your raw .memory, .zettelkasten, and .synapse — stays below the waterline and is never written into the shared artifact.

The one idea: the unit is the atom, and it travels as ciphertext

Everything shareable is a unit — one memory entry, one note, one document, one skill, one session, one synapse edge. Iceberg never shares "a store"; it shares a set of units. Publishing does three things to each unit, in this order:

  1. Filter — keep only the units the owner's grants actually allow this audience to see. Ungranted and private units are physically omitted — they never enter the bundle, so they cannot leak.
  2. Scrub — rewrite each kept unit's cross-references so it can only name things the recipient can also open. A link to an omitted entry is dropped; a note that would reveal an un-shared box name has that name removed.
  3. Encrypt — encrypt each unit natively to its own recipients' public keys using age. age wraps one file key to many recipients, so a unit shared to three people is one ciphertext any of the three (and no one else) can open.

The output is a bundle: a small cleartext manifest (structure only) plus a pile of opaque per-unit ciphertexts. Because a unit is unreadable without its key, the whole bundle can live in a single git repo — even a public one. There is no server deciding who sees what; the math does.

No hand-rolled crypto

All encryption is delegated to pyrage, the maintained Rust age binding. Iceberg never invents a cipher, a key-wrap, or a content-key scheme — it uses age's native multi-recipient encryption directly.

Four pillars

Pillar Question it answers How iceberg answers it
Audience scopes Who may see a unit? private (default) · firm · bilateral (audience: [ids]), declared in sharing/grants config and inherited down the tree/project.
Owner-controlled grants Who decides? The repo owner, via an owner-authored, committed grants manifest. A consumer may subscribe, but the owner's grants decide the yield (producer-grant, not consumer-pull).
Keypair identity Who is a recipient? A firm-wide registry (.memory/identities.yaml) maps each logical id to an age public key. Grantees are encryption recipients — the registry is the enforcement anchor, not a directory of ACLs. The private key lives only in ~/.angelo/.
Transport axis How far may it travel? git-ok vs local-only. A local-only unit is never emitted to a bundle; a sealed repo (no git remote) is the enforcement primitive, and derived outputs inherit the most restrictive transport of their inputs.

Anatomy of a bundle

A bundle is deliberately boring on disk — that is the point. The manifest is cleartext but structural only: format/version, and for each unit an opaque content-addressed filename. It carries no ids, no titles, no recipient lists, no box names. Everything sensitive — including which unit this is and who can read it — lives inside the ciphertext.

bundle-repo/
  manifest.json                 # cleartext: format, version, opaque unit paths (no ids/recipients)
  project.md                    # minimal stub (id/name/status only) — the real project body is NOT shared
  units/
    3f9a…c2.age                 # one entry/note/… encrypted to ITS recipients
    a17b…e0.age
  recipient-index/              # (zettelkasten bundles only) firm-encrypted note→recipients map
    9c00…bd.age                 #   used for cross-producer synapse edges — see below
raw unit entry · note · edge 1 · Filter drop private/ungranted 2 · Scrub rewrite cross-links 3 · Encrypt to recipients' keys unit.age opaque filter → scrub → encrypt, per unit the order matters: a unit is filtered and scrubbed BEFORE it is ever encrypted, so an omitted unit is never keyed to anyone
Each unit is filtered, scrubbed, then encrypted — in that order. Filtering happens before encryption, so an ungranted unit is never turned into ciphertext for anyone; a shared unit can never name (by id or title) something its recipient cannot also open.

Why a recipient can never learn what they can't open

Two rules give iceberg its id-secrecy property — a recipient sees only the units they were granted, and those units reveal nothing about the rest:

  • Filter before encrypt. Private/ungranted units are omitted, not encrypted-then-hidden. A scrubber can even return an OMIT_UNIT sentinel to drop a unit that would leak once redacted (e.g. a comparison table whose every row points at un-shared notes).
  • Per-unit cross-link scrub. References are checked against this unit's recipient set. A memory entry shared to Alice never lists a child entry Alice can't open; a note never names a box, dataset, or citation outside Alice's grant; a session's auto-derived summary is rebuilt from only the titles of entries Alice can see.

The honest residual: body prose is shipped verbatim. If you type a secret into an entry body you do share, the recipient reads it — iceberg controls which units and references cross the boundary, not what you write inside a unit you chose to share. (See Honest limitations below.)

One bundle, many audiences

A single bundle can serve a whole team at once: it is one manifest over many independently-encrypted units. Alice, Bob, and Carol all fetch the same repo, but each key opens only its own slice. Nobody needs a per-person repo, and the producer publishes once.

bundle repo u1.age {A} u2.age {A,B} u3.age {B} u4.age {A,C} u5.age {C} Alice · key opens u1, u2, u4 Bob · key opens u2, u3 Carol · key opens u4, u5
One bundle, three audiences. Every recipient fetches the same repo; each private key opens only the units it was made a recipient of. Overlapping grants (u2 → Alice + Bob, u4 → Alice + Carol) are a single ciphertext each — age's native multi-recipient encryption, no duplication.

The round-trip: publish → sync → decrypt → render

Sharing is peer-to-peer over git. The producer publishes to a bundle repo they own (separate from the private source repo); the consumer subscribes to it as a git remote and pulls. There is no central store and no trusted decryptor — every read happens client-side.

producer private store bundle repo git (GitHub) · ciphertext only always-on rendezvous consumer federation-cache/ → union view publish (encrypt) sync (fetch+decrypt) no central store · client-side decryption only the consumer decrypts only what its key opens, then the existing federation read path renders it exactly like a local repo
Publish encrypts a filtered slice into a producer-owned bundle repo; federation sync git-fetches it and decrypts only what the consumer's key opens into .angelo/federation-cache/<peer>/; the existing federation read path renders that cache in the union view — same code as a local repo. A locked-down consumer that hosts nothing still works, because it only ever pulls.

What iceberg covers

Iceberg reaches the whole of both stores — memory has full coverage, and the zettelkasten was brought to parity with it:

Store Shareable units
Memory entries, a minimal project stub, documents, skills, sessions
Zettelkasten notes, source _meta, projects, citations; editorial overlays (reviews, organizations/tables, outline drafts); binary/dataset sidecars (equation PNGs, dataset files, and DVC pointers so a recipient pulls large data instead of receiving it inline)
Synapse cross-store memory ↔ zk edges — both same-producer and cross-producer (below)

Each family has its own scrubbers so a shared unit never leaks an id, title, box name, quote, or file path outside the recipient's grant.

Cross-store edges across producers

A synapse edge links a memory entry to a zettelkasten note, and it may only ship to someone who can open both ends — edge_recipients = entry_recipients ∩ note_recipients. That is easy when both ends are yours. It is hard when the note lives in someone else's repo: you can't see their grant graph, only the filtered content they already shared with you.

Iceberg solves this with a firm-scoped recipient-index policy export. When a zettelkasten producer publishes, it also emits a small, firm-encrypted map of note → recipients for exactly the notes it already shares. The edge author fetches that index during federation sync and uses it to compute the both-ends intersection locally — fail-closed: if the peer policy is missing, stale, or doesn't cover the note, the edge is simply not shipped.

Repo A · memory entry M recipients(M) = {Alice, Bob} (the edge author) Repo B · zk note N recipients(N) = {Bob, Carol} (federated endpoint) recipient-index.age firm-encrypted: N → {Bob, Carol} A intersects, locally {Alice,Bob} ∩ {Bob,Carol} edge ships to {Bob} the only person who opens BOTH ends fetched on sync
Cross-producer edge. Repo A can't see Repo B's grants, so B publishes a firm-encrypted recipient index for the notes it already shares. A intersects its own entry's recipients with B's index and ships the edge to the intersection only — here, just Bob. If B's index is missing or stale, A ships nothing (fail-closed).

Revoking access: a worked example

Sharing has a lifecycle — sometimes you need to take a grant back. angelo memory revoke / angelo zettelkasten revoke edits the policy, rebuilds the ciphertext without the revoked recipient, and reports honestly what it did and did not achieve. Revocation is forward-only (a law of physics, not a bug — you can't un-send a plaintext someone already decrypted), so the whole command is built around never overstating the cut-off.

Take a store shared like this — Alice and Bob both see project alpha, but only Bob sees beta:

# .zettelkasten/config.yaml
grants:
  alpha:            [alice, bob]   # project alpha
  b1/n1-alpha-core: [alice, bob]
  beta:             [bob]          # bob only
  b2/n2-beta-core:  [bob]

After angelo zettelkasten publish, Alice's key opens her note (b1/n1-alpha-core). Now revoke her from alpha:

$ angelo zettelkasten revoke --id alice --project alpha
[zk revoke] revoked 'alice' within project 'alpha' from the sharing policy
[zk revoke]   grants dropped from: alpha, b1/n1-alpha-core
[zk revoke] republished bundle to …/bundle: 4 encrypted units for bob
[zk revoke] 'alice' is now CUT OFF FROM:
[zk revoke]   - the current bundle and all future bundles (new ciphertext excludes them)
[zk revoke] they MAY STILL HOLD (cannot be clawed back):
[zk revoke]   - anything they already decrypted and saved locally
[zk revoke]   - any old clone of the bundle repo they kept (unless you --purge-history AND they re-fetch)
[zk revoke] reminders:
[zk revoke]   - review + commit the source-config change (.zettelkasten/config.yaml)
[zk revoke]   - if cross-store synapse edges exist, run 'angelo synapse publish'

Three things happened, in order: it edited the grants (dropped Alice from alpha's closure only), rebuilt every unit's ciphertext against the new recipient set (note "4 encrypted units for bob" — the audience shrank), and printed an honest three-tier report. If you now try to decrypt the rebuilt bundle with Alice's key, she opens nothing — while Bob's beta grant was never touched (scoped confinement).

what a revoke can — and cannot — cut off 1 · future bundles ✓ cut off rebuild ciphertext without their key — automatic on every revoke + republish 2 · remote's old ciphertext ✓ with --purge-history orphan-collapse the bundle history + force-push, so old blobs go unreachable 3 · already decrypted ✗ cannot claw back plaintext they saved, or an old clone they kept — forward-only revocation, by nature
Revocation has three tiers. Tiers 1–2 are what the command controls; tier 3 is a physical residual the report always names out loud. The cut-off message only ever claims the tiers it actually achieved.

Closing the old-clone gap. Republishing overwrites the current files, but git history still holds the old, readable ciphertext — an old clone could git checkout a previous commit. Add --purge-history (with --push) to collapse the bundle repo to a single orphan commit and force-push it, making the superseded blobs unreachable on the remote:

$ angelo zettelkasten revoke --id alice --project alpha --purge-history --push
[zk revoke]   ...
[zk revoke] 'alice' is now CUT OFF FROM:
[zk revoke]   - the current bundle and all future bundles (new ciphertext excludes them)
[zk revoke]   - the remote's superseded ciphertext (purged + force-pushed where a remote exists)

This is the single most destructive thing iceberg does, so before it rewrites anything it verifies — by canonical repo identity, not by path or URL spelling — that the bundle repo is genuinely separate from your source workspace, and it fails closed if it can't be sure. And the report never lies: if the purge step fails (e.g. no write access to the remote), it prints a warning and omits the "purged the remote's superseded ciphertext" line rather than claiming a cut-off it didn't achieve.

Same shape for memory, and for scoped vs. global

angelo memory revoke --id alice works identically; use --scope <node> to confine it to a subtree (the zettelkasten equivalent is --project). A scoped revoke honestly reports residual access — if the id is still granted outside the revoked scope (an ancestor node, another project, or a key shared with another project), the command says so instead of claiming a clean cut-off. Omit the scope to revoke everywhere.

Security properties & honest limitations

Iceberg is deliberately explicit about what it does and does not guarantee.

Guarantees

  • Id-secrecy — a recipient sees only granted units, and those units name nothing (id, title, box, quote, path) outside the grant.
  • Both-ends edges — a cross-store edge ships only to those who can open both endpoints; the intersection is always computed before encryption.
  • Fail-closed — missing/stale/malformed policy omits the affected edge rather than over-sharing.
  • No central trust — decryption is client-side; the bundle repo (and any optional relay hub) only ever holds ciphertext.

Accepted limitations (documented in code and memory)

  • Forward-only revocation. Revoking a grant stops future bundles from including a unit (and --purge-history can remove the superseded ciphertext from the remote), but anyone who already decrypted it keeps their plaintext copy, and a stale cached policy can lag until the consumer re-syncs. See Revoking access above for the exact three-tier behavior. Re-keying + re-encryption is a planned future item. For content where that residual is unacceptable, use the opt-in hosted federation tier (an owner-run broker that serves grant-scoped results and never hands out a decryptable copy, so revocation bites on the next read) — see Hosted federation tier below.
  • Firm-scoped membership leak. The recipient index is encrypted to the whole firm, so any firm member can learn who else an already-shared note is shared with. This is the deliberate, bounded cost of cross-producer edges.
  • Unverifiable peer assertions. age hides recipient identities, so the edge author cannot cryptographically verify a peer's recipient index — a malicious producer could overstate a note's grants. Bounded (the note id is already firm-visible via the index; a producer can always leak its own content), and documented as a trust boundary.
  • Registry consistency precondition. Cross-producer edges assume one byte-consistent firm identities.yaml across repos, so ids resolve to the same keys everywhere.
  • Body prose ships verbatim. Iceberg controls which units and references cross the boundary, not the free text inside a unit you chose to share.

Topology: decentralized by default

Each producer owns a bundle repo; each consumer subscribes to the ones it cares about and pulls on a schedule. Sharing is peer-to-peer via git hosting — nothing multi-tenant to stand up or trust. A blind-relay hub (a mirror behind one URL that also distributes the identity registry) is an optional later convenience, never required: it holds only ciphertext, so it needs no auth and does no per-viewer slicing.

Hosted federation tier

For content sensitive enough that forward-only revocation isn't enough, an opt-in tier inverts the model: instead of shipping a recipient a decryptable copy they keep forever, the recipient reads through an owner-run broker that holds the keys, serves grant-scoped rendered results (search, tree, notes, synapse — all computed over a per-caller scoped projection), and hands the client nothing decryptable. Revocation becomes immediate: drop the caller from the live ACL (or edit a grant) and the next read re-projects and fails. Write-back is owner-owned via a git branch/merge flow, so a departing contributor's merged work stays with the employer while they lose access to both the corpus and their additions. This trades iceberg's zero-trust and offline properties for revocability, and is scoped as an additional tier alongside the offline default — not a replacement. It coexists with offline sharing at the per-unit grain: a unit carries a transport: offline | hosted tier, so the same store can ship some units as bundles and serve others only through the broker.

caller holds age key no local plaintext owner-run broker age-nonce auth · bearer token per-caller scoped projection serves rendered results only holds the keys + source store source store .memory · .zettelkasten grants = live ACL authenticate (age-nonce) rendered results the client never receives a decryptable copy — revocation bites on the next read edit a grant → the projection cache key changes → the next read re-projects → revoked content is gone
The hosted tier keeps the keys and the source store on the owner's broker. A caller proves key possession via an age-nonce handshake, then reads grant-scoped rendered results over a per-caller projection — never a decryptable copy. Because the projection cache is keyed by the current grant version, a revoke takes effect on the very next read.

The design (auth, projection, signing, write-back, and the accepted trust boundaries) lives in documents/260708_iceberg-revocation-and-binding.md.

Running a broker

The broker is a FastAPI app run under uvicorn. From the owner's checkout:

angelo iceberg serve                       # binds 127.0.0.1:8779, read-only
angelo iceberg serve --port 9000
angelo iceberg serve --enable-writeback    # also mount /memory/contribute + /zk/contribute
angelo iceberg serve --rate-limit          # shed load per-IP (pre-auth) / per-caller (reads)

Everything is fail-closed by default: it binds loopback only (127.0.0.1), write-back and rate limiting are off, and X-Forwarded-For is not trusted. Flags only ever turn a feature on. Configuration can also come from ICEBERG_BROKER_* environment variables (flags win over env); the ones you are most likely to set:

Variable / flag Default What it controls
--host / ICEBERG_BROKER_HOST 127.0.0.1 Bind interface. Keep it loopback and put a TLS proxy in front (below).
--port / ICEBERG_BROKER_PORT 8779 Bind port.
--registry-path / ICEBERG_BROKER_REGISTRY_PATH repo .memory/identities.yaml The firm registry of caller public keys — the auth anchor.
--memory-source / ICEBERG_BROKER_MEMORY_SOURCE process-local .memory Source memory store to project from.
--zk-store-root / ICEBERG_BROKER_ZK_STORE_ROOT process-local .zettelkasten Source zk store to project from.
--cache-root / ICEBERG_BROKER_CACHE_ROOT .angelo/iceberg-cache Where per-caller projections are materialized.
--enable-writeback / ICEBERG_BROKER_ENABLE_WRITEBACK off Mount the contributor write-back routes.
--rate-limit / ICEBERG_BROKER_RATE_LIMIT_ENABLED off Per-request load shedding (429 before expensive work).
--trust-forwarded-for / ICEBERG_BROKER_TRUST_FORWARDED_FOR off Read the client IP from X-Forwarded-Foronly behind a trusted proxy.
ICEBERG_BROKER_TRUSTED_PROXY_HOPS 1 Number of trusted proxies; the real client is the N-th from the right of the header.

An unauthenticated liveness probe is always available:

$ curl -s http://127.0.0.1:8779/api/health
{"status":"ok","service":"iceberg-broker","writeback_enabled":false,"rate_limit_enabled":false,"pid":12345}

Exposing it beyond loopback (TLS / reverse-proxy runbook)

The broker deliberately does not terminate TLS itself — it binds loopback and expects a reverse proxy (nginx, Caddy, a cloud load balancer) to terminate HTTPS and forward to it. The recommended shape:

  1. Keep the broker on loopback. Leave --host 127.0.0.1 (or bind a private interface the proxy can reach). Never expose the raw broker port publicly.
  2. Terminate TLS at the proxy and proxy to http://127.0.0.1:8779. With Caddy that is a two-line site block; with nginx a proxy_pass to the loopback upstream inside a server { listen 443 ssl; }.
  3. Turn on rate limiting (--rate-limit) once you are reachable from more than one client, so an unauthenticated flood is shed with a 429 before the broker does any crypto or projection work.
  4. Only then turn on --trust-forwarded-for, and only if the proxy sets X-Forwarded-For. Set ICEBERG_BROKER_TRUSTED_PROXY_HOPS to the exact number of proxies you run so the broker reads the real client IP from the right of the chain. This pairing matters: without a trusted proxy, a client could forge X-Forwarded-For to dodge (or frame another IP for) the per-IP limit — so the default is to ignore the header entirely and key off the socket IP.

--trust-forwarded-for is only safe behind a proxy you run

Enable it exclusively when every request reaches the broker through a reverse proxy that overwrites/appends X-Forwarded-For. On a directly-exposed broker, leave it off (the default) — the socket IP is the only trustworthy client identity there, and trusting a client-settable header would let a single client evade the rate limiter.

The client half is iceberg.client.IcebergClient (or drive the routes directly over HTTPS). It runs the age-nonce handshake, caches the bearer token, and verifies TLS by default:

from iceberg.client import IcebergClient

with IcebergClient("https://broker.example.com") as ic:   # uses ~/.angelo/identity.yaml
    ic.health()
    hits = ic.memory_search("liquidity")
    note = ic.zk_note("b_main", "n1-alpha-core")
    # when the broker was started with --enable-writeback:
    ic.contribute_memory("e-new", {"id": "e-new", "title": "…", "body": "…"})

Enabling it

Iceberg ships with angelo; there is nothing extra to install (encryption uses pyrage, pulled in with the core package).

1. Create this machine's identity (private key stays in ~/.angelo/; the public key can be registered for others to encrypt to):

angelo memory identity init --id alice --display-name "Alice" --register

2. Declare grants in config (sharing/grants blocks in .memory/config.yaml, .zettelkasten/config.yaml, .synapse/config.yaml) and point at a producer-owned bundle repo (sharing.bundle_repo).

3. Publish a filtered, per-recipient-encrypted bundle:

angelo memory publish --push          # memory entries/docs/skills/sessions
angelo zettelkasten publish --push    # notes/sources/projects/citations/editorial/sidecars (+ recipient-index)
angelo synapse publish --push         # cross-store edges

4. Subscribe & sync on the consumer (git-fetch + decrypt into the federation cache):

angelo federation sync

The decrypted cache under .angelo/federation-cache/<peer>/ then renders in the memory/zettelkasten union views exactly like a local repo.

Run federation sync before synapse publish

Cross-producer edges reflect each peer's recipient policy as of your last sync. Sync first so revocations and new grants are current; synapse publish will warn (never fail) if a peer policy looks stale.

Commands

Natural-language phrasings that drive iceberg — say the thing, the agent runs it. See the full command cheatsheet.

Say this What runs
"set up my sharing identity", "make my key" angelo memory identity init
"publish my memory bundle", "share these entries" angelo memory publish
"publish the zettelkasten bundle", "share these notes" angelo zettelkasten publish
"publish the cross-store links", "share the synapse edges" angelo synapse publish
"sync the shared bundles", "pull the latest shared knowledge" angelo federation sync
"revoke alice", "cut off alice from these notes" angelo memory revoke / angelo zettelkasten revoke
"revoke alice and purge the old ciphertext" angelo … revoke --purge-history --push
"run the hosted broker", "serve the federation tier" angelo iceberg serve
"review contributions", "merge alice's work back" angelo iceberg contributions / review / merge

Design notes

The decisions behind this, drawn as a slice of the memory tree.

  • D Pivot to encryption-first enforcement (drop git-ACL stage)active
    Reversed the earlier 'graduated enforcement' decision.
  • D Phase 1 slice: encryption-first per-recipient memory bundle sharingactive
    Implemented the Phase 1 vertical slice for owner-granted, per-recipient-encrypted memory bundle sharing: a full publish -> sync -> decrypt -> render round-trip for single/multi recipient using age.
  • D Publish target = producer-owned bundle repo; no central store (hub optional)active
    Decided the transport concretely: the publish target is a PRODUCER-OWNED bundle repo, separate from the private source repo.
  • D Phase 2: bundle sharing extended to documents/skills/sessionsactive
    Extended the encryption-first per-recipient memory bundle from entries+project to also cover documents, skills, and sessions, rendered via federation.read_repo.
  • D Zettelkasten encryption-first bundle sharing (v1)active
    Brought encryption-first bundle sharing to the committed .zettelkasten/ store via a new zettelkasten/sharing/ package mirroring memory/sharing/, reusing memory.sharing crypto+identity (one firm-wide age registry;…
  • D ZK sharing v2 Phase C shipped: cross-store synapse-edge bundle (same-producer slice)active
  • Added a THIRD encryption-first per-recipient bundle in a new package zettelkasten/synapse/sharing/ for cross-store synapse edges (.synapse/links/links.json — bipartite memory<->zk edges).
  • D Cross-producer synapse edges via firm-scoped recipient-index policy exportactive
    Enable synapse edges whose ZK endpoint is FEDERATED (zk_source contains ':') — previously omitted because edge author repo A cannot compute note_recipients for peer B's note.
  • N Accepted limitations of encryption-first bundle sharing
    Two limitations were explicitly accepted (not bugs):
  • N Accepted trust boundary: recipient-index is an unverifiable peer assertion
    security-review Medium, no cheap fix.
  • R Angelo
    • P Experiment Infrastructure: Memory Hardening + DVC Artifactsactive
      • P Phase 3: Cross-Repo Federation + Unified Visualizationactive
        • P Firm-wide memory + zk sharing: owner-controlled, consent-first, angelo-native layer on gitactive
          • D Pivot to encryption-first enforcement (drop git-ACL stage)active
          • D Publish target = producer-owned bundle repo; no central store (hub optional)active
          • D Phase 1 slice: encryption-first per-recipient memory bundle sharingactive
            • E 3-cycle checker hardening of the sharing bundle (high rigor)active
            • N Accepted limitations of encryption-first bundle sharing
            • A Use pyrage (Rust age binding) for bundle encryption; no hand-rolled crypto
            • C Phase 1 sharing slice complete: publish->sync->decrypt->render round-trip green
          • P Phase 2: extend memory bundle sharing to documents/skills/sessionsactive
            • D Phase 2: bundle sharing extended to documents/skills/sessionsactive
              • E High-rigor checker cycle: session free-text leak caught + fixedactive
              • N Accepted limitations + follow-ups for docs/skills/sessions sharing
              • C Phase 2 sharing extension complete: docs/skills/sessions round-trip green
              • N Resolved Phase 2 nits: docstring wording + case-insensitive session blanking
          • P Phase 3: Zettelkasten sharing v1 (note/source/project/citation)active
            • D Zettelkasten encryption-first bundle sharing (v1)active
              • E Checker-driven hardening: 3 fix cycles closing id/box-name leaksactive
              • A Invariants for zettelkasten/sharing (preserve when editing)
              • C ZK sharing v1: reviewer/security/tester PASS, critic P2-only; not yet committed
            • P ZK sharing v2 Phase C: synapse edge sharing (same-producer slice)active
              • D ZK sharing v2 Phase C shipped: cross-store synapse-edge bundle (same-producer slice)active
                • N Phase C accepted P2s (no fix cycle) + follow-ups
                • C ZK sharing COMPLETE — full-store parity with memory (A+B+C done)
              • P Phase D: cross-producer synapse edges via firm-scoped recipient-index policy exportactive
                • D Cross-producer synapse edges via firm-scoped recipient-index policy exportactive
                  • N Accepted trust boundary: recipient-index is an unverifiable peer assertion
                  • N Open user decision: forward-only revocation vs fail-closed staleness TTL
                  • C Cross-producer synapse sharing shipped (PASS)
                  • N P2 hardening: guard stale-policy diagnostic + truncated-config retain
                  • N Iceberg user-facing docs page published + design docs updated

Semantically related entries from the memory graph.