You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Split out of #7749 so it is decided deliberately rather than inherited. #7749's fix (PR #7940) deliberately left this ordering untouched — that was the dispatch instruction, and this card is the other half of it.
Background
The five /meta write sites resolve the recorded actor as, in effect:
X-Actor header ?? the authenticated identity
Until PR #7940 the second limb never produced anything (req.user / req.userId are never set on this transport), so the ordering was inert: the header was the only limb that ever resolved. #7940 fixed that producer, which makes the precedence live and load-bearing for the first time.
The consequence: an authenticated caller who holds manage_metadata can attribute a metadata write to somebody else by sending X-Actor: <someone-else>. The sys_metadata_audit.actor and sys_metadata_history.recorded_by rows will name that other person. Today's behaviour is pinned by a test in packages/rest/src/meta-write-actor-identity.test.ts that is explicitly labelled as the test to change when this is ruled on.
⚠️ This is not a live regression introduced by #7940 — the header behaved this way before. What changed is that it now sits in front of a limb that actually produces a value, so it is a real precedence rather than a theoretical one.
Premises (each with its re-check)
Precedence is header-first: git grep -n "resolveMetaWriteActor" origin/main -- packages/rest/src and read the method — a well-formed string header returns before resolveExecCtx is consulted.
The consumer census (who sets X-Actor, and whether anything depends on it out-ranking the session) was requested from A bearer-authenticated admin metadata write is stamped actor: 'system' — req.user / req.userId are unset on the /meta PUT path #7749's dev and reported in its <!-- os-dev-report --> on that issue. ⚠️ I am citing that report, not an independent census of my own — re-read it before ruling, and re-run git grep -rn "X-Actor\|x-actor" origin/main plus the console side, since it is the load-bearing input for option B.
Should X-Actor continue to outrank the authenticated identity when stamping /meta write audit and history rows?
Options
A — Authenticated identity wins; X-Actor is ignored (or accepted only for machine/system callers). The audit trail then always names the principal the request was actually authorized as, and cannot be self-reported. Strongest audit integrity. Risk: breaks any legitimate delegation/impersonation flow that relies on the header today — which is exactly what the census must establish before this is safe.
C — Header-first, but gated. Honour X-Actor only from callers holding a dedicated capability (an impersonation/service grant), otherwise fall back to the authenticated identity. Preserves a real delegation path while closing self-attribution for ordinary admins. Cost: a new capability and its enforcement — the largest change of the three.
Recommendation: A if the census is empty, otherwise C
If nothing legitimate sets X-Actor today, A is the honest default and the cheapest: an audit field that any writer can spoof is weaker than it looks, and #7749 exists precisely because the trail could not answer "who changed this". If something does depend on it, C keeps that flow working without leaving every manage_metadata holder able to sign someone else's name. ⛔ I would not pick B silently — status quo is defensible, but only as a stated decision, because "the header wins" is currently an accident of ordering that nobody chose.
Four-lens block
Platform long-term coherence — A and C make the audit trail mean one thing ("the authorized principal"); B leaves two meanings behind one column, resolved by whichever caller spoke last. C adds a capability, i.e. a small amount of special-case surface, in exchange for keeping delegation expressible.
AI-agent error-resistance — A is strongest: an agent cannot mis-attribute a write by copying a header from an example. B is weakest — an audit field that is self-reported reads to an agent (and an auditor) as authoritative when it is not. C sits between, and makes the delegation path explicit rather than ambient.
Startup scope discipline — A is a deletion (remove a limb); B is zero work; C is declare-and-maintain (a new capability is a permanent obligation). Discipline favours A over C unless the census forces C.
Related: #7749 (the producer fix, PR #7940), #7748 (publish/rollback writes no audit row — inherits whatever is decided here), packages/rest/src/meta-write-actor-identity.test.ts (the test that pins today's ordering and should change with the ruling).
Split out of #7749 so it is decided deliberately rather than inherited. #7749's fix (PR #7940) deliberately left this ordering untouched — that was the dispatch instruction, and this card is the other half of it.
Background
The five
/metawrite sites resolve the recorded actor as, in effect:Until PR #7940 the second limb never produced anything (
req.user/req.userIdare never set on this transport), so the ordering was inert: the header was the only limb that ever resolved. #7940 fixed that producer, which makes the precedence live and load-bearing for the first time.The consequence: an authenticated caller who holds
manage_metadatacan attribute a metadata write to somebody else by sendingX-Actor: <someone-else>. Thesys_metadata_audit.actorandsys_metadata_history.recorded_byrows will name that other person. Today's behaviour is pinned by a test inpackages/rest/src/meta-write-actor-identity.test.tsthat is explicitly labelled as the test to change when this is ruled on.Premises (each with its re-check)
git grep -n "resolveMetaWriteActor" origin/main -- packages/rest/srcand read the method — a well-formed string header returns beforeresolveExecCtxis consulted.X-Actor, and whether anything depends on it out-ranking the session) was requested from A bearer-authenticated admin metadata write is stampedactor: 'system'—req.user/req.userIdare unset on the/metaPUT path #7749's dev and reported in its<!-- os-dev-report -->on that issue.git grep -rn "X-Actor\|x-actor" origin/mainplus the console side, since it is the load-bearing input for option B.sys_metadata_audit.actordefaults to'system',sys_metadata_history.recorded_bytoNULL(recordMetadataAudit,sys_metadata_history.recorded_by是lookup('sys_user')却存哨兵字符串'system'——声明的类型与实际存的值不是一回事 #4556).The question
Should
X-Actorcontinue to outrank the authenticated identity when stamping/metawrite audit and history rows?Options
X-Actoris ignored (or accepted only for machine/system callers). The audit trail then always names the principal the request was actually authorized as, and cannot be self-reported. Strongest audit integrity. Risk: breaks any legitimate delegation/impersonation flow that relies on the header today — which is exactly what the census must establish before this is safe.actor: 'system'—req.user/req.userIdare unset on the/metaPUT path #7749 was filed to make answerable.X-Actoronly from callers holding a dedicated capability (an impersonation/service grant), otherwise fall back to the authenticated identity. Preserves a real delegation path while closing self-attribution for ordinary admins. Cost: a new capability and its enforcement — the largest change of the three.Recommendation: A if the census is empty, otherwise C
If nothing legitimate sets
X-Actortoday, A is the honest default and the cheapest: an audit field that any writer can spoof is weaker than it looks, and #7749 exists precisely because the trail could not answer "who changed this". If something does depend on it, C keeps that flow working without leaving everymanage_metadataholder able to sign someone else's name. ⛔ I would not pick B silently — status quo is defensible, but only as a stated decision, because "the header wins" is currently an accident of ordering that nobody chose.Four-lens block
actor: 'system'—req.user/req.userIdare unset on the/metaPUT path #7749 came from a QA run: every normal console write recordedsystem). The pull for keeping header-first is unmeasured pending the census — that is the input that decides A vs C, and it is the reason this is a decision rather than a dispatch.Related: #7749 (the producer fix, PR #7940), #7748 (publish/rollback writes no audit row — inherits whatever is decided here),
packages/rest/src/meta-write-actor-identity.test.ts(the test that pins today's ordering and should change with the ruling).