@@ -119,6 +119,94 @@ For significant decisions:
119119
120120-->
121121
122+ ## [ 2026-04-11-180000] ` Entry.Author ` is server-authoritative, not client-authoritative
123+
124+ ** Status** : Accepted
125+
126+ ** Context** : The ` Entry.Author ` field on hub entries is copied verbatim from
127+ the client's publish request (` handler.go:82 ` ). It's optional, freeform, and
128+ unauthenticated — a client with a valid token for project ` alpha ` can publish
129+ entries claiming ` Author: "bob@acme.com" ` regardless of who actually
130+ authenticated. This is the same spoofing pattern as ` Origin ` (audit finding
131+ H-04) and was flagged as audit finding H-22 with three options: keep, drop,
132+ override, or promote. The decision was never formally closed.
133+
134+ The premise that resolved it: ** identity is eventually part of the token** .
135+ Under the sysadmin-registry MVP, the server already knows ` {user_id, project} `
136+ from the authenticated token. Under the PKI stretch, the signed claim carries
137+ identity cryptographically. In both models, the client has nothing to say about
138+ authorship that the server doesn't already know with higher confidence.
139+
140+ ** Decision** : ` Entry.Author ` is ** server-authoritative** . The server stamps it
141+ from the authenticated identity source on every publish. The client's
142+ ` pe.Author ` input is ignored (or rejected — implementation choice, not
143+ semantic difference). The field stays in the wire format but its semantics
144+ change from "whatever the client said" to "whatever the server's auth layer
145+ resolved."
146+
147+ Stamping source by phase:
148+
149+ - ** Today (pre-registry)** : ` Author = ClientInfo.ProjectName ` , same source as
150+ the ` Origin ` server-enforcement fix (H-04). Lossy but consistent.
151+ - ** Registry MVP** : ` Author = users.json ` row's ` user_id ` (e.g.,
152+ ` alice@acme.com ` ). Precise per-human attribution.
153+ - ** PKI stretch** : ` Author = signed claim's sub field ` . Cryptographic identity.
154+
155+ ** Rationale** : Dropping the field is wrong because the registry MVP will
156+ already give us a per-user identity to stamp — removing Author just to re-add
157+ it later is churn. "Override" and "promote" are cosmetically different forms
158+ of the same decision (server fills from auth context); "promote" is what
159+ happens naturally once the registry MVP types the field as ` UserID ` .
160+ Client-sourced Author is indefensible because it replicates the Origin
161+ spoofing vector in a second field.
162+
163+ ** Consequence** :
164+
165+ - The Author field stays on the wire and in ` Entry{} ` .
166+ - Client-side code that populates ` pe.Author ` from local config becomes a
167+ no-op. Audit ` ctx connect publish ` and ` ctx add --share ` for any such
168+ code paths before the server-enforcement fix lands.
169+ - ` handler.go publish() ` fills Author from the authenticated context (the
170+ same ` ClientInfo ` that H-04 pulls for Origin). Single unified
171+ auth-to-handler pipe.
172+ - ` docs/security/hub.md ` "Compromised client token" section gets rewritten:
173+ attribution becomes ** wrong** on compromise (attacker's token maps to
174+ attacker's identity), not ** forgeable** (attacker cannot stamp someone
175+ else's name).
176+ - The sysadmin-registry spec (` specs/hub-identity-registry.md ` , tasked)
177+ MUST include a ` user_id ` field per row — it's the stamping source.
178+ - Three open tasks collapse into one: H-22 resolves to "implement
179+ server-authoritative Author" instead of "decide Author fate." TASKS.md
180+ updated.
181+
182+ ** Alternatives considered** :
183+
184+ - ** Keep client-authoritative** : rejected. Same spoofing vector as Origin;
185+ trivially defeats any downstream attribution check.
186+ - ** Drop the field** : rejected. The registry MVP will need per-human
187+ attribution anyway. Dropping today is churn that gets undone
188+ immediately.
189+ - ** Override at client-side before publish** : rejected. Puts the security
190+ boundary on the wrong side of the trust zone. Must be server-side.
191+
192+ ** Follow-up — client-advisory metadata** : the client still has useful
193+ information to share that isn't an identity claim: a human-friendly
194+ display name, the machine that made the publish, the tool version, a
195+ CI system label, a team/role handle. This lives on a ** new sibling
196+ field ` Meta ` ** (a ` ClientMetadata ` sub-struct), not on ` Author ` . The
197+ separation of types is what protects the security property: ` Author `
198+ is reserved for server-authoritative identity, ` Meta ` is
199+ client-advisory and explicitly labeled as such in any rendered
200+ surface. ` Meta ` fields are size-capped individually (256 bytes) and
201+ in aggregate (2 KB), validated for plain-string content (no
202+ newlines, no control characters), and never claimed as attribution
203+ in any API response. The renderer MUST label ` Meta ` -sourced values
204+ with prose like "client label" or "client-reported" so readers
205+ cannot mistake them for authoritative identity. See TASKS.md for
206+ the implementation task.
207+
208+ ---
209+
122210## [ 2026-04-09-001332] Architecture skill pipeline is a triad not a quartet
123211
124212** Status** : Accepted
0 commit comments