D904 — The writer is the author of a commit, the server its committer, the token a trailer¶
Decision. The author of every commit a write makes is whoever wrote it. The client names the writer in the
Cartographer-Author: Name <email> HTTP header of the request; without a valid one the author is
<token id> <cartographer@<host>>. The committer of every commit is the server, Cartographer
<cartographer@<host>> (<host> the machine name reduced to what an email domain accepts). Every write ends its
trailer paragraph with Cartographer-Token: <token id>, appended last by the server, which drops any such line
the caller put in the message. A commit that is not a write (the external changes commit, a sync merge,
conflict resolution, the first commit serve --init makes) is authored by the server too; a sync that rebases
local writes keeps their authors and makes the server their committer; kb create (a new KB's first commit)
and import --commit author as the operator running them.
A valid header is one Name <email> of at most 256 bytes, valid UTF-8, with no control character, a non-empty
name and a plain local@domain email. A value outside plain ASCII travels in MCP's encoded form for header values
(SEP-2243, =?base64?…?=), which the server decodes first. Cartographer's own senders encode every value that
holds anything but letters, digits, inner spaces and . - _ @ < > + ' ( ) ,. An invalid header is treated as
absent and the write's response gains one warning line that says why and whom the write was authored as.
The writer's identity is a client setting: identities in .cartographer.yaml, by the client's KB name or
default. A present entry always decides, and an empty one sends no author. Without one:
- the stdio bridge asks git in the session's folder, inside a work tree only, so an includeIf gitdir: identity
applies to KB writes as it does to commits;
- cartographer kb repair does the same from its own folder;
- an HTTP entry that connect and sync write once into a file every folder shares carries a static header with
the global git identity. It is written only when every KB the entry reaches agrees on one, and the global
config is not read when it holds an includeIf. Either case sends none and warns.
The header goes on tools/call only, and it decides nothing about access. The server's settings for an author or
committer are removed: global git.author_*/committer_*, kbs[].author_*/committer_*, a token's
author_name/author_email, and author_*/committer_* in a KB's settings.yaml. Neither the machine's git
config nor GIT_AUTHOR_*/GIT_COMMITTER_* in the server's environment changes a commit any more.
Supersedes D341 (a token naming the author). Amends D46, whose per-KB author and committer are gone while its SSH and credential half stays, and D265, whose committer fallback no KB commit reaches any more, since the server always sets one.
Why. History should say who wrote a change and which instance recorded it. A per-server or per-KB identity (D46) made every commit look alike, which on Kubernetes, where the server is a pod, says nothing. A token's author pair (D341) named a token, not a person, and needed a server-side entry per writer. The person is known on the client, where git already knows them, and a header reaches the server on every transport that has one without changing a tool's schema. It cannot be set by the model, because the client configuration sets it, not the tool arguments.
The cost: the author is a claim, not a proof. Anyone holding a token can write under any name. The trailer is the
part the server vouches for, so concept_history reads the token from it, never from the author line. A forge
with an author push rule refuses the fallback author, which is not an account on it. The committer needs no
configuration and no account.
Alternatives rejected.
- Keeping the token's author pair (D341): it names a token, needs the server's config to change for each person,
and cannot follow a per-folder identity.
- An author argument on the write tools, or a _meta field: every write schema changes, and the model, which
writes the arguments, could pick the author.
- The writer as committer as well: the committer records which instance made the commit, and needs no account.
- Signing commits per writer: it needs a key per writer and per client. It is out of scope; the trailer gives
what the server can vouch for.
- Refusing a write whose header is invalid: an author is attribution, not authorization, so a bad display string
would break an agent's write for nothing. The warning makes it visible.
- Raw UTF-8 or RFC 8187 header values: header bytes beyond ASCII are not portable, while MCP already has an
encoded form (Mcp-Name).
- Encoding only non-ASCII values: client configs expand ${VAR}, $VAR, $(cmd) and {env:VAR} in a header
value, so a plain $ in a name would be read as a template. Hence the allowlist.
- Writing an HTTP entry's author from the folder connect runs in: the entry is shared by every folder, so one
folder's identity would author the others' writes.
- Reading the global git identity even when it has an includeIf (the issue's plain rule): a folder whose
includeIf names a work identity would silently write as the personal one. Sending none, with a warning that
names identities.default, fails closed.
Consequences. Breaking: commit authors change from the configured server, KB or token identity to the writer,
and the committer becomes Cartographer <cartographer@<host>>. The removed keys load with a warning and are
dropped by the 0.25 realignment: they are entries in config.Retired (the server's, kbs[] and token keys) and
kb.SettingsRetired. A client that sends no header (CI, a timer, a client config that cannot carry one, such as Kiro's
today) writes as its token.
A future change here must preserve all of these:
- the header never authorizes anything;
- the token trailer is appended last by the server, and a forged one is dropped however it is spaced;
- the header goes on tools/call only;
- Cartographer's senders encode by the allowlist;
- an HTTP entry names an author only when every KB it reaches agrees on it.