Sense 38: The Scribe — Who Carries the Author's Action Across a Credential Boundary
Sense 38 · Folded in · Last touched 2026-08-20
Folded into
Sense sense-61-the-reservoir.
- last_verified: 2026-07-27
Synced from
docs/design/sense_38_the_scribe.mdin the engine repo — that’s the source; this page is a build-time mirror.
The author writes the words. The Scribe carries them past the walls. They sign with different ink, and that’s the point.
A user dismisses a Bell ring. A user comments on a notebook cell. A user saves a smebit. In every case, the user’s intent is theirs; the physical act of pushing bytes to git, R2, an HTTP endpoint is performed by something else — the deployment, holding a credential the user can’t reach themselves.
This Sense names the second half of that pair. Sense 25 (Identity & Passes) tells us who the user is and what they’re allowed to do. Sense 38 tells us who carries their action past a boundary the user themselves can’t cross.
The two compose. Authoriser and Carrier. User and Scribe.
What pulled us in
Section titled “What pulled us in”The pattern has been hiding in plain sight across half a dozen services:
- Bell flush. A Reader (only
view:bellcap) leaves a comment.jin bell flushwritesafs/bell/<ring>.yamlandgit commits. Whose name lands ingit log? The Reader has no GitHub account. - Notes flush. Same shape. SIS notes → AFS YAML → git push.
- Bookmarks / recents flush. Same shape again.
- Atelier saves. Today on a local laptop, the user is the committer. In the future cloud-Atelier, they won’t be.
- Cloud-make. A cloud worker builds a KLS, pushes provenance back to git, uploads the result to R2. The “who” is a service account by definition.
- Future: action-cell side effects (send email, post to Slack) — same composition.
In every case there are two distinct identities at play:
| Identity | Belongs to | Lives in | Examples |
|---|---|---|---|
| Actor | The user | The YAML body, the SIS row, the Inbox card | actor: sarah, resolved_by: ronnie, assignee: mig |
| Scribe | The deployment | Git author, OAuth token, R2 access key | Bell Service <bell@jinflow>, Cloud Make <ci@jinflow> |
Without naming the second axis, every implementation reinvents the question “who commits this?” — and answers it differently. The Atelier writer today uses the local git config (per-user, by accident); the Bell publisher I just shipped also uses local git config (which is wrong for cloud-Atelier, viewer-comments via tunnel, and CFO-as-Reader scenarios). Without Sense 38, both implementations look correct in isolation and create category confusion together.
The Cognitive Move
Section titled “The Cognitive Move”Stop conflating who authored with who committed. They are allowed to differ. They typically should differ when the user isn’t the deployment owner.
Once that move is made, the operational rules drop out cleanly:
- The Scribe is a property of the deployment, not of the user. Every deployment has exactly one Scribe identity for any given credential boundary.
- The Actor is a property of the user, captured at the moment of intent and preserved in whatever artifact records the action (YAML body, SIS row, audit log).
- Both travel together. Git captures the Scribe; the YAML captures the Actor. Loss of either is loss of the full record.
Anatomy of a Crossing
Section titled “Anatomy of a Crossing”A “credential boundary” is any operation where the user authorises something but doesn’t hold the keys to perform it themselves:
boundary │ USER ── intent ────┼────► CREDENTIAL HELD BY ──────► EXTERNAL SYSTEM (Actor) │ DEPLOYMENT (Scribe) (git / R2 / API) │ ┌──────────────┼──────────────┐ │ │ │ recorded in Scribe acts bytes leave the YAML body deploymentThe Scribe’s job is single-act: take an authorised intent, execute it against the credentialed system, leave breadcrumbs for both identities in the artifact that results.
A Scribe is not:
- The user’s proxy in any cryptographic sense (no signing of behalf-of-user statements; that would be a separate primitive).
- A bypass of capability checks (the Authoriser still gates whether the action can happen; the Scribe only handles how it crosses the boundary).
- A general-purpose worker (it’s bound to one credential boundary; a deployment may have several Scribes).
Common Scribes in jinflow today
Section titled “Common Scribes in jinflow today”| Surface | Boundary | Scribe identity (today) | Where it should land |
|---|---|---|---|
| Bell flush | Git commit on AFS | local user.email | Bell Service <bell@<tenant>> |
| Notes / bookmarks / recents flush | Git commit on AFS | local user.email | Notes Service <notes@<tenant>> (or shared Artifacts Service) |
| Atelier save | Git commit on AFS | local user.email | Atelier <atelier@<tenant>> (cloud); user’s identity (local laptop solo mode) |
| Cloud-make | Git push, R2 upload | not implemented | Cloud Make <make@<tenant>> |
| Action cell run-side-effect | Webhook call, email send | not implemented | Action Runner <actions@<tenant>> |
The pattern is one Scribe per service, scoped to the tenant. The domain naming reflects what the credential controls, not which human pressed the button.
The composition with Sense 25
Section titled “The composition with Sense 25”Sense 25 answers: can this user do this action? Sense 38 answers: who commits the action once authorised?
They are orthogonal and sequential:
Sense 25 (Authoriser) Sense 38 (Scribe) ───────────────────── ─────────────────1. Does user U hold the 1. Does the deployment have the capability act:bell? credential to push to git?2. If no → 403. 2. If no → fail loud, surface to ops: "the Scribe is mute."3. If yes → record the 3. If yes → execute the act, intent (SIS row, record the actor in the YAML body), then artifact, sign as Scribe. hand to the Scribe.A passing capability check doesn’t imply the deployment has the credentials. A failing capability check doesn’t try to engage the Scribe at all. The two layers fail independently with different error shapes.
Single-user laptop as a special case
Section titled “Single-user laptop as a special case”A solo analyst on their own laptop is both Authoriser and Scribe. Their Clerk identity (or fallback) is the Actor; their local git config is the Scribe. They’re the same person, and that’s fine.
The principle still holds — it just collapses. The deployment owns the credential; in this case the deployment owns the person too.
This matters because Sense 38 should not introduce ceremony for
solo developers. The default Scribe configuration on a local
machine is “use whatever the current git config user.email
says.” We’re not asking solo users to declare a fictional bell
service account when they’re the only one resolving rings.
What changes the moment a second user enters the picture — a viewer in a proxy session, a CFO on the Cloud JinDesk, a webhook in cloud-make — is that the Scribe identity diverges from the Actor identity. At that point it becomes explicit.
Where the Scribe is configured
Section titled “Where the Scribe is configured”The deployment declares its Scribes. Two cleanest places:
afs/jinflow.yml— per-tenant Scribes for tenant-scoped services (Bell, Notes, Atelier writes targeting tenant AFS).- Engine
instances.yml/ env — deployment-scoped Scribes for cross-tenant services (cloud-make, R2 uploads, webhook senders).
A skeleton:
scribes: bell: git_name: "Bell Service" git_email: "bell@numetrix.rmc.local" notes: git_name: "Notes Service" git_email: "notes@numetrix.rmc.local" atelier: git_name: "Atelier" git_email: "atelier@numetrix.rmc.local"The engine reads this when a Scribe is invoked. Missing entries
fall back to local git config (which is the solo-laptop default).
Cloud deployments set them explicitly.
What Falls Apart Without Sense 38
Section titled “What Falls Apart Without Sense 38”Concretely, the failure modes that motivated this:
- Per-user credential proliferation. Every Reader of a tenant’s data needs a GitHub account with write access to the AFS repo, because every comment they leave is a git commit. Wrong category; wrong scale.
- Tenant-blindness violations. A multi-tenant deployment that uses the deployment owner’s identity for every commit makes tenant-A’s data attributable to tenant-B’s owner. Audit-toxic.
- Service-account collisions. Two services on the same
deployment using the same
git config user.emailmakes their commits indistinguishable ingit log. Bell-comment vs note-edit look the same. - Cloud-make can’t start. A worker building a KLS in CI has no individual user to be — it MUST commit as a service. Without Sense 38 there’s no place to declare what that service is.
Why “The Scribe”
Section titled “Why “The Scribe””A scribe was, classically, the person who wrote down what someone else dictated. Two distinct identities — the author of the words and the hand that put them on the page — clearly separated, both preserved in the record. Monastic scribes signed the colophon with their own name and attributed the work to its author.
That’s exactly the composition Sense 38 names. The user authors; the Scribe inscribes; the resulting artifact carries both.
We considered “The Courier” (too transport-flavoured — emphasises delivery, not the writing) and “The Seal” (too symbolic — emphasises finality, not the act of recording). The Scribe is closer to the mechanic: someone in the loop who turns intent into committed record.
Operational specifics
Section titled “Operational specifics”The git author / committer split
Section titled “The git author / committer split”Git itself already supports two identities per commit: author
(who wrote the change) and committer (who applied it). We
deliberately do not use this split for Sense 38 — both are set
to the Scribe.
Reason: GitHub displays the committer prominently and the author secondarily, with confusing semantics in PRs. Putting the actor in the YAML body — a place we fully control — avoids that mess. The git log shows “the Bell Service flushed at
Multi-user batching
Section titled “Multi-user batching”A flush can bundle many users’ actions into one commit (CFO
dismissed three rings, analyst left two comments). One commit,
one Scribe author, multiple actors recorded individually in the
YAMLs. The commit message can summarise: Bell: flush 5 resolutions (3 dismissed, 2 commented).
Credential storage
Section titled “Credential storage”Out of scope for this Sense. SSH keys, PATs, GitHub App installations — wherever the deployment normally keeps them. Sense 38 is about identity not secret management; it assumes the credential is reachable from the Scribe at execution time.
Relationship to other Senses
Section titled “Relationship to other Senses”- Sense 25 (Identity & Passes). Composes directly. Sense 25 resolves the Authoriser; Sense 38 resolves the Scribe; both are consulted at every action.
- Sense 16 (P2P2P). The proxy-mode case where the Scribe is owner-side and the Actor is viewer-side. Sense 38 makes the asymmetry explicit that Sense 16 implies.
- Sense 28 (The Direct Line). Same flavor as P2P2P but with cloud-signed SQL — the cloud is the Scribe for SQL writes, the laptop holds the data. Sense 38’s frame fits.
- Sense 31 (The Inlet) / Sense 32 (Chores). Channel runs and chore commits — both have a service identity question identical to what Sense 38 names.
- Sense 29 (The Atelier). The Atelier writer is one of the
most direct Scribe consumers: every smebit / signal / thesis
save is a credential boundary crossing. Today it uses local
git config(correct for solo-laptop bootstrap, structurally insufficient once cloud-Atelier ships). The Scribe configuration inafs/jinflow.ymlplus a fall-back to local git config is the exact pattern the Atelier writer should adopt. - Sense 33 (The Bell). The forcing function for this Sense. The comment-author question was the moment Sense 38 became unavoidable.
- Sense 18 (The Ledger). The audit substrate. Sense 38’s Actor/Scribe split is what makes the Ledger interpretable — without it, “who did this” has no clean answer.
How this manifests in jinflow
Section titled “How this manifests in jinflow”The Scribe is the attribution layer at every AFS write boundary. Each boundary (make, Atelier, evolve, publish, Bell, action-runner, cloud-make) routes its commits through resolve_scribe(), which walks a three-layer fallback to determine the right authorship metadata: tenant AFS jinflow.yml → user ~/.jinflow/config.yml → engine jinflow.yml. Each Sense 21 Beat 3 publish, each Sense 29 Atelier save, each Sense 14 make Phase 1d commit carries Scribe attribution.
- Boundaries:
afs·make·bell·notes·atelier·cloud-make·action-runner(the BOUNDARIES register injinflow/cli/scribe.py) - Resolver:
resolve_scribe(boundary, *, engine_root, afs_root=None, user_config_path=None)— three-layer fallback per boundary - Configured via:
jin us --scribe <boundary>=<spec>(settable per layer)
Status: shipped (June 2026). Every AFS write today routes through the Scribe.
Open Questions
Section titled “Open Questions”Is the Scribe per-tenant or per-deployment?Resolved (2026-06-08): per-tenant is the primary layer, with per-machine and per-deployment defaults as fallbacks. The resolution ladder in §Phase 1 reflects this. The tenant AFS’sjinflow.ymlis the natural home — it travels with the AFS, keeps tenant isolation, and the file already exists for every tenant.- Display of Scribe identity in JinDesk. Does the user need to see “Bell Service committed this”? Probably not on the ring card, but maybe somewhere in audit views.
- Migration story. Existing AFS git history was authored by real humans (locally). Future Scribe-authored commits will appear alongside. No retroactive rewrite — Sense 38 applies going forward only.
- Failure modes when the Scribe is mute. The deployment has no credentials, or its credentials have expired, or it can’t reach the git remote. The action is authorised but uncommittable. What does JinDesk surface to the user? This is the “saves disabled on this branch” mode the sister and I started sketching in the Phase D letter.
- Webhooks and email. When Sense 38 extends past git, the
Scribe identity is a
From:header or a webhook-signing key. The same pattern should hold; concrete spec deferred until action-cell side effects are real.
Phase 1 — what landed (2026-06-05)
Section titled “Phase 1 — what landed (2026-06-05)”The first concrete usage picks the schema. The Phase 1 deliverables are scoped to naming the primitive and proving it through one real boundary. Per-tenant overrides, per-service granularity, and the multi-user batching refactor are deliberately deferred.
Schema
Section titled “Schema”A scribes: block in YAML. Each entry maps a boundary name to a
git-format "Name <email>" string. Every boundary the tenant wants to
sign with a specific identity must be declared explicitly.
# Goes in any of three layers (most specific wins):# <live_root>/<pack>/<tenant>/afs/jinflow.yml (per-tenant)# ~/.jinflow/config.yml (per-machine)# <engine_root>/jinflow.yml (deploy bundle)scribes: afs: "AFS Service <afs@example.com>" # `jin afs push` commits make: "Make Service <make@example.com>" # `jin make` build commitsNo within-layer default
Section titled “No within-layer default”There is no catch-all key. Boundaries are a closed set (afs, make,
bell, notes, atelier, cloud-make, action-runner) declared
ahead of time as a design event — they don’t appear at runtime.
Silently substituting a default identity for an unconfigured boundary
turns the audit trail into a quiet lie: every commit looks intentional
even when no decision was made.
An unconfigured boundary falls through to the next layer, never to
a sibling key. If every layer is silent, the caller gets None and
git config user.email takes over — visibly wrong in a way that
screams “configure this,” rather than silently mis-attributed.
This is the same principle that argues against default_pack in the
engine yml: a default that picks a winner without naming the choice is
worse than no default at all.
Resolution order
Section titled “Resolution order”scribes.<boundary>in<afs_root>/jinflow.yml(per-tenant — primary)scribes.<boundary>in~/.jinflow/config.yml(per-machine)scribes.<boundary>in<engine_root>/jinflow.yml(deploy bundle)None— caller falls back to localgit config(solo-laptop mode)
The tenant AFS layer is the primary home. Scribes are tenant-scoped
by their nature — Bell Service <bell@numetrix.rmc> is not the
same identity as Bell Service <bell@hrcentral.vai>, even though
they’re the same service. The tenant’s jinflow.yml already exists,
travels with the AFS over git, and is the only layer that keeps
tenant isolation intact on a multi-tenant deployment.
The user-config layer covers per-machine settings. PyInstaller bundles
freeze <engine_root>/jinflow.yml inside the binary, so a deployed
jin cannot override scribes via that layer — but it remains useful
for engine-built-in service identities that ship with the binary
itself.
Helper
Section titled “Helper”jinflow.cli.scribe.resolve_scribe(boundary, *, engine_root) returns
a ScribeIdentity(name, email) or None. Malformed YAML, malformed
entries, missing block — all treated as “no Scribe configured” and
return None. The function never raises: Scribe config is an optional
convenience, not a contract.
Canonical boundary register
Section titled “Canonical boundary register”The machine-readable list of every boundary jinflow recognises —
wired and unwired — lives at jinflow.cli.scribe.BOUNDARIES (a
single dict with a one-line description per key). The table below
mirrors it for prose. When you add a boundary, update both —
the constant is the source of truth for callers, the table is the
source of truth for readers.
Wired boundaries
Section titled “Wired boundaries”| Boundary | Caller | Side | Behaviour |
|---|---|---|---|
afs | jin afs push | Python | Per-commit --author + GIT_COMMITTER_* env vars. Solo-laptop default: no scribes: block → no change. |
make | jin make build commit | Python | Same. Lays groundwork for cloud-make where the committer is a service by definition. |
bell | jin bell flush | Python | Commits afs/bell/<ring>.yaml files directly under the Bell Service identity instead of waiting for jin afs push to sweep them. |
notes | jin sis publish | Python | One commit per publish round, covering notes + bookmarks + recents (per the Sense doc — they share a service). |
atelier | JinDesk route handlers (smebit, signal, thesis, verdict, perspective, dossier, observation, contributing-factor saves; signal-builder) | TypeScript | getGitIdentity(tenant, "atelier") consults the same ladder via $lib/server/scribe.ts. Each save commits under the Atelier identity (when configured) instead of the laptop owner. |
Both git author and committer are set to the same Scribe — keeps
the GitHub UI honest (it shows committer prominently with confusing
PR semantics; aligning the two avoids the trap). The Actor of the
intent stays in the YAML body, where Sense 25’s identity layer can
read it.
Deferred to Phase 3+
Section titled “Deferred to Phase 3+”- Multi-user batching commit messages. Bell flush commits one round at a time today; per-commit messages naming individual Actors are pure ergonomics — no schema decisions.
- Cloud-make Scribe. Gated on cloud-make existing.
- Webhook / email Scribes. Concrete spec deferred until action-cell side effects are real.
How a deployment turns it on
Section titled “How a deployment turns it on”The most natural home for tenant-scoped identity is the tenant’s own
AFS manifest. Add a scribes: block to
<live_root>/<pack>/<tenant>/afs/jinflow.yml, run jin make, and
the next build commit carries that identity. Per-machine or engine-
bundled defaults work too (per the resolution ladder above), but the
tenant layer is the one that keeps multi-tenant attribution honest.
Removing the block returns to the previous layer (machine → engine → git config), so opt-out is symmetric with opt-in.
Quotable
Section titled “Quotable”The author writes the words. The Scribe carries them past the walls. Sense 25 asks if the author may. Sense 38 asks who will.
Numerical neighbors: ← Sense 37: The Snapshot — A Witness Made Citable · Sense 39: The Governance →