Actor attribution envelope — every mutation knows who did it
The verified didi.sh identity now rides beside every capability invoke into the domain services, stamping created_by/updated_by on domains, sources, and source_usages rows in SurrealDB and created_by into the corpus frontmatter content-ingest writes — no consumers yet, just the wire proven end to end against local dev.
Why Care?
Step 3 proved the membership gate — only the right people get onto an instance's WS. Step 4 answers the next question the humain-vc flow needs before real content lands: once someone's in, whose fingerprints are on what they touch? Every domain, source, and source_usage row was anonymous until now. That's fine solo; it stops being fine the moment Michael and Aniel are both curating the same thesis corpus.
What's New?
The envelope.
ws.tsbuilds{ didi_id, via? }from the verified session (never client-asserted) and threads it intodispatch();capabilities.tsmerges it into the NATS request body besideargsfor every domain-service capability. Mirrors the existingclient_idtenant-envelope pattern rather than inventing a new shape.Resolver stamps it.
domains.tsgained anactorSetClause()helper —created_by/created_viaon every CREATE,updated_by/updated_viaon every actor-driven UPDATE (domain re-touch, source registry edits, source_usages fetch/attach/tag/re-slug). Never clobbers a stamped value with NULL when a request arrives with no actor (background Jina fetches, dev withDIDI_AUTH=off).Frontmatter carries it too.
domain.createandsource.addforwardcreated_byto content-ingest, which writes it into the domain'sindex.mdand the source's per-file frontmatter — the corpus stays self-describing on disk, not just in the DB.Chat turns get a
viatag. When the chat surface replays an acceptedchat_invoketool call,workspace.invoke(capability, args, 'didi-agent')threads aviafield through the same envelope, landing asupdated_via/created_via— didi's writes are distinguishable from a direct click without a second code path.No consumers. Per the flow plan's rule: no filtering, no UI, no gating on any of this yet. It's provenance, waiting for step 8's chat surface and any future curator UI to read it.
Proven live. Added an
ATTRIBUTION=1mode toscripts/prove-didi-auth.mjs: signs in, firesdomain.create+source.addover the authenticated WS session, and asserts the response'screated_bymatches the signed-indidi_id. Ran it against local dev (docker compose backend +id-didi-shon :4000) — confirmed both the DB round-trip and the on-disk frontmatter (index.mdand the source file) carry the didi_id. Test rows cleaned from the shared SurrealDB Cloud instance after.
What's Next
Step 5 — thesis vocabulary: swap the curator's hardcoded strategy domain
type for a per-workspace default so humain-vc reads "Thesis" while
reach-edu keeps "Strategy." Directly serves the humain-vc flow's actual
goal (domain corpus building) and has no deploy dependency.
