Skip to content

Commit 6ba64f0

Browse files
committed
handover to @parlakisik.
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
1 parent 67c21db commit 6ba64f0

15 files changed

Lines changed: 2983 additions & 44 deletions

File tree

.context/DECISIONS.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)