Why: RDM delegated rename rebuilt the creator Host and could commit a Backend project rename after the delegated operator was disabled.
Evidence: SQLite and isolated PostgreSQL targeted checks cover operator/creator disable order, two processes, sorted locks, auth-off local compatibility, and HTTP/command Host wiring; exact committed-head replay remains pending.
Impact: Adds a request-bound single-project extension capability and checks operator plus creator in the final Backend transaction. Global ScopedHost cannot derive delegated authority; RDM cross-database atomicity remains outside the promise.
Refs: Approved Documents delegated-projection amendment 5783940, U3 A1-A4 candidate adcd8fe, Dev feature/iam-u3-account-directory-recovery.
22 KiB
IAM U3 A1: account write fence inventory
Status: stage 4–5 implementation inventory; the fixed candidate is the commit containing this file, and stage 6 human review remains pending. The route table records the 4053c4d baseline finding and the intended final sink; its Now column is historical, not a claim about the amended candidate. Scope follows the approved Documents A1–A4 amendment fb3a8214df963697ebd745ad11eb852ed9444fe3 and approved delegated-actor amendment 5783940e4c434102cbca8c22bd282d87b28fa840.
The 66 non-GET /api registrations were enumerated from internal/server/server.go. Paths below omit /api. A means s.owner(r) (auth-off: implicit store.OwnerDefault); B means body owner, K redeemed-key owner, S current password-session owner. Target resource owner never substitutes for actor. In the historical Now column, T means the baseline already had a final Store check; M means a Backend DB write lacked one; E means an external or process-local effect; — means no business mutation or permitted credential cleanup. D means a short Store transaction with actor account lock and enabled read before resource/scope/conversation locks; F means stage immutable bytes, then D for the visible DB reference; X means last effective-state check before external action plus cancellation; C means D at each command handler's actual sink; P means D for Backend projection and X at the separate RDM callback. Vnn identifies the route for traceability; verification uses representative concurrency cases and complete source-to-sink review, rather than one duplicate test per route.
| # | Route → handler (source line) | Actor | Final sink | Now | Required fence | Verify |
|---|---|---|---|---|---|---|
| 01 | POST /auth/login → login (accounts.go:148) |
B | CreateSessionVerified |
T | retain account/version transaction check | V01 |
| 02 | POST /auth/login-key/redeem → redeemLoginKey (accounts.go:106) |
K | RedeemLoginKey; failed-attempt security counter |
T | retain account/grant transaction check | V02 |
| 03 | POST /auth/recovery-grants/redeem → redeemLoginKey (accounts.go:106) |
K | same as 02 | T | same as 02 | V03 |
| 04 | POST /auth/logout → logout (accounts.go:202) |
A | DeleteSession credential cleanup |
— | permit cleanup after disable | V04 |
| 05 | POST /auth/password → changePassword (accounts.go:214) |
S | checked setup/forced/active password Store transaction | T | retain session/account recheck | V05 |
| 06 | POST /admin/users → createAdminUser (accounts.go:321) |
A | CreatePendingAccount |
T | retain actor enabled/admin transaction check | V06 |
| 07 | PATCH /me/profile → patchMeProfile (accounts.go:394) |
S | PatchAccountProfile |
T | retain actor/target transaction check | V07 |
| 08 | PATCH /admin/users/{userId}/profile → patchAdminAccountProfile (accounts.go:419) |
A | PatchAccountProfile |
T | retain actor/target transaction check | V08 |
| 09 | PUT /admin/users/{userId}/state → setAdminAccountState (accounts.go:444) |
A | SetAccountEnabled |
T | retain shared account lock and in-tx actor check | V09 |
| 10 | POST /admin/users/{userId}/recovery-proposals → createAdminRecoveryProposal (recovery.go:7) |
A | CreateRecoveryProposal |
T | retain actor/target transaction check | V10 |
| 11 | DELETE /admin/users/{userId}/recovery-proposals/{id} → revokeAdminRecoveryProposal (recovery.go:47) |
A | RevokeRecoveryProposal |
T | retain actor/target transaction check | V11 |
| 12 | POST /auth/recovery-proposals/{id}/confirm → confirmRecoveryProposal (recovery.go:81) |
S | ConfirmRecoveryProposal |
T | retain owner/session proof in transaction | V12 |
| 13 | POST /auth/recovery-proposals/{id}/rotate-grant → rotateRecoveryGrant (recovery.go:105) |
S | RotateRecoveryGrant |
T | retain owner/session proof in transaction | V13 |
| 14 | POST /admin/users/{owner}/password → resetAdminUserPassword (accounts.go:468) |
A | 410 response only | — | no business sink | V14 |
| 15 | POST /admin/users/{owner}/login-keys → issueAdminLoginKey (accounts.go:478) |
A | 410 response only | — | no business sink | V15 |
| 16 | DELETE /admin/users/{owner}/login-keys/{id} → revokeAdminLoginKey (accounts.go:494) |
A | RevokeLoginKeyCascade |
M | D: lock/recheck admin actor too, then target/grant | V16 |
| 17 | POST /projects → createProject (server.go:713) |
A | CreateProject or RDM Create + Host projection |
M/E | P | V17 |
| 18 | PATCH /projects/{id} → patchProject (server.go:734) |
A | RDM Rename or RenameProject; UpdateProjectSettings |
M/E | P; D for every settings write | V18 |
| 19 | DELETE /projects/{id} → deleteProject (server.go:812) |
A | RDM Archive or DeleteProject |
M/E | P | V19 |
| 20 | POST /projects/{id}/conversations → createConversation (server.go:875) |
A | CreateConversation[InProject]; RDM Work precheck |
M | D; keep domain work check | V20 |
| 21 | PATCH /conversations/{id} → patchConversation (server.go:914) |
A | update, permission, note, flags: separate Store writes | M | D at each final write | V21 |
| 22 | DELETE /conversations/{id} → deleteConversation (server.go:950) |
A | DeleteConversation |
M | D | V22 |
| 23 | POST /conversations/{id}/messages → postMessage (server.go:956) |
A | message, run, message-run link; optional Bus dispatch | M/E | D per write; C for command; actor lock before conversation lock | V23 |
| 24 | POST /conversations/{id}/rewind → rewindFiles (rewind.go:27) |
A | checkpoint files, then AppendMessage |
M/E | X for rewind, D for message | V24 |
| 25 | POST /conversations/{id}/regenerate → regenerateMessage (borrow.go:159) |
A | message flags/exclusion, regen marker, rewind files, new run | M/E | D per write, X for rewind | V25 |
| 26 | POST /messages/{id}/exclude → excludeMessage (borrow.go:122) |
A | ExcludeRunGroup(messageID) |
M | D; derive conversation owner in transaction | V26 |
| 27 | POST /messages/{id}/variant → selectVariant (borrow.go:146) |
A | SelectVariant(messageID) |
M | D; derive conversation owner in transaction | V27 |
| 28 | POST /secrets → createSecret (borrow.go:21) |
A | AddSecret |
M | D | V28 |
| 29 | POST /secrets/{id}/activate → activateSecret (borrow.go:39) |
A | ActivateSecret |
M | D | V29 |
| 30 | PATCH /secrets/{id} → renameSecret (borrow.go:43) |
A | RenameSecret |
M | D | V30 |
| 31 | DELETE /secrets/{id} → deleteSecret (borrow.go:54) |
A | DeleteSecret |
M | D | V31 |
| 32 | POST /context-entries → saveContextEntry (borrow.go:64) |
A | SaveContextEntry |
M | D | V32 |
| 33 | DELETE /context-entries/{id} → deleteContextEntry (borrow.go:78) |
A | DeleteContextEntry |
M | D | V33 |
| 34 | POST /prompt-commands → saveUserPromptCommand (borrow.go:87) |
A | SaveUserCommand, then process Bus register |
M/E | D; X before Bus mutation | V34 |
| 35 | DELETE /prompt-commands/{id} → deleteUserPromptCommand (borrow.go:102) |
A | DeleteUserCommand, then Bus unregister |
M/E | D; X before Bus mutation | V35 |
| 36 | POST /runs/{id}/cancel → cancelRun (server.go:1278) |
A | PG EnqueueCommand or local cancel handle |
M/E | D for enqueue with in-tx run owner; X locally | V36 |
| 37 | POST /hitl/{id}/resolve → resolveHITL (server.go:1314) |
A | local HITL/card/event writes or PG EnqueueCommand |
M | D at each Store sink, including queue | V37 |
| 38 | POST /cards/{id}/actions/{actionId} → cardAction (server.go:1401) |
A | Cards action or Bus confirm, then receipt message | M/E | D for card/message; C for confirmed handler | V38 |
| 39 | POST /cards/{id}/pin → cardPin (server.go:1453) |
A | card version write | M | D | V39 |
| 40 | POST /cards/{id}/dismiss → cardDismiss (server.go:1462) |
A | card version/event write | M | D | V40 |
| 41 | POST /cards/{id}/to_project → cardToProject (server.go:1386) |
A | project card copy and original dismissal | M | D at both writes | V41 |
| 42 | POST /projects/{id}/uploads → upload (server.go:1469) |
A | Blob + artifact row, card, knowledge/vector index | M/E | F; D at later card/index writes | V42 |
| 43 | PUT /artifacts/{id}/content → putArtifactContent (server.go:1651) |
A | in-place Blob overwrite before SQL UPDATE | M/E | F using new immutable blob ref, guarded DB pointer swap | V43 |
| 44 | DELETE /artifacts/{id} → deleteArtifact (server.go:1656) |
A | metadata, index/vector rows, Blob deletion | M/E | D for visible rows; X for post-commit GC | V44 |
| 45 | POST /profiles → saveProfile (server.go:1667) |
A | profile Store row | M | D | V45 |
| 46 | DELETE /profiles/{id} → deleteProfile (server.go:1683) |
A | profile Store row | M | D | V46 |
| 47 | POST /prompts → savePromptModule (server.go:2023) |
A | prompt Store row | M | D | V47 |
| 48 | DELETE /prompts/{id} → deletePromptModule (server.go:2044) |
A | prompt Store row | M | D | V48 |
| 49 | PUT /settings/user → putSettingsUser (server.go:1717) |
A | settings transaction, then separate snapshot row | M | D for settings and snapshot | V49 |
| 50 | PUT /settings/project/{id} → putSettingsProject (server.go:1726) |
A | settings transaction, then separate snapshot row | M | D for settings and snapshot | V50 |
| 51 | PUT /settings/system → putSettingsSystem (server.go:1782) |
A | system settings and snapshot under synthetic owner | M | D with request actor, not OwnerSystemRow |
V51 |
| 52 | POST /tools → createUserTool (server.go:1815) |
A | process Registry register; future HTTP tool execution | E | X for registration and each tool execution | V52 |
| 53 | DELETE /tools/{name} → deleteUserTool (server.go:1834) |
A | process Registry unregister | E | X | V53 |
| 54 | POST /plugins/{id}/enabled → setPluginEnabled (platform_api.go:132) |
A | SetPluginEnabled upsert |
M | D | V54 |
| 55 | POST /plugins/{id}/activation → setSurfaceActivation (platform_api.go:251) |
A | activation transaction, separate audit row | M | D for both writes | V55 |
| 56 | POST /surfaces → submitSurface (platform_api.go:194) |
A | snapshot transaction, then event/pending row | M | D for snapshot/event writes | V56 |
| 57 | POST /surfaces/{id}/actions → submitSurfaceAction (platform_api.go:271) |
A | action insert, Bus dispatch, action-state update | M/E | D for both action writes; C for dispatch | V57 |
| 58 | POST /plugins/reload → reloadPlugins (server.go:1908) |
A | process registry/sidecar changes and UpsertPlugin |
M/E | X for process changes; D for DB writes | V58 |
| 59 | POST /commands/{id} → execCommand (server.go:1211) |
A | Bus audit/card/pending or arbitrary command handler/RDM | M/E | C, X for external handler | V59 |
| 60 | POST /projects/{id}/knowledge → addKnowledge (server.go:2055) |
A | AddKnowledge batch transaction |
M | D; in-tx project/owner check | V60 |
| 61 | POST /tasks → createBackgroundTask (platform_api.go:16) |
A | task insert | M | D | V61 |
| 62 | POST /tasks/{id} → updateBackgroundTask (platform_api.go:52) |
A | task update | M | D | V62 |
| 63 | POST /memory → putMemory (platform_api.go:69) |
A | memory upsert | M | D | V63 |
| 64 | POST /audit → postAudit (platform_api.go:112) |
A | client-origin business audit row | M | D; security-denial audit is a separate exception | V64 |
| 65 | POST /admin/backup → backup (server.go:2224) |
A | directory + SQLite backup file | E | X and explicit admin authorization | V65 |
| 66 | POST /admin/recovery-holds/{runId}/release → releaseRecoveryHold (accounts.go:526) |
A | recovery-hold UPDATE | M | D with actor admin check and run/hold owner lookup | V66 |
The route count is not a full write count. GET /api/llm/budget → llmBudget (platform_api.go) calls AccountTokens(owner, 0, budget); the amended Store returns a read-only total when tokens are zero. V67 therefore has no business write. Ordinary authenticated GETs can refresh session last_seen_at as credential maintenance; session/API-key in-flight revocation remains a separate impact analysis.
Host and run manager exits
| ID | Entry and actor | Final sink | Now → required fence | Verify |
|---|---|---|---|---|
| H01 | inprocHost.KVPut, host owner (hostcaps.go:295) |
PutPluginKV |
M → D | VH01 |
| H02 | inprocHost.Audit, host owner; actor label is plugin:<id> (hostcaps.go:309) |
AppendAuditEvent |
M → D using host owner | VH02 |
| H03 | KnowledgeWrite/Publish/Delete, host owner (hostcaps.go:350-424) |
knowledge rows, vector rows; publish deletes then writes | M/E → D at every DB sink; X before embedding/remote model | VH03 |
| H04 | TaskCreate/Update, host owner (hostcaps.go:427-443) |
background-task rows | M → D | VH04 |
| H05 | EnsureProject/RenameOwnedProject, host owner (project_lifecycle.go:125-135) |
Backend projection rows, RDM callback upstream | M/E → actor D before project-name lock; X at callback | VH05 |
| H06 | SurfaceSave, host owner (hostcaps.go:481) |
surfacesvc.Submit snapshot/event |
M → D at Store sinks | VH06 |
| H07 | Complete, host owner (hostcaps.go:319) |
external LLM request | E → X with context cancellation | VH07 |
| H08 | scoped Host reconstructed from owner/plugin ID (hostcaps.go:103) |
same H01-H07 methods | M/E → same final sink fences; early requireEffective alone is insufficient |
VH08 |
| G01 | startClaimedRun/subagent spawn, run owner (runmgr.go:1443,1924) |
CreateRun, UpdateRun and admission rows |
M → D for business state; keep failure-only cleanup explicit | VG01 |
| G02 | Emit, AskHITL, cards, plan/todos (runmgr.go:676-935) |
events, HITL/card, plan/todo rows | M → D at each final Store write | VG02 |
| G03 | WriteArtifact, run owner (runmgr.go:968) |
Blob, artifact, event, knowledge/vector rows | M/E → F then D for each later write | VG03 |
| G04 | ExecuteTool, run owner (runmgr.go:1080-1167) |
Registry HTTP/command/filesystem side effects, output artifacts | E/M → X before external call; C/D for command and output sinks | VG04 |
| G05 | stageInputs, checkpoint recording/rewind and subagent transcript (inputs.go:17-90, rewind.go:37-99, subagent_transcript.go:46-84) |
workspace files | E → X before each effect and cancellation; no cross-system atomic claim | VG05 |
| G06 | ResolveHITL, run/card owner (runmgr.go:1780-1860) |
HITL/card/event and possible run writes | M → D | VG06 |
| G07 | background notice delivery and subagent completion (runmgr.go:1982-2041,2444-2455) |
run/message/event plus transcript files | M/E → D for business rows, X for files | VG07 |
| G08 | settleRunWithFence, run owner (runmgr.go:1700-1722) |
final run + assistant message | T for enabled check in store/recovery.go:184-220; retain narrow failure-state exception |
VG08 |
| G09 | OnUsage, run owner (app.go:668-680) |
usage and budget audit rows | M → D or documented accounting-only exception | VG09 |
At the 4053c4d baseline, Host/runmgr effective-account queries were early checks, Bus confirmation and Surface writes had separate sinks, and artifact content could be overwritten in place before a checked SQL update. These are the reasons for the sink-level mapping below; file paths and old line numbers in the historical tables are navigational rather than final-SHA evidence.
Amended candidate mapping and limits
This section supersedes the historical Now column when checking the amended candidate. V01–V15 retain their existing account/credential transaction or response-only behavior; V16 uses actor-checked login-key revocation. V17–V23 route project, conversation, message, run creation and link writes through account-checked Store entries. V24–V35 use guarded file/message and borrow/secret/context/command entries; V25 prepares the message exclusion and regeneration marker in one checked transaction, then consumes the marker only with successful run settlement. V36–V44 guard command queue, HITL, card, surface/event, artifact metadata and index writes. Artifact content updates switch immutable blob references in the checked transaction. V45–V51 guard profile, prompt, settings and snapshot writes, using the request actor for system settings. V52–V59 combine last checks for process/HTTP/tool effects with guarded plugin, surface, card, command pending and audit DB writes. V60–V66 use guarded knowledge, task, memory, audit and hold updates. V65 refuses whole-database backup under authenticated mode because administrator status does not grant access to other owners' business content; auth-off backup has a last check. V67 is read-only at zero tokens.
VH01–VH04/VH06/VH08 use the Host owner and final Store account checks for Backend-owned rows. H05 delegated rename now derives a single-project projection from the current authenticated lifecycle or command Host; its final Store transaction locks the real operator and creator in stable order, checks both effective states and verifies the persisted project owner. The global NewScopedHost cannot derive this delegated capability; its original creator-owned methods remain. VH07 checks immediately before its external model call and passes the caller context. VG01–VG09 use checked run, admission/lease, event, HITL, card, artifact, index, notice, command and settle sinks. File staging, transcript, tool HTTP/search/code execution and overlay writeback check just before effects and propagate cancellation. Failed/cancelled/interrupted run sealing, lease release, notice deletion and failed-start marker deletion are narrowly scoped safety cleanup; positive token usage is accounting for model work already incurred.
The H05/V17–V19 late-write blocker was reproduced in the earlier candidate and repaired under the separately approved Documents 5783940 plan. Tests cover creator/operator effective state, a disable between early check and final write on SQLite/PG, a two-process PG disable, opposite lock order progress, and both HTTP PATCH and no-conversation command Host sources. The RDM database authorization/intent commit is still separate from Backend's account/project transaction; a permission withdrawal or cross-database failure is handled by the existing I1 failure intent and recovery path rather than a zero-window assertion. Completion and stage 6 claims require exact-HEAD receipts and human review.
The account lock closes the account-enabled versus Backend-owned business write race on the same database. It does not prove that a session or API key revoked after request authentication cannot complete a write while that account remains enabled; that is a distinct credential-version fence and outside this amendment's promise. RDM commits in its own database and external requests, processes and files cannot share the Backend transaction. The last check can race with those effects, and a multi-file operation can leave earlier files committed when a later check fails. No cross-database or external zero-window claim is made.
Sink discovery still required: Backend source alone cannot enumerate every dynamically registered Bus handler, plugin sidecar command, or the RDM callback's independent database writes. V17–V19, V23, V38, V57–V59 and VH05 must record the actual composed handlers and their final sinks in the tested composition; an unknown or unguarded handler blocks the A1 completion claim. The external HTTP tool destination and filesystem actions depend on runtime configuration, so VG04/VG05 must verify the actual enabled tool set and cancellation points.
Common implementation and verification gate
- Build one short Store write-transaction primitive:
Begin→ PG actor advisory lock (account:<owner>) →SELECT enabled ... FOR UPDATE→ existing resource/scope/conversation/admission locks → resource authorization and write → commit. The actor lock must precede all other PG locks, including project-name locks; participating account owners use sorted order, then bootstrap lock where required.SetAccountEnabledalready takes the account lock (store/account_state.go:20-45);lockAccountUserssupplies sorted PG account locking (store/account_model.go:115-145). SQLite uses_txlock=immediate(store/store.go:188-199). In auth-off mode, only a missingOwnerDefaultrow is implicitly enabled; an existing disabled row rejects (app/account_guard.go:10-23). No DB connection or transaction spans request body decoding, embedding, tool execution, RDM callback, or full HTTP lifetime. - Make methods with only resource IDs derive owner inside the guarded transaction. Pass actor separately for system settings (
OwnerSystemRow), delegated Host calls, commands, queue commands, audit and background jobs. Preserve project/duty authorization as a separate condition; account enabled does not grant resource access. For files, stage invisible immutable bytes and switch the visible DB reference inD; failed transaction deletes staged bytes, post-commit old-blob GC may continue. RDM's independent database keeps the explicit cross-database window from the amendment. - Review every V/VH/VG route against its final sink on the fixed candidate SHA. Use deterministic pause-and-disable tests for representative project, message/run, artifact, settings, command/surface and Host/runmgr paths on SQLite and isolated PostgreSQL. Test account lock ordering with two PG connections. Verify the existing credential routes' in-transaction ordering. For external effects, verify the last check and cancellation and state the remaining check-to-effect window. Record exact command, tested SHA and output in the Dev task card; local SELF is not independent REPLAY or CI GATE.
Stage 6 stops if any Backend-owned business write lacks a final actor-enabled transaction fence, if artifact content remains visibly mutable after disable via in-place overwrite, or if an external/RDM effect is described as same-DB atomic. Session/API-key in-flight revocation is a separate amendment impact analysis; enabled checks do not prove credential-revocation fencing.