Skip to content

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.md in 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.


The pattern has been hiding in plain sight across half a dozen services:

  • Bell flush. A Reader (only view:bell cap) leaves a comment. jin bell flush writes afs/bell/<ring>.yaml and git commits. Whose name lands in git 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:

IdentityBelongs toLives inExamples
ActorThe userThe YAML body, the SIS row, the Inbox cardactor: sarah, resolved_by: ronnie, assignee: mig
ScribeThe deploymentGit author, OAuth token, R2 access keyBell 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.


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:

  1. The Scribe is a property of the deployment, not of the user. Every deployment has exactly one Scribe identity for any given credential boundary.
  2. 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).
  3. Both travel together. Git captures the Scribe; the YAML captures the Actor. Loss of either is loss of the full record.

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 deployment

The 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).

SurfaceBoundaryScribe identity (today)Where it should land
Bell flushGit commit on AFSlocal user.emailBell Service <bell@<tenant>>
Notes / bookmarks / recents flushGit commit on AFSlocal user.emailNotes Service <notes@<tenant>> (or shared Artifacts Service)
Atelier saveGit commit on AFSlocal user.emailAtelier <atelier@<tenant>> (cloud); user’s identity (local laptop solo mode)
Cloud-makeGit push, R2 uploadnot implementedCloud Make <make@<tenant>>
Action cell run-side-effectWebhook call, email sendnot implementedAction 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.


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.


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.


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.


Concretely, the failure modes that motivated this:

  1. 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.
  2. 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.
  3. Service-account collisions. Two services on the same deployment using the same git config user.email makes their commits indistinguishable in git log. Bell-comment vs note-edit look the same.
  4. 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.

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.


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

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).

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.


  • 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 in afs/jinflow.yml plus 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.

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.ymluser ~/.jinflow/config.ymlengine 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 in jinflow/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.


  1. 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’s jinflow.yml is the natural home — it travels with the AFS, keeps tenant isolation, and the file already exists for every tenant.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

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.

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 commits

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.

  1. scribes.<boundary> in <afs_root>/jinflow.yml (per-tenant — primary)
  2. scribes.<boundary> in ~/.jinflow/config.yml (per-machine)
  3. scribes.<boundary> in <engine_root>/jinflow.yml (deploy bundle)
  4. None — caller falls back to local git 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.

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.

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.

BoundaryCallerSideBehaviour
afsjin afs pushPythonPer-commit --author + GIT_COMMITTER_* env vars. Solo-laptop default: no scribes: block → no change.
makejin make build commitPythonSame. Lays groundwork for cloud-make where the committer is a service by definition.
belljin bell flushPythonCommits afs/bell/<ring>.yaml files directly under the Bell Service identity instead of waiting for jin afs push to sweep them.
notesjin sis publishPythonOne commit per publish round, covering notes + bookmarks + recents (per the Sense doc — they share a service).
atelierJinDesk route handlers (smebit, signal, thesis, verdict, perspective, dossier, observation, contributing-factor saves; signal-builder)TypeScriptgetGitIdentity(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.

  • 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.

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.


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

jazzisnow jinflow is a jazzisnow product
v0.64.7 · built 2026-09-20 19:48 UTC