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
{{ message }}
Repository navigation
security(service-settings, service-messaging, service-datasource, plugin-webhooks): plumbing reads and writes reach the engine with no principal and no system opt-in — the services-lane producers of #21908's closure #21913
Why now.#21908 closes the principal-less hand-off in the security middleware: a non-system engine context with no principal will be denied (403 PERMISSION_DENIED, the seat's verdict on #21908). The measure-first round (6003676228) found the producers that reach the hand-off today. Each must take a route before the deny lands, or the deny breaks it. This card is one slice, a seat-owned sub-issue of #21908 with its domain and priority (domain:services seat 1, #6021, session_011K3zqE8Pv1Evw5hc8tZCnN).
The route for every producer here: the explicit system opt-in that exists today (isSystem: true on the engine call's context). ⛔ No new elevation API. ⛔ No change to what any door authorizes.
The trap to measure, per producer: an isSystem context short-circuits the gates the hand-off still runs before next(): package-managed, system-row, curated-capability, audience-anchor, engine-owned, and the delegated-administration gate. Moving a producer is neutral today only if none of those gates fires on its calls. Measure that per producer (an instrumented run is fine; commit nothing of it). A producer on which a gate fires is reported, not moved.
The producers (rows of 6003676228, positions on origin/maincab63967):
Row 7: service-settingssrc/settings-service.tsSettingsService.loadRows (sys_setting). It is on every REST request's execution-context build (localization), at boot, and on the audit write locale.
Row 8: SettingsService.upsertRow and the audit write in src/settings-service-plugin.ts.
Row 11: service-datasourceloadDatasourceRows (boot). By static read, also src/datasource-admin-plugin.ts (sys_metadata) and src/datasource-secret-binder.ts (sys_secret).
Row 12: plugin-webhookssrc/auto-enqueuer.tsAutoEnqueuer.doRefresh, and src/redeliver-guard.tscreateWebhookRedeliverGuard (static).
Row 14: the emit fan-out, which covers src/messaging-service.tswriteEvent, src/inbox-channel.tssend / writeDeliveredReceipt, src/preference-resolver.tsloadRows and src/recipient-resolver.tsresolveEmail.
Not in this slice: rows 15 (listInbox / readReceiptStates) and 16 (StorageMetadataStore.createFile). Those act on the caller's own rows from a request door, and their posture waits for the maintainer's ruling (the seat's verdict on #21908).
Done when: each producer above passes the explicit system opt-in, or is reported with the gate that fires on it. An instrumented run of the dogfood suite and a booted dev composition records no principal-less context from these functions. The packages' suites are unchanged, and the isSystem census page is current.
Claim: PM loop round 3 · 2026-10-05T21:55Z
Session: session_011K3zqE8Pv1Evw5hc8tZCnN
Account: os-steve (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-21913-principal-less-producers-services
Worktree: objectstack-issue-21913
Domain: domain:services
Seat: domain:services#1 (seat post #6021)
File surface (at origin/maincab63967), per the seat's verdict on #21908 (6003795556):
Corrected at the seat's review of PR fix(services): settings, datasource, webhook and messaging plumbing passes the explicit system opt-in instead of no principal #21940 (one round with the PR line and the changesets): the exported SettingsEngine (find, insert) and SecretStoreEngineLike (delete) gain an optional context on their options parameter, a widening of the published type surface (minor for @objectstack/service-settings and @objectstack/service-datasource). Nothing accepted or refused changes: the dangling actor reference the opt-in would stop refusing is restored at the producer in the same patch round. Each producer takes the explicit system opt-in that exists today; a producer on which a gate before the hand-off fires is reported, not moved.
Thread-read: 6003795556
Serial constraints cleared: at 2026-10-05T21:55Z:
os-dev-report
{
"issue": 21913,
"status": "done",
"branch": "claude/issue-21913-principal-less-producers-services",
"pr": "#21940",
"session": "session_011K3zqE8Pv1Evw5hc8tZCnN — subagent of the PM session (harness Claude-Session line)",
"premise_still_valid": true,
"summary": "Every engine call in the card's named functions (rows 7, 8, 11, 12, 13 and 14) now passes the explicit system opt-in { isSystem: true }. The rows cover SettingsService.loadRows and upsertRow, the sys_setting_audit writer, the runtime-datasource sys_metadata helpers, the sys_secret binder, the two sys_webhook reads, the dispatcher claim path on both outboxes, and the emit fan-out. No elevation API is new and no door's authorization changes. A local, uncommitted instrument at the plugin-security hand-off measured the move over the whole dogfood suite and a booted showcase dev composition, before and after. For the moved functions, principal-less records went from 2168 (dogfood) and 239 (boot) to 0. The gates before the hand-off threw 0 times, and call counts and outcome shapes are identical. One isSystem-keyed engine branch, the dangling-reference check (census row 23), no longer runs on three actor_id lookups. It resolved on all 10 measured calls, so no measured behaviour changed. A local probe shows the latent difference: today an actor_id naming no user is refused, and under the opt-in it is written. That is open_questions[0]. Draft PR #21940 is open at 62960ff, assigned to os-steve, and all 105 derived gates pass.",
"tests": "Unit suites at 10e77fe (merged with main faf8dce; the later merge to 62960ff touches none of these packages): service-settings 614 passed, service-messaging 510, service-datasource 743, plugin-webhooks 165, all exit 0. Typecheck of the four packages exit 0. Pins: one per package, 9 tests. Ablation: 7 legs through scripts/ablation-replace.mjs with anchor hits verified on disk. Each went red under the dropped opt-in, for example 'find on sys_setting: expected undefined to deeply equal { isSystem: true }' and 'update on sys_notification_delivery: expected { isSystem: false } ...'. Each was restored with blob == HEAD and an empty git diff HEAD, and went green again. The pins import src relatively, so no dist is in the path. Instrument runs, all under the lock: dogfood 206 files (205 passed + 1 skipped, 1590 tests passed, 9 skipped) identical before and after. Boot: identical HTTP outcomes (settings PUT x2 200, datasource POST 201, PATCH 200, DELETE 204, anonymous 401). The instrument was reverted with security-plugin.ts blob 5b4ab28045af == HEAD, and the plugin-security dist was rebuilt with ablation-dist-preflight --absent '__p21913Run' PASS on both readings (3 hits in the instrumented dist). ESLint narrowed to the 19 changed TS files: 0 errors, 0 warnings. They are inside the config's packages/**/.ts population, and the config enables no type-aware linting, so no untouched file's verdict can move. Full pnpm lint is CI's.",
"mcp_calls": "0",
"api_writes": "3 — POST /repos/objectstack-ai/objectstack/pulls (pr_create via relay run 37404795666, body read back byte-identical); POST /repos//issues/21940/assignees (label-write via relay run 37404850430, read back matches); POST /repos//issues/21913/comments (this report, scripts/pm/post-stamped.mjs). git push is not REST.",
"producers_moved": [
"row 7 service-settings SettingsService.loadRows: find sys_setting",
"row 8 service-settings SettingsService.upsertRow: existence-probe find + insert sys_setting (its update already carried isSystem); buildSettingAuditWriter write: insert sys_setting_audit",
"row 11 service-datasource loadDatasourceRows (find), loadDatasourceRow (findOne), persistDatasourceRow (findOne + insert/update), deleteDatasourceRow (findOne + delete/update) on sys_metadata; secret binder bind (insert), unbind (delete), resolve (find) on sys_secret",
"row 12 plugin-webhooks AutoEnqueuer.doRefresh (find sys_webhook); createWebhookRedeliverGuard (findOne sys_webhook)",
"row 13 service-messaging SqlNotificationOutbox.claim / claimDigest / reapExpired and SqlHttpOutbox.claim / reapExpired: candidate find, claiming update, read-back find, reap update",
"row 14 service-messaging MessagingService.writeEvent (insert sys_notification); inbox send (insert sys_inbox_message; recipient-locale findOne sys_user via its local helper readRecipientLocale) and writeDeliveredReceipt (insert sys_notification_receipt); PreferenceResolver.loadRows (2 x find sys_notification_preference); RecipientResolver.resolveEmail (findOne sys_user)"
],
"producers_reported": [
"None held back by a gate: across 35245 dogfood + 422 boot principal-less records, the six gates before the hand-off threw 0 times. Only the engine-owned guard matched (bucket + write verb on engine-owned objects), and it returns at isUserContextWrite for a context with no userId.",
"Not a gate, reported: census row 23 (engine assertReferencesResolve skipped for isSystem) stops running on actor_id of sys_notification (writeEvent), sys_inbox_message (inbox send) and sys_setting_audit (audit write). Measured: 10 nested probes before, all resolved; 0 after. Latent: actor_id naming no user — no context = refused VALIDATION_FAILED, isSystem = written (local probe, deleted). See open_questions[0]."
],
"instrument_counts": {
"method": "principal-less records BEFORE / principal-less AFTER / isSystem AFTER, attributed by the first frame after the innermost engine verb frame",
"dogfood": {
"SettingsService.loadRows": "2090 / 0 / 2090",
"SettingsService.upsertRow (probe+insert)": "7 / 0 / 7",
"setting-audit write": "4 / 0 / 4",
"SqlNotificationOutbox.claim": "8 / 0 / 8",
"SqlNotificationOutbox.claimDigest": "8 / 0 / 8",
"SqlNotificationOutbox.reapExpired": "1 / 0 / 1",
"SqlHttpOutbox.claim": "8 / 0 / 8",
"SqlHttpOutbox.reapExpired": "1 / 0 / 1",
"MessagingService.writeEvent": "8 / 0 / 8",
"inbox send (row insert)": "8 / 0 / 8",
"writeDeliveredReceipt": "8 / 0 / 8",
"PreferenceResolver.loadRows": "16 / 0 / 16",
"RecipientResolver.resolveEmail": "1 / 0 / 1",
"totals principal-less": "35245 → 33077 (delta 2168 = sum above)"
},
"boot": {
"SettingsService.loadRows": "42 / 0 / 42",
"SettingsService.upsertRow (probe+insert)": "3 / 0 / 3",
"setting-audit write": "2 / 0 / 2",
"loadDatasourceRows": "1 / 0 / 1",
"persistDatasourceRow": "4 / 0 / 4",
"deleteDatasourceRow": "2 / 0 / 2",
"AutoEnqueuer.doRefresh": "3 / 0 / 3",
"SqlNotificationOutbox.claim": "56 / 0 / 56",
"SqlNotificationOutbox.claimDigest": "56 / 0 / 56",
"SqlNotificationOutbox.reapExpired": "7 / 0 / 7",
"SqlHttpOutbox.claim": "56 / 0 / 56",
"SqlHttpOutbox.reapExpired": "7 / 0 / 7",
"totals principal-less": "422 → 183 (delta 239 = sum above)"
},
"not_reached_by_any_run_pinned_instead": "loadDatasourceRow; secret binder (no composition here wires it); redeliver guard; inbox recipient-locale read (template path); claim-path update and read-back (no pending rows)",
"outcomes": "Per function, counts by object.op, result type, row count and key set are identical before and after, with 0 errors. Content hashes are identical for 11/14 (dogfood) and 7/9 (boot). The differences sit on volatile values: receipt 'at' timestamps, deep links or bodies carrying per-run ids or dates, and the probe's per-phase datasource file path. Nested plugin-audit rows (sys_audit_log, sys_activity) are unchanged in count."
},
"h_readings": {
"H1": "Positions hold at cab6396. Every engine call in each named function is moved. The inbox send's recipient-locale read sits in the local helper readRecipientLocale, which only send calls, and is counted as part of send.",
"H2": "Done as specified. The instrument was local and uncommitted, then reverted with blob == HEAD.",
"H3": "isSystem census OK, and --fix changed nothing: no elevation read site was added. The tenant-audit census did move: the write sites now thread a context, so it was regenerated with --write and the hand-written page figures were updated (provable no-context tenancy-enabled 9 → 2, unreadable 67 → 60, elevated 114 → 121).",
"H4": "Confirmed: suites green with no existing test edited. One existing inbox test pins the exact findOne query shape, so the read opt-in rides the trailing options argument, which the contract prefers. Pins and ablations are listed above.",
"H5": "#21906 has merged into main (faf8dce), and merging main into this branch was clean. git merge-tree against #21877's head 5c40584 (closed draft) is clean at 62960ff. Neither PR's region was edited.",
"H6": "loadRows: same call count (2090 + 42), and the returned settings hash-identical on every call. The cost is one frozen context object, and the middleware exits at the system short-circuit instead of running the gates and the hand-off. No wall time is quoted (shared box).",
"H7": "Results are consumed inside the fan-out. emit() returns the id, counts and per-delivery outcomes, whose one read-derived value is the recipient id resolved from an address the emitter named. Its three in-repo callers (approvals, notify node, comment mentions) relay counts and the id only. The principal-less read returned the same rows."
},
"private_producers": "1 candidate held off-thread. Class: an authenticated request door (any role); a refusal text that carries a configuration row's field. NOT MEASURED whether the caller may read that row.",
"open_questions": [
{
"question": "Census row 23: moving writeEvent, the inbox send and the setting-audit write to isSystem stops the engine's dangling-reference check on their actor_id. Today an actor_id naming no user, which a flow notify node's actorId can author, is refused with VALIDATION_FAILED. Under the opt-in it is stored. Measured traffic is unchanged (10/10 references resolved). Accept the latent difference, or restore the refusal?",
"options": [
"A: accept it as landed. Every write reached the deny closure with the explicit opt-in, and the latent difference is recorded.",
"B: restore the refusal at the producer: writeEvent checks a supplied actorId against sys_user under the opt-in and refuses an unknown one with the same reference_not_found answer. That is one extra read per emit with an actor.",
"C: carry the emitter's principal into emit(). That is a contract change to IMessagingService, and the deny baseline would then need grants on these engine-owned objects."
],
"recommendation": "B, as a follow-up on the closure, with this PR landing as is. Business need (measured): flow notify nodes author actorId, and today a wrong one fails loudly. Long term: contract-first, so the refusal belongs at the producer and not in a silent store. AI-error axis: A silently accepts a declared value the runtime cannot honour, which is the failure the frame ranks worst. Startup: B is one guarded read, with no new API and no grant model. C widens a contract and drags in grant authoring."
}
],
"out_of_scope_findings": [
"carrier: #21908 closure (the seat) · noted, not filed. Producers in this slice's packages that the card does not name still reach the engine with no context (static read; no run exercised them). service-settings: the plugin-built sys_secret store (insert/get/update) and SettingsService.readStoredHandle. service-messaging: SqlOutbox enqueue/ack/list, the email and SMS channels' recipient reads, RecipientResolver.resolveRole/resolveTeam/resolveOwnerOf, the emit dedup lookup, and the template renderer read. resolveOwnerOf is not neutral to move: today the sharing middleware answers a principal-less read of a private object with deny-all, so the opt-in would widen it. Dedupe words: principal-less producer, unnamed producer, messaging outbox ack enqueue, settings secret store, owner_of recipient",
"carrier: the maintainer's ruling on rows 15/16 · noted, not filed. More request-door producers act on the caller's own rows like rows 15 and 16: the inbox unread count, and mark-read / mark-all-read (unreadNotificationIds, upsertReadReceipt, notificationOrganization). Dedupe words: inbox mark read, unread count, read receipt, caller own rows"
],
"gates": [
{
"command": "git push (empty-branch probe, then 6 pushes)",
"exit": 0
},
{
"command": "dispatch-gates --commands --repo objectstack-ai/objectstack at 62960ff: 105 commands",
"exit": "0 x105"
},
{
"command": "dispatch-gates --ran ran.list --repo objectstack-ai/objectstack",
"exit": 0,
"verdict": "105 derived famil(ies) accounted for — 105 run, 0 NOT-MEASURED"
},
{
"command": "os-verify-lock: typecheck x4 packages (merged tree)",
"exit": 0
},
{
"command": "os-verify-lock: vitest service-settings / service-messaging / service-datasource / plugin-webhooks",
"exit": "0 x4"
},
{
"command": "os-verify-lock: ablate.sh (7 legs)",
"exit": "mutated 1 x7 (red), restored 0 x7"
},
{
"command": "os-verify-lock: dogfood chunks 00..06 before / after",
"exit": "0 x7 / 0 x7"
},
{
"command": "os-verify-lock: boot-probe before / after",
"exit": "0 / 0"
},
{
"command": "os-verify-lock: turbo build closure (merged tree) + 8 packages for check:dual-build-cjs-loads",
"exit": "0 / 0"
},
{
"command": "check-system-context-census + --fix",
"exit": "0 / 0 (no change)"
},
{
"command": "eslint --no-inline-config --format json (19 changed TS files)",
"exit": 0
}
],
"line_budget": "n/a",
"deviations": [
"The local instrument edited plugin-security src. That is a stop-list package, but H2 places the instrument at the hand-off. It was never committed, it was reverted with blob == HEAD, and the dist was rebuilt with the marker absent.",
"inbox send's recipient-locale read sits in a local helper (readRecipientLocale) that only send calls. It was moved as part of send, and is named in the PR.",
"The PR changes the type of two exported interfaces: SettingsEngine.find/insert and SecretStoreEngineLike.delete now declare an optional context. No symbol is new on any package entry.",
"The tenant-audit census page and docs/audits counts were regenerated, and the page's hand-written figures updated. The dispatch's file surface named the isSystem census page; this one moved because of the same change.",
"scripts/engine-double-contract.pinned.json gained 5 rows recording the new pinned doubles (--write). The gate asks for exactly this.",
"Measurement was left unfinished by a container restart (about 01:15Z). The reference probe in flight then (exit 137) was re-run under the lock and is not read from its partial log. Everything else had completed with logged exits before the restart.",
"Dogfood and boot instrument runs were measured on the pre-merge tree (cab6396 + this change). The main commits merged afterwards touch none of these packages' files except service-datasource plugin.ts (#21906, outside these functions)."
],
"files_changed": [
".changeset/21913-principal-less-producers.md",
"content/docs/permissions/tenant-audit-census.mdx",
"docs/audits/2026-08-tenant-audit-write-call-sites.counts.md",
"packages/plugins/plugin-webhooks/src/auto-enqueuer.ts",
"packages/plugins/plugin-webhooks/src/redeliver-guard.ts",
"packages/plugins/plugin-webhooks/src/webhook-system-context.pin.test.ts",
"packages/services/service-datasource/src/tests/datasource-system-context.pin.test.ts",
"packages/services/service-datasource/src/datasource-admin-plugin.ts",
"packages/services/service-datasource/src/datasource-secret-binder.ts",
"packages/services/service-messaging/src/fan-out-system-context.ts",
"packages/services/service-messaging/src/inbox-channel.ts",
"packages/services/service-messaging/src/messaging-service.ts",
"packages/services/service-messaging/src/outbox-dispatcher-scope.ts",
"packages/services/service-messaging/src/preference-resolver.ts",
"packages/services/service-messaging/src/recipient-resolver.ts",
"packages/services/service-messaging/src/sql-http-outbox.ts",
"packages/services/service-messaging/src/sql-outbox.ts",
"packages/services/service-messaging/src/system-context.pin.test.ts",
"packages/services/service-settings/src/settings-service-plugin.ts",
"packages/services/service-settings/src/settings-service.ts",
"packages/services/service-settings/src/settings-service.types.ts",
"packages/services/service-settings/src/settings-system-context.pin.test.ts",
"scripts/engine-double-contract.pinned.json"
],
"resume": "Found: remote head 458499d == local, no PR, no report, and only the untracked reference probe in the tree. The probe measures the census row 23 latent difference. Re-ran it under the lock (no context: refused VALIDATION_FAILED; isSystem: written; control written both ways), then deleted it. It was never committed. Then: merged main twice (faf8dce, 9dce635; clean); refreshed the build; re-ran typecheck and the four suites; ran the gate battery, which turned up four reds; fixed them (tenant-audit census regenerated, pin doubles through the dispatch asserts with bounds, ledger rows, extra packages built for the dual-build gate); re-ran the seven ablations on the final pins; ran all 105 gates and --ran at 62960ff; then opened PR #21940 and assigned it."
}
Patch round 1 (one round; decided by the four-axis frame, the axes agree):
Restore the dangling actor-reference refusal at the producer. Under isSystem the engine's reference check no longer runs on actor_id for sys_notification (writeEvent), sys_inbox_message (the inbox send) and sys_setting_audit (the audit write). Today an actor_id naming no user is refused; under the opt-in it would be stored. That is option A of the dev's open question 0, and it is not accepted:
it stores a declared value the runtime cannot honour, the failure the frame ranks worst;
an audit trail is inside this lane's security boundary, which a seat never loosens.
Option C would change the IMessagingService contract. B, in this PR: the producer keeps today's refusal (same code and status) for an unknown actor_id. It prefers an existing engine path that keeps the reference check under the opt-in, and otherwise uses one guarded read; no new API.
Clause-② correction. The exported SettingsEngine (find, insert) and SecretStoreEngineLike (delete) gain an optional context, which widens the published type surface. So the line is Clause-②: yes (widening), minor for @objectstack/service-settings and @objectstack/service-datasource. The claim is amended (6003840075). The PR's line 2 and the changesets move in this round.
The at-tier contract review runs on the patch head (Clause-②: yes).
To the maintainer's open ruling on rows 15 and 16: the inbox unread count and mark-read / mark-all-read also act on the caller's own rows from a request door.
One held candidate, at class level only: an authenticated door's refusal text that carries a configuration row's field. It went to the maintainer privately, and this PR does not change it.
os-dev-report
{
"issue": 21913,
"status": "done",
"branch": "claude/issue-21913-principal-less-producers-services",
"pr": "#21940",
"session": "session_011K3zqE8Pv1Evw5hc8tZCnN",
"premise_still_valid": true,
"round": "patch round 1 (seat verdict 6008259054), plus the coordinator's CI-red follow-up (error-code-provenance) and the #21935 addendum",
"summary": "Each producer that writes a user reference under the opt-in now refuses a dangling one itself, with the engine's own answer. This covers writeEvent (sys_notification.actor_id), the inbox send (sys_inbox_message.actor_id), the setting-audit writer (sys_setting_audit.actor_id), and also upsertRow's insert (sys_setting.user_id). The check is one guarded sys_user read under the opt-in (route b), and the refusal is built by the shared validationFailure constructor from @objectstack/types. That clears the provenance red CI found on e1e6c51 without a spec row, a waiver or a new export. The changeset is now minor for service-settings and service-datasource, carrying 'Clause-②: yes (widening)'. #21935 is on main and merged in (a3c2209). The head is 57f738d.",
"route": "Route (b). Route (a) does not exist. ObjectQL.assertReferencesResolve returns before any probe when context.isSystem is set, and nothing can keep it on. engine.validate() runs no reference check (census row 23). So each producer does one guarded existence read of sys_user by id under the explicit opt-in, before its insert. It refuses only when that read ran and found nothing. A read that cannot run lets the write through, as the engine's fail-open does. The refusal is the engine's own answer: VALIDATION_FAILED with one reference_not_found finding. The message comes from the same catalog entry (renderValidationMessage), with the field's registered label. It is built by validationFailure from @objectstack/types. That package is already a runtime dependency of both packages, and its stamp site is held by the recorded types-to-runtime waiver in the ledger. So neither package stamps the code itself, and the provenance gate is green with no spec row and no new waiver. Why not objectql's ValidationError class: objectql is only a devDependency of both packages. Making the engine a runtime dependency of two services just to throw its class would change the dependency graph, and the order did not ask for that. The types constructor exists for exactly this shape. The helpers are module-internal: service-messaging src/actor-reference.ts (assertActorReferenceResolves) and service-settings src/actor-reference.ts (assertUserReferenceResolves, registeredLabel). Neither is on a package entry, and IMessagingService is unchanged. Call sites: writeEvent before the sys_notification insert, the inbox send before the sys_inbox_message insert, the setting-audit writer before the sys_setting_audit insert, and SettingsService.upsertRow's insert branch for sys_setting.user_id.",
"pins": [
"packages/services/service-messaging/src/actor-reference.pin.test.ts. 4 tests on a real ObjectQL with SqlDriver (better-sqlite3 :memory:). (1) emit to writeEvent with an unknown actor_id is compared with the engine's own refusal of the context-less sys_notification insert. Asserted: code VALIDATION_FAILED; status equal to the engine's (neither carries one, and the door maps the shape to 400); fields[0] {field actor_id, code reference_not_found, constraint.target sys_user}; the name/code/status/message/fields envelope deep-equal; sys_notification count 0. (2) a known actor and no actor are both written. (3) the inbox send with an unknown actor answers {ok:false, error:'inbox insert failed: ' + the engine's message}; sys_inbox_message count 0. (4) a known actor and no actor are both delivered.",
"packages/services/service-settings/src/settings-user-reference.pin.test.ts. 4 tests on a real ObjectQL over an in-file memory driver (its find applies the caller's bound after the filter). (1) set(..., {userId: unknown}) to upsertRow insert is compared with the engine's refusal of the context-less sys_setting insert: the same code/status/fields[0]/envelope assertions; sys_setting count 0. (2) a known user is written. (3) the setting-audit writer with an unknown actor warns 'SettingsServicePlugin: setting-audit write failed: ' + the engine's message; sys_setting_audit count 0. (4) a known actor and no actor are both written, with no warning."
],
"ablation": "4 legs through scripts/ablation-replace.mjs, under the lock, at 57f738d (a first pass at e1e6c51 read the same). Each leg drops one check: the call is deleted, or the checked value is replaced with null. The anchor must hit 1 to 0 on disk, and an independent probe-line count reads 1 before and 0 under the mutation. Then the leg runs its pin and restores. writeEvent (messaging-service.ts, delete the call): red, 1 failed | 3 passed, 'the emit must refuse: expected null not to be null'. inbox send (inbox-channel.ts, delete the call): red, 1 failed | 3 passed, 'expected { ok: true, ... } to deeply equal { ok: false, ... }'. setting-audit (settings-service-plugin.ts, entry.actorId to null): red, 1 failed | 3 passed, 'expected [] to deeply equal [ Array(1) ]'. upsertRow (settings-service.ts, row.user_id to null): red, 1 failed | 3 passed, 'the write must refuse: expected null not to be null'. Every leg restored to blob == HEAD (d978e84d929d, a463ba83828b, d468299688fc, 745d1930ec9f), with git diff HEAD empty and the probe count back to 1. The restored re-run is green, 4 passed (4). The tree is clean after all legs. The pins import the mutated module from src relatively, so no dist is in the path and no rebuild or dist preflight applies.",
"changeset_path": ".changeset/21913-principal-less-producers.md",
"changeset_text": "---\n"@objectstack/service-settings": minor\n"@objectstack/service-messaging": patch\n"@objectstack/service-datasource": minor\n"@objectstack/plugin-webhooks": patch\n---\n\nPlatform plumbing in these four packages now passes the explicit system opt-in ({ isSystem: true }) on its data-engine calls. Until now it reached the engine with no principal and no opt-in, and the security middleware let that through only because of its principal-less hand-off.\n\nClause-②: yes (widening)\n\n- Why yes (widening): two exported option types gain an optional context that an adapter must forward as-is. They are SettingsEngine.find / .insert (@objectstack/service-settings) and SecretStoreEngineLike.delete (@objectstack/service-datasource), so both packages take a minor. An implementation written against the old types still type-checks, and nothing accepted or refused at any door changes.\n- service-settings:SettingsService reads and writes its own sys_setting rows under the opt-in: loadRows, plus the existence probe and insert in upsertRow (the update already used it). The sys_setting_audit writer does too.\n- service-datasource: the sys_metadata helpers behind runtime datasources use the opt-in. They cover boot restore, cluster convergence, and persist and delete behind the admin doors. So do the sys_secret binder's bind, unbind and resolve.\n- plugin-webhooks: the auto-enqueuer's subscription refresh and the redeliver guard's subscription lookup use the opt-in.\n- service-messaging: two paths use the opt-in. One is the dispatcher's claim path: claim, claimDigest and the visibility-timeout reap on both outboxes. The other is the emit fan-out: the sys_notification row, the recipient's address and locale reads, the preference reads, the inbox row and the delivered receipt.\n- A user reference that names no user is still refused. The engine skips its dangling-reference check for an isSystem write, so each producer that writes a user reference checks it first. The checked references are the actor_id of sys_notification, sys_inbox_message and sys_setting_audit, and the user_id of a user-scope sys_setting row. An unknown id is refused with the engine's own answer: VALIDATION_FAILED, one reference_not_found finding, and the same message. A write that names no user is unchanged.\n- What each call reads and writes is otherwise unchanged. None of the gates the middleware runs before its hand-off applies to these objects.\n- ⛔ No new export on any package entry, and no new elevation API.\n",
"pr_line_2": "Clause-②: yes (widening)",
"acceptance_notes_block": "## Acceptance notes\n\n- Producers in these packages that the card does not name. A static read finds that they still reach the engine with no context. No run exercised them, so the measured table never listed them. Without a route, #21908's deny breaks each one, so they are listed for the seat's closure rather than moved here:\n - service-settings: the sys_secret store the plugin builds (insert / get / update), and SettingsService.readStoredHandle.\n - service-messaging: SqlNotificationOutbox and SqlHttpOutboxenqueue, ack and list; the email and SMS channels' recipient reads; RecipientResolver.resolveRole / resolveTeam / resolveOwnerOf; the emit dedup lookup; the template renderer's read.\n - resolveOwnerOf reads a business object, and its posture is not neutral. Today the sharing middleware answers a principal-less read of a private object with a deny-all filter, so an owner_of: recipient on such an object resolves to nobody. Under the opt-in, that filter would be bypassed.\n- Request-door producers that act on the caller's own rows, like rows 15 and 16 (report-only, for the maintainer's ruling): the inbox unread count, and mark-read / mark-all-read (unreadNotificationIds, upsertReadReceipt, notificationOrganization).\n- Census row 23: the dangling user-reference refusal is kept at the producer. The engine skips its dangling-reference check for an isSystem write and has no option to keep it, so each producer that writes a user reference now checks it first. It does one sys_user read by id under the opt-in, then refuses an unknown id with the engine's own answer: VALIDATION_FAILED, one reference_not_found finding, and the same message. The checked references are:\n - the actor_id of sys_notification (writeEvent), of sys_inbox_message (the inbox send) and of sys_setting_audit (the setting-audit writer);\n - the user_id of a user-scope sys_setting row, on the insert in SettingsService.upsertRow. The seat's verdict named the first three. This fourth is the same difference, which this PR introduced when it put the opt-in on that insert.\n- A write that names no user is unchanged. If the read cannot run, the write goes through, which matches the engine's fail-open. There is one extra sys_user read for each write that names a user.\n- Differential pins over a real engine hold each producer's answer equal to what the engine itself answers a context-less insert. Each check was also ablated: with the check dropped, its pin goes red; after the restore, the blob equals HEAD and the pin is green again.\n- The refusal has the same shape but is a different class. It is built by validationFailure from @objectstack/types, already a runtime dependency of both packages, so neither package stamps the code itself. Its name, code, message and fields are the engine's. But instanceof objectql's ValidationError is false: objectql is only a dev dependency here. The callers on these paths read the message or the code: the flow notify node, the inbox SendResult, and the audit writer's warn line. Every door maps the shape to 400 VALIDATION_FAILED by duck-typing.\n- One posture question was noted on a request-door read and is held off-thread. It was not measured.\n",
"tests": "At 57f738d, all under scripts/pm/os-verify-lock.sh: four packages built (exit 0). Typecheck of service-settings, service-messaging, service-datasource and plugin-webhooks exits 0. Both new pins are inside the tsc program (tsc --listFiles finds 1 each; neither tsconfig excludes tests). Unit suites: service-settings 618 passed, service-messaging 514, service-datasource 743, plugin-webhooks 165, all exit 0. Pins: 4 + 4 pass. Ablation: see ablation. ESLint, narrowed: the 26 changed TS files, through the ESLint API with allowInlineConfig false, which is the --no-inline-config of pnpm lint. Evidence (1): the population is read from eslint's own config. isPathIgnored is false for all 26, and calculateConfigForFile gives each 4 to 6 active rules. Evidence (2): lintFiles returned 26 results, with 0 errors and 0 warnings. Evidence (3): the config enables no type-aware linting (no parserOptions.project, as its own header records). The only files it reads, scripts/slot-lookup-baseline.json and scripts/query-options-erasure-baseline.json, are not in this diff, and neither is eslint.config.mjs. So no verdict on an untouched file can move. The full pnpm lint run belongs to CI.",
"gates": {
"derived": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 57f738d: 105 commands (the list matches the one at a3c2209). Each ran at 57f738d. 105 exit 0.",
"ran": "✓ dispatch-gates --ran: 105 derived famil(ies) accounted for — 105 run, 0 NOT-MEASURED (a DERIVED zero — all 105 recorded an exit code and none of them is 3).",
"extra": [
"pnpm --filter @objectstack/spec check:error-code-provenance :: exit 0",
"pnpm check:pm-dispatch-gates :: exit 0"
],
"roster": "The 54 artifact-roster families that --commands prints outside its total, each run at 57f738d: 51 exit 0. 3 exited 2, which is NOT WIRED (no pull-request context in a local run) and so NOT MEASURED, not a finding: node scripts/check-closing-target-claim.mjs :: exit 2 | node scripts/check-partof-closing-keyword.mjs :: exit 2 | node scripts/check-single-claim-paths.mjs :: exit 2. Of those, check-partof-closing-keyword was then measured with PR_BODY set to the current PR body with line 2 replaced by 'Clause-②: yes (widening)': exit 0. closing-target-claim and single-claim-paths need PR_NUMBER and a token, so they stay NOT MEASURED and are left to CI. check:error-code-provenance is one of these 54, and it exits 0.",
"changeset": "On the committed changeset (unchanged since e1e6c51), against origin/main, re-read at 57f738d: check-changeset-no-major --base origin/main exits 0. It reports no major, and the level axis is NOT APPLICABLE because there is no pull_request payload. With --event and a synthetic pull_request payload whose body line 2 is 'Clause-②: yes (widening)', it exits 0: 'LEVEL AXIS: this PR declares clause-② yes (widening), and it grades a package whose packages//src/ it moves at minor or above', naming service-settings minor and service-datasource minor; messaging and webhooks stay patch and are not refused. check-adr-0087-registration --base origin/main exits 0: '1 non-breaking changeset(s) seen'.",
"earlier_this_round": "At a3c2209, before the provenance fix, the same 105 ran with 4 non-zero. Two were PREREQUISITE NOT MET (exit 3), which are not findings: check:skill-examples (spec dist stale after the merge) and check:dual-build-cjs-loads (8 package dists missing in the fresh worktree). Both were built, and both are green above. Two were real findings in my new settings pin, both fixed in 57f738d: check:objectql-double-limit (the double copied rows before applying the bound) and check:test-source-alias (platform-objects/identity resolved to dist; an anchored alias was added). pnpm check:pm-dispatch-gates exited 0 there as well (battery 831.8s)."
},
"addendum_21935": "#21935 merged on main as 9e33ee7. I merged origin/main into the branch as a merge commit (a3c2209, clean; the two incoming commits touch no file of this branch) and did not copy any of its packages/qa edits. pnpm check:pm-dispatch-gates: exit 0 at a3c2209 and exit 0 at 57f738d.",
"main_moved_after": "origin/main moved on to 412d7dd (#21939, #21937, #21943). None of it touches a file of this branch, so it was not merged; the order merges only when main touches these files. A throwaway test merge (git merge-tree, then a detached worktree, removed afterwards) is clean, and on the merged tree check-system-context-census and check-tenant-audit-census both exit 0. That matters because #21939 edits the isSystem census page.",
"ci_red_followup": "The coordinator reported CI red on e1e6c51 at check:error-code-provenance: 2 'assign' stamp sites of VALIDATION_FAILED, one in each actor-reference.ts. Fixed in 57f738d by reusing validationFailure (@objectstack/types). Its stamp site is held by the recorded types-to-runtime waiver, so no new stamp site exists. No packages/spec edit, no row, no waiver. pnpm --filter @objectstack/spec check:error-code-provenance: exit 0, 'every registered-code stamp site is listed under its own owner key or carries a recorded waiver (10 waiver(s), all live)'.",
"mcp_calls": "0",
"open_questions": [],
"out_of_scope_findings": [
"carrier: #21908 closure (the seat) · noted, not filed. Producers in this slice's packages that the card does not name still reach the engine with no context (static read; no run exercised them). service-settings: the plugin-built sys_secret store (insert/get/update) and SettingsService.readStoredHandle. service-messaging: Sql*Outbox enqueue/ack/list, the email and SMS channels' recipient reads, RecipientResolver.resolveRole/resolveTeam/resolveOwnerOf, the emit dedup lookup, and the template renderer read. resolveOwnerOf is not neutral to move. Dedupe words: principal-less producer, unnamed producer, messaging outbox ack enqueue, settings secret store, owner_of recipient",
"carrier: the maintainer's ruling on rows 15/16 · noted, not filed. More request-door producers act on the caller's own rows like rows 15 and 16: the inbox unread count, and mark-read / mark-all-read. Dedupe words: inbox mark read, unread count, read receipt, caller own rows"
],
"deviations": [
"Item 1 is extended past the three named writes to sys_setting.user_id on upsertRow's insert. It is the same row-23 difference, and this PR's own opt-in on that insert introduced it. It is in the changeset and the acceptance notes.",
"The refusal is shape-identical to the engine's but is not instanceof objectql's ValidationError (see route). Callers on these paths read the message or code, and the doors duck-type it.",
"Existing doubles were adjusted minimally to answer sys_user lookups, because they do not model sys_user and the new check reads it: messaging-service.test.ts (one test), inbox-channel.test.ts (a knownUsers helper, two tests) and settings-system-context.pin.test.ts (now expects 1 sys_user probe, still isSystem on every call).",
"service-settings vitest.config.ts gained one anchored alias (platform-objects/identity to src), which check:test-source-alias asks for, for the new pin.",
"My e1e6c51 push was red in CI at check:error-code-provenance. That gate sits in dispatch-gates' artifact-roster block, outside the 105 derived commands, and I had not run that block. This round all 54 roster commands ran at the final head (see gates.roster).",
"origin/main 412d7dd is not merged (no overlap; test merge and both census gates clean).",
"The PR body is not edited and no label is written. The seat patches line 2 and the Acceptance notes. The body's Tests and row-23 paragraphs still describe 62960ff; the seat may refresh them from this report."
],
"files_changed_this_round": [
"packages/services/service-messaging/src/actor-reference.ts (new)",
"packages/services/service-messaging/src/actor-reference.pin.test.ts (new)",
"packages/services/service-messaging/src/messaging-service.ts",
"packages/services/service-messaging/src/inbox-channel.ts",
"packages/services/service-messaging/src/messaging-service.test.ts",
"packages/services/service-messaging/src/inbox-channel.test.ts",
"packages/services/service-settings/src/actor-reference.ts (new)",
"packages/services/service-settings/src/settings-user-reference.pin.test.ts (new)",
"packages/services/service-settings/src/settings-service-plugin.ts",
"packages/services/service-settings/src/settings-service.ts",
"packages/services/service-settings/src/settings-system-context.pin.test.ts",
"packages/services/service-settings/vitest.config.ts",
".changeset/21913-principal-less-producers.md"
],
"commits_this_round": [
"e1e6c51816 (pushed earlier)",
"a3c2209a68 merge origin/main (#21935)",
"57f738dfb1 provenance + pin fixes"
],
"api_writes": "2 this round: git push of 57f738d (not REST, not relayed) and this os-dev-report comment on #21913 via scripts/pm/post-stamped.mjs through the fleet-write relay. No PR body edit and no label.",
"private_producers": "1 candidate, held off GitHub (class: a refusal reason that names a row the caller may not be allowed to read; NOT MEASURED; unchanged by this PR). The detail is in the final report to the seat only."
}
Fixes #21913 closed this card completed. In this act the seat clears the pm:dispatched state label and the assignee os-steve. The domain:services, area:access, security and priority:p1 labels stay.
Activity
objectstack-fleet commentedon Oct 5, 2026
Claim: PM loop round 3 · 2026-10-05T21:55Z
Session:
session_011K3zqE8Pv1Evw5hc8tZCnNAccount:
os-steve(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-21913-principal-less-producers-servicesWorktree:
objectstack-issue-21913Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)File surface (at
origin/maincab63967), per the seat's verdict on #21908 (6003795556):packages/services/service-settings(rows 7 and 8),packages/services/service-messaging(rows 13 and 14, ⛔ not 15),packages/services/service-datasource(row 11) andpackages/plugins/plugin-webhooks(row 12). Their unit tests, theisSystemcensus page, and apatchchangeset per package.⛔ No edit in
plugin-security,packages/specorpackages/qa. ⛔ Rows 15 and 16 wait for the maintainer's ruling. ⛔ The deny is security(spec, plugin-security): the AI tool contract says a context with no caller runs "RLS-on, sees-nothing", but plugin-security hands a principal-less context straight through, and on a hosted kernel it read and wrote more than a member may #21908's.Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default tier).Clause-②: yes (widening)
SettingsEngine(find,insert) andSecretStoreEngineLike(delete) gain an optionalcontexton their options parameter, a widening of the published type surface (minorfor@objectstack/service-settingsand@objectstack/service-datasource). Nothing accepted or refused changes: the dangling actor reference the opt-in would stop refusing is restored at the producer in the same patch round. Each producer takes the explicit system opt-in that exists today; a producer on which a gate before the hand-off fires is reported, not moved.Thread-read: 6003795556
Serial constraints cleared: at 2026-10-05T21:55Z:
domain:cliseat's PR fix(runtime,service-datasource): an import over a code-defined datasource is held to its package's ADR-0028 namespace #21906 ([finding] A code-defined datasource is registered without its package's provenance, so the external import never applies the ADR-0028 namespace rule to it — an import names an unprefixed object and is accepted #21889) editsservice-datasource/src/plugin.ts(getNamespace), andhotlong's PR fix(spec)!: credential-shaped datasource config values are refused at write and redacted on every read door for drivers with no shipped contract #21877 (security(datasource): credential-shaped values in some datasource configurations are stored and served without the platform's secret handling — detail withheld pending maintainer #21840) edits the datasource redaction and credential-migration files. This card keeps to its named functions, edits neither PR's region, and test-merges both heads before opening its PR. A conflict stops it.Selection: the lane, read fresh at this pick, holds security(metadata-protocol, objectql, core): platform store reads and writes reach the engine with no principal and no system opt-in — the engine-lane producers of #21908's closure #21911, security(plugin-auth, runtime): raw-engine reads and writes outside the adapter's system context reach the engine principal-less, two of them behind a fail-open catch — the identity and runtime producers of #21908's closure #21912 and security(service-settings, service-messaging, service-datasource, plugin-webhooks): plumbing reads and writes reach the engine with no principal and no system opt-in — the services-lane producers of #21908's closure #21913 (p1, this seat's slices of security(spec, plugin-security): the AI tool contract says a context with no caller runs "RLS-on, sees-nothing", but plugin-security hands a principal-less context straight through, and on a hosted kernel it read and wrote more than a member may #21908) and security(automation): a paused flow run's stored state keeps record-change values that #21830's mask removes elsewhere — detail withheld pending maintainer #21867 (p2
security, withheld; by the maintainer's routing it is dispatched by the session holding its detail). This card and security(metadata-protocol, objectql, core): platform store reads and writes reach the engine with no principal and no system opt-in — the engine-lane producers of #21908's closure #21911 take two of the three slots. security(plugin-auth, runtime): raw-engine reads and writes outside the adapter's system context reach the engine principal-less, two of them behind a fail-open catch — the identity and runtime producers of #21908's closure #21912 waits on PR fix(plugin-auth)!: implicit account linking requires the standard local-ownership condition; unlink is honoured #21872 (auth-manager.ts).Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
os-dev-report
{
"issue": 21913,
"status": "done",
"branch": "claude/issue-21913-principal-less-producers-services",
"pr": "#21940",
"session": "session_011K3zqE8Pv1Evw5hc8tZCnN — subagent of the PM session (harness Claude-Session line)",
"premise_still_valid": true,
"summary": "Every engine call in the card's named functions (rows 7, 8, 11, 12, 13 and 14) now passes the explicit system opt-in { isSystem: true }. The rows cover SettingsService.loadRows and upsertRow, the sys_setting_audit writer, the runtime-datasource sys_metadata helpers, the sys_secret binder, the two sys_webhook reads, the dispatcher claim path on both outboxes, and the emit fan-out. No elevation API is new and no door's authorization changes. A local, uncommitted instrument at the plugin-security hand-off measured the move over the whole dogfood suite and a booted showcase dev composition, before and after. For the moved functions, principal-less records went from 2168 (dogfood) and 239 (boot) to 0. The gates before the hand-off threw 0 times, and call counts and outcome shapes are identical. One isSystem-keyed engine branch, the dangling-reference check (census row 23), no longer runs on three actor_id lookups. It resolved on all 10 measured calls, so no measured behaviour changed. A local probe shows the latent difference: today an actor_id naming no user is refused, and under the opt-in it is written. That is open_questions[0]. Draft PR #21940 is open at 62960ff, assigned to os-steve, and all 105 derived gates pass.",
"tests": "Unit suites at 10e77fe (merged with main faf8dce; the later merge to 62960ff touches none of these packages): service-settings 614 passed, service-messaging 510, service-datasource 743, plugin-webhooks 165, all exit 0. Typecheck of the four packages exit 0. Pins: one per package, 9 tests. Ablation: 7 legs through scripts/ablation-replace.mjs with anchor hits verified on disk. Each went red under the dropped opt-in, for example 'find on sys_setting: expected undefined to deeply equal { isSystem: true }' and 'update on sys_notification_delivery: expected { isSystem: false } ...'. Each was restored with blob == HEAD and an empty git diff HEAD, and went green again. The pins import src relatively, so no dist is in the path. Instrument runs, all under the lock: dogfood 206 files (205 passed + 1 skipped, 1590 tests passed, 9 skipped) identical before and after. Boot: identical HTTP outcomes (settings PUT x2 200, datasource POST 201, PATCH 200, DELETE 204, anonymous 401). The instrument was reverted with security-plugin.ts blob 5b4ab28045af == HEAD, and the plugin-security dist was rebuilt with ablation-dist-preflight --absent '__p21913Run' PASS on both readings (3 hits in the instrumented dist). ESLint narrowed to the 19 changed TS files: 0 errors, 0 warnings. They are inside the config's packages/**/.ts population, and the config enables no type-aware linting, so no untouched file's verdict can move. Full pnpm lint is CI's.",
"mcp_calls": "0",
"api_writes": "3 — POST /repos/objectstack-ai/objectstack/pulls (pr_create via relay run 37404795666, body read back byte-identical); POST /repos//issues/21940/assignees (label-write via relay run 37404850430, read back matches); POST /repos//issues/21913/comments (this report, scripts/pm/post-stamped.mjs). git push is not REST.",
"producers_moved": [
"row 7 service-settings SettingsService.loadRows: find sys_setting",
"row 8 service-settings SettingsService.upsertRow: existence-probe find + insert sys_setting (its update already carried isSystem); buildSettingAuditWriter write: insert sys_setting_audit",
"row 11 service-datasource loadDatasourceRows (find), loadDatasourceRow (findOne), persistDatasourceRow (findOne + insert/update), deleteDatasourceRow (findOne + delete/update) on sys_metadata; secret binder bind (insert), unbind (delete), resolve (find) on sys_secret",
"row 12 plugin-webhooks AutoEnqueuer.doRefresh (find sys_webhook); createWebhookRedeliverGuard (findOne sys_webhook)",
"row 13 service-messaging SqlNotificationOutbox.claim / claimDigest / reapExpired and SqlHttpOutbox.claim / reapExpired: candidate find, claiming update, read-back find, reap update",
"row 14 service-messaging MessagingService.writeEvent (insert sys_notification); inbox send (insert sys_inbox_message; recipient-locale findOne sys_user via its local helper readRecipientLocale) and writeDeliveredReceipt (insert sys_notification_receipt); PreferenceResolver.loadRows (2 x find sys_notification_preference); RecipientResolver.resolveEmail (findOne sys_user)"
],
"producers_reported": [
"None held back by a gate: across 35245 dogfood + 422 boot principal-less records, the six gates before the hand-off threw 0 times. Only the engine-owned guard matched (bucket + write verb on engine-owned objects), and it returns at isUserContextWrite for a context with no userId.",
"Not a gate, reported: census row 23 (engine assertReferencesResolve skipped for isSystem) stops running on actor_id of sys_notification (writeEvent), sys_inbox_message (inbox send) and sys_setting_audit (audit write). Measured: 10 nested probes before, all resolved; 0 after. Latent: actor_id naming no user — no context = refused VALIDATION_FAILED, isSystem = written (local probe, deleted). See open_questions[0]."
],
"instrument_counts": {
"method": "principal-less records BEFORE / principal-less AFTER / isSystem AFTER, attributed by the first frame after the innermost engine verb frame",
"dogfood": {
"SettingsService.loadRows": "2090 / 0 / 2090",
"SettingsService.upsertRow (probe+insert)": "7 / 0 / 7",
"setting-audit write": "4 / 0 / 4",
"SqlNotificationOutbox.claim": "8 / 0 / 8",
"SqlNotificationOutbox.claimDigest": "8 / 0 / 8",
"SqlNotificationOutbox.reapExpired": "1 / 0 / 1",
"SqlHttpOutbox.claim": "8 / 0 / 8",
"SqlHttpOutbox.reapExpired": "1 / 0 / 1",
"MessagingService.writeEvent": "8 / 0 / 8",
"inbox send (row insert)": "8 / 0 / 8",
"writeDeliveredReceipt": "8 / 0 / 8",
"PreferenceResolver.loadRows": "16 / 0 / 16",
"RecipientResolver.resolveEmail": "1 / 0 / 1",
"totals principal-less": "35245 → 33077 (delta 2168 = sum above)"
},
"boot": {
"SettingsService.loadRows": "42 / 0 / 42",
"SettingsService.upsertRow (probe+insert)": "3 / 0 / 3",
"setting-audit write": "2 / 0 / 2",
"loadDatasourceRows": "1 / 0 / 1",
"persistDatasourceRow": "4 / 0 / 4",
"deleteDatasourceRow": "2 / 0 / 2",
"AutoEnqueuer.doRefresh": "3 / 0 / 3",
"SqlNotificationOutbox.claim": "56 / 0 / 56",
"SqlNotificationOutbox.claimDigest": "56 / 0 / 56",
"SqlNotificationOutbox.reapExpired": "7 / 0 / 7",
"SqlHttpOutbox.claim": "56 / 0 / 56",
"SqlHttpOutbox.reapExpired": "7 / 0 / 7",
"totals principal-less": "422 → 183 (delta 239 = sum above)"
},
"not_reached_by_any_run_pinned_instead": "loadDatasourceRow; secret binder (no composition here wires it); redeliver guard; inbox recipient-locale read (template path); claim-path update and read-back (no pending rows)",
"outcomes": "Per function, counts by object.op, result type, row count and key set are identical before and after, with 0 errors. Content hashes are identical for 11/14 (dogfood) and 7/9 (boot). The differences sit on volatile values: receipt 'at' timestamps, deep links or bodies carrying per-run ids or dates, and the probe's per-phase datasource file path. Nested plugin-audit rows (sys_audit_log, sys_activity) are unchanged in count."
},
"h_readings": {
"H1": "Positions hold at cab6396. Every engine call in each named function is moved. The inbox send's recipient-locale read sits in the local helper readRecipientLocale, which only send calls, and is counted as part of send.",
"H2": "Done as specified. The instrument was local and uncommitted, then reverted with blob == HEAD.",
"H3": "isSystem census OK, and --fix changed nothing: no elevation read site was added. The tenant-audit census did move: the write sites now thread a context, so it was regenerated with --write and the hand-written page figures were updated (provable no-context tenancy-enabled 9 → 2, unreadable 67 → 60, elevated 114 → 121).",
"H4": "Confirmed: suites green with no existing test edited. One existing inbox test pins the exact findOne query shape, so the read opt-in rides the trailing options argument, which the contract prefers. Pins and ablations are listed above.",
"H5": "#21906 has merged into main (faf8dce), and merging main into this branch was clean. git merge-tree against #21877's head 5c40584 (closed draft) is clean at 62960ff. Neither PR's region was edited.",
"H6": "loadRows: same call count (2090 + 42), and the returned settings hash-identical on every call. The cost is one frozen context object, and the middleware exits at the system short-circuit instead of running the gates and the hand-off. No wall time is quoted (shared box).",
"H7": "Results are consumed inside the fan-out. emit() returns the id, counts and per-delivery outcomes, whose one read-derived value is the recipient id resolved from an address the emitter named. Its three in-repo callers (approvals, notify node, comment mentions) relay counts and the id only. The principal-less read returned the same rows."
},
"private_producers": "1 candidate held off-thread. Class: an authenticated request door (any role); a refusal text that carries a configuration row's field. NOT MEASURED whether the caller may read that row.",
"open_questions": [
{
"question": "Census row 23: moving writeEvent, the inbox send and the setting-audit write to isSystem stops the engine's dangling-reference check on their actor_id. Today an actor_id naming no user, which a flow notify node's actorId can author, is refused with VALIDATION_FAILED. Under the opt-in it is stored. Measured traffic is unchanged (10/10 references resolved). Accept the latent difference, or restore the refusal?",
"options": [
"A: accept it as landed. Every write reached the deny closure with the explicit opt-in, and the latent difference is recorded.",
"B: restore the refusal at the producer: writeEvent checks a supplied actorId against sys_user under the opt-in and refuses an unknown one with the same reference_not_found answer. That is one extra read per emit with an actor.",
"C: carry the emitter's principal into emit(). That is a contract change to IMessagingService, and the deny baseline would then need grants on these engine-owned objects."
],
"recommendation": "B, as a follow-up on the closure, with this PR landing as is. Business need (measured): flow notify nodes author actorId, and today a wrong one fails loudly. Long term: contract-first, so the refusal belongs at the producer and not in a silent store. AI-error axis: A silently accepts a declared value the runtime cannot honour, which is the failure the frame ranks worst. Startup: B is one guarded read, with no new API and no grant model. C widens a contract and drags in grant authoring."
}
],
"out_of_scope_findings": [
"carrier: #21908 closure (the seat) · noted, not filed. Producers in this slice's packages that the card does not name still reach the engine with no context (static read; no run exercised them). service-settings: the plugin-built sys_secret store (insert/get/update) and SettingsService.readStoredHandle. service-messaging: SqlOutbox enqueue/ack/list, the email and SMS channels' recipient reads, RecipientResolver.resolveRole/resolveTeam/resolveOwnerOf, the emit dedup lookup, and the template renderer read. resolveOwnerOf is not neutral to move: today the sharing middleware answers a principal-less read of a private object with deny-all, so the opt-in would widen it. Dedupe words: principal-less producer, unnamed producer, messaging outbox ack enqueue, settings secret store, owner_of recipient",
"carrier: the maintainer's ruling on rows 15/16 · noted, not filed. More request-door producers act on the caller's own rows like rows 15 and 16: the inbox unread count, and mark-read / mark-all-read (unreadNotificationIds, upsertReadReceipt, notificationOrganization). Dedupe words: inbox mark read, unread count, read receipt, caller own rows"
],
"gates": [
{
"command": "git push (empty-branch probe, then 6 pushes)",
"exit": 0
},
{
"command": "dispatch-gates --commands --repo objectstack-ai/objectstack at 62960ff: 105 commands",
"exit": "0 x105"
},
{
"command": "dispatch-gates --ran ran.list --repo objectstack-ai/objectstack",
"exit": 0,
"verdict": "105 derived famil(ies) accounted for — 105 run, 0 NOT-MEASURED"
},
{
"command": "os-verify-lock: typecheck x4 packages (merged tree)",
"exit": 0
},
{
"command": "os-verify-lock: vitest service-settings / service-messaging / service-datasource / plugin-webhooks",
"exit": "0 x4"
},
{
"command": "os-verify-lock: ablate.sh (7 legs)",
"exit": "mutated 1 x7 (red), restored 0 x7"
},
{
"command": "os-verify-lock: dogfood chunks 00..06 before / after",
"exit": "0 x7 / 0 x7"
},
{
"command": "os-verify-lock: boot-probe before / after",
"exit": "0 / 0"
},
{
"command": "os-verify-lock: turbo build closure (merged tree) + 8 packages for check:dual-build-cjs-loads",
"exit": "0 / 0"
},
{
"command": "check-system-context-census + --fix",
"exit": "0 / 0 (no change)"
},
{
"command": "eslint --no-inline-config --format json (19 changed TS files)",
"exit": 0
}
],
"line_budget": "n/a",
"deviations": [
"The local instrument edited plugin-security src. That is a stop-list package, but H2 places the instrument at the hand-off. It was never committed, it was reverted with blob == HEAD, and the dist was rebuilt with the marker absent.",
"inbox send's recipient-locale read sits in a local helper (readRecipientLocale) that only send calls. It was moved as part of send, and is named in the PR.",
"The PR changes the type of two exported interfaces: SettingsEngine.find/insert and SecretStoreEngineLike.delete now declare an optional context. No symbol is new on any package entry.",
"The tenant-audit census page and docs/audits counts were regenerated, and the page's hand-written figures updated. The dispatch's file surface named the isSystem census page; this one moved because of the same change.",
"scripts/engine-double-contract.pinned.json gained 5 rows recording the new pinned doubles (--write). The gate asks for exactly this.",
"Measurement was left unfinished by a container restart (about 01:15Z). The reference probe in flight then (exit 137) was re-run under the lock and is not read from its partial log. Everything else had completed with logged exits before the restart.",
"Dogfood and boot instrument runs were measured on the pre-merge tree (cab6396 + this change). The main commits merged afterwards touch none of these packages' files except service-datasource plugin.ts (#21906, outside these functions)."
],
"files_changed": [
".changeset/21913-principal-less-producers.md",
"content/docs/permissions/tenant-audit-census.mdx",
"docs/audits/2026-08-tenant-audit-write-call-sites.counts.md",
"packages/plugins/plugin-webhooks/src/auto-enqueuer.ts",
"packages/plugins/plugin-webhooks/src/redeliver-guard.ts",
"packages/plugins/plugin-webhooks/src/webhook-system-context.pin.test.ts",
"packages/services/service-datasource/src/tests/datasource-system-context.pin.test.ts",
"packages/services/service-datasource/src/datasource-admin-plugin.ts",
"packages/services/service-datasource/src/datasource-secret-binder.ts",
"packages/services/service-messaging/src/fan-out-system-context.ts",
"packages/services/service-messaging/src/inbox-channel.ts",
"packages/services/service-messaging/src/messaging-service.ts",
"packages/services/service-messaging/src/outbox-dispatcher-scope.ts",
"packages/services/service-messaging/src/preference-resolver.ts",
"packages/services/service-messaging/src/recipient-resolver.ts",
"packages/services/service-messaging/src/sql-http-outbox.ts",
"packages/services/service-messaging/src/sql-outbox.ts",
"packages/services/service-messaging/src/system-context.pin.test.ts",
"packages/services/service-settings/src/settings-service-plugin.ts",
"packages/services/service-settings/src/settings-service.ts",
"packages/services/service-settings/src/settings-service.types.ts",
"packages/services/service-settings/src/settings-system-context.pin.test.ts",
"scripts/engine-double-contract.pinned.json"
],
"resume": "Found: remote head 458499d == local, no PR, no report, and only the untracked reference probe in the tree. The probe measures the census row 23 latent difference. Re-ran it under the lock (no context: refused VALIDATION_FAILED; isSystem: written; control written both ways), then deleted it. It was never committed. Then: merged main twice (faf8dce, 9dce635; clean); refreshed the build; re-ran typecheck and the four suites; ran the gate battery, which turned up four reds; fixed them (tenant-audit census regenerated, pin doubles through the dispatch asserts with bounds, ledger rows, extra packages built for the dual-build gate); re-ran the seven ablations on the final pins; ran all 105 gates and --ran at 62960ff; then opened PR #21940 and assigned it."
}
Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Review: PR #21940 at
62960ffa1a· seatdomain:services#1(#6021) ·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-06T02:42ZACCEPT the build, with one patch round (verdict on the dev report
6008199477). Checked against the PR, not the report:Patch round 1 (one round; decided by the four-axis frame, the axes agree):
Restore the dangling actor-reference refusal at the producer. Under
isSystemthe engine's reference check no longer runs onactor_idforsys_notification(writeEvent),sys_inbox_message(the inbox send) andsys_setting_audit(the audit write). Today anactor_idnaming no user is refused; under the opt-in it would be stored. That is option A of the dev's open question 0, and it is not accepted:Option C would change the
IMessagingServicecontract. B, in this PR: the producer keeps today's refusal (same code and status) for an unknownactor_id. It prefers an existing engine path that keeps the reference check under the opt-in, and otherwise uses one guarded read; no new API.Clause-②correction. The exportedSettingsEngine(find,insert) andSecretStoreEngineLike(delete) gain an optionalcontext, which widens the published type surface. So the line isClause-②: yes (widening),minorfor@objectstack/service-settingsand@objectstack/service-datasource. The claim is amended (6003840075). The PR's line 2 and the changesets move in this round.The at-tier contract review runs on the patch head (
Clause-②: yes).Carried:
service-settingssecret store andreadStoredHandle, the outboxes' enqueue, ack and list, the email and SMS recipient reads,RecipientResolver.resolveRole/resolveTeam/resolveOwnerOf, the emit dedup lookup, and the template renderer read.resolveOwnerOfis not neutral to move, because the opt-in would widen a read the sharing middleware answers deny-all today. It is recorded on security(spec, plugin-security): the AI tool contract says a context with no caller runs "RLS-on, sees-nothing", but plugin-security hands a principal-less context straight through, and on a hosted kernel it read and wrote more than a member may #21908.Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
os-dev-report
{
"issue": 21913,
"status": "done",
"branch": "claude/issue-21913-principal-less-producers-services",
"pr": "#21940",
"session": "session_011K3zqE8Pv1Evw5hc8tZCnN",
"premise_still_valid": true,
"round": "patch round 1 (seat verdict 6008259054), plus the coordinator's CI-red follow-up (error-code-provenance) and the #21935 addendum",
"summary": "Each producer that writes a user reference under the opt-in now refuses a dangling one itself, with the engine's own answer. This covers writeEvent (sys_notification.actor_id), the inbox send (sys_inbox_message.actor_id), the setting-audit writer (sys_setting_audit.actor_id), and also upsertRow's insert (sys_setting.user_id). The check is one guarded sys_user read under the opt-in (route b), and the refusal is built by the shared validationFailure constructor from @objectstack/types. That clears the provenance red CI found on e1e6c51 without a spec row, a waiver or a new export. The changeset is now minor for service-settings and service-datasource, carrying 'Clause-②: yes (widening)'. #21935 is on main and merged in (a3c2209). The head is 57f738d.",
"route": "Route (b). Route (a) does not exist. ObjectQL.assertReferencesResolve returns before any probe when context.isSystem is set, and nothing can keep it on. engine.validate() runs no reference check (census row 23). So each producer does one guarded existence read of sys_user by id under the explicit opt-in, before its insert. It refuses only when that read ran and found nothing. A read that cannot run lets the write through, as the engine's fail-open does. The refusal is the engine's own answer: VALIDATION_FAILED with one reference_not_found finding. The message comes from the same catalog entry (renderValidationMessage), with the field's registered label. It is built by validationFailure from @objectstack/types. That package is already a runtime dependency of both packages, and its stamp site is held by the recorded types-to-runtime waiver in the ledger. So neither package stamps the code itself, and the provenance gate is green with no spec row and no new waiver. Why not objectql's ValidationError class: objectql is only a devDependency of both packages. Making the engine a runtime dependency of two services just to throw its class would change the dependency graph, and the order did not ask for that. The types constructor exists for exactly this shape. The helpers are module-internal: service-messaging src/actor-reference.ts (assertActorReferenceResolves) and service-settings src/actor-reference.ts (assertUserReferenceResolves, registeredLabel). Neither is on a package entry, and IMessagingService is unchanged. Call sites: writeEvent before the sys_notification insert, the inbox send before the sys_inbox_message insert, the setting-audit writer before the sys_setting_audit insert, and SettingsService.upsertRow's insert branch for sys_setting.user_id.",
"pins": [
"packages/services/service-messaging/src/actor-reference.pin.test.ts. 4 tests on a real ObjectQL with SqlDriver (better-sqlite3 :memory:). (1) emit to writeEvent with an unknown actor_id is compared with the engine's own refusal of the context-less sys_notification insert. Asserted: code VALIDATION_FAILED; status equal to the engine's (neither carries one, and the door maps the shape to 400); fields[0] {field actor_id, code reference_not_found, constraint.target sys_user}; the name/code/status/message/fields envelope deep-equal; sys_notification count 0. (2) a known actor and no actor are both written. (3) the inbox send with an unknown actor answers {ok:false, error:'inbox insert failed: ' + the engine's message}; sys_inbox_message count 0. (4) a known actor and no actor are both delivered.",
"packages/services/service-settings/src/settings-user-reference.pin.test.ts. 4 tests on a real ObjectQL over an in-file memory driver (its find applies the caller's bound after the filter). (1) set(..., {userId: unknown}) to upsertRow insert is compared with the engine's refusal of the context-less sys_setting insert: the same code/status/fields[0]/envelope assertions; sys_setting count 0. (2) a known user is written. (3) the setting-audit writer with an unknown actor warns 'SettingsServicePlugin: setting-audit write failed: ' + the engine's message; sys_setting_audit count 0. (4) a known actor and no actor are both written, with no warning."
],
"ablation": "4 legs through scripts/ablation-replace.mjs, under the lock, at 57f738d (a first pass at e1e6c51 read the same). Each leg drops one check: the call is deleted, or the checked value is replaced with null. The anchor must hit 1 to 0 on disk, and an independent probe-line count reads 1 before and 0 under the mutation. Then the leg runs its pin and restores. writeEvent (messaging-service.ts, delete the call): red, 1 failed | 3 passed, 'the emit must refuse: expected null not to be null'. inbox send (inbox-channel.ts, delete the call): red, 1 failed | 3 passed, 'expected { ok: true, ... } to deeply equal { ok: false, ... }'. setting-audit (settings-service-plugin.ts, entry.actorId to null): red, 1 failed | 3 passed, 'expected [] to deeply equal [ Array(1) ]'. upsertRow (settings-service.ts, row.user_id to null): red, 1 failed | 3 passed, 'the write must refuse: expected null not to be null'. Every leg restored to blob == HEAD (d978e84d929d, a463ba83828b, d468299688fc, 745d1930ec9f), with git diff HEAD empty and the probe count back to 1. The restored re-run is green, 4 passed (4). The tree is clean after all legs. The pins import the mutated module from src relatively, so no dist is in the path and no rebuild or dist preflight applies.",
"changeset_path": ".changeset/21913-principal-less-producers.md",
"changeset_text": "---\n"@objectstack/service-settings": minor\n"@objectstack/service-messaging": patch\n"@objectstack/service-datasource": minor\n"@objectstack/plugin-webhooks": patch\n---\n\nPlatform plumbing in these four packages now passes the explicit system opt-in (
{ isSystem: true }) on its data-engine calls. Until now it reached the engine with no principal and no opt-in, and the security middleware let that through only because of its principal-less hand-off.\n\nClause-②: yes (widening)\n\n- Whyyes (widening): two exported option types gain an optionalcontextthat an adapter must forward as-is. They areSettingsEngine.find/.insert(@objectstack/service-settings) andSecretStoreEngineLike.delete(@objectstack/service-datasource), so both packages take aminor. An implementation written against the old types still type-checks, and nothing accepted or refused at any door changes.\n- service-settings:SettingsServicereads and writes its ownsys_settingrows under the opt-in:loadRows, plus the existence probe and insert inupsertRow(the update already used it). Thesys_setting_auditwriter does too.\n- service-datasource: thesys_metadatahelpers behind runtime datasources use the opt-in. They cover boot restore, cluster convergence, and persist and delete behind the admin doors. So do thesys_secretbinder'sbind,unbindandresolve.\n- plugin-webhooks: the auto-enqueuer's subscription refresh and the redeliver guard's subscription lookup use the opt-in.\n- service-messaging: two paths use the opt-in. One is the dispatcher's claim path:claim,claimDigestand the visibility-timeout reap on both outboxes. The other is the emit fan-out: thesys_notificationrow, the recipient's address and locale reads, the preference reads, the inbox row and the delivered receipt.\n- A user reference that names no user is still refused. The engine skips its dangling-reference check for anisSystemwrite, so each producer that writes a user reference checks it first. The checked references are theactor_idofsys_notification,sys_inbox_messageandsys_setting_audit, and theuser_idof a user-scopesys_settingrow. An unknown id is refused with the engine's own answer:VALIDATION_FAILED, onereference_not_foundfinding, and the same message. A write that names no user is unchanged.\n- What each call reads and writes is otherwise unchanged. None of the gates the middleware runs before its hand-off applies to these objects.\n- ⛔ No new export on any package entry, and no new elevation API.\n","pr_line_2": "Clause-②: yes (widening)",
"acceptance_notes_block": "## Acceptance notes\n\n- Producers in these packages that the card does not name. A static read finds that they still reach the engine with no context. No run exercised them, so the measured table never listed them. Without a route, #21908's deny breaks each one, so they are listed for the seat's closure rather than moved here:\n - service-settings: the
sys_secretstore the plugin builds (insert/get/update), andSettingsService.readStoredHandle.\n - service-messaging:SqlNotificationOutboxandSqlHttpOutboxenqueue,ackandlist; the email and SMS channels' recipient reads;RecipientResolver.resolveRole/resolveTeam/resolveOwnerOf; the emit dedup lookup; the template renderer's read.\n -resolveOwnerOfreads a business object, and its posture is not neutral. Today the sharing middleware answers a principal-less read of aprivateobject with a deny-all filter, so anowner_of:recipient on such an object resolves to nobody. Under the opt-in, that filter would be bypassed.\n- Request-door producers that act on the caller's own rows, like rows 15 and 16 (report-only, for the maintainer's ruling): the inbox unread count, and mark-read / mark-all-read (unreadNotificationIds,upsertReadReceipt,notificationOrganization).\n- Census row 23: the dangling user-reference refusal is kept at the producer. The engine skips its dangling-reference check for anisSystemwrite and has no option to keep it, so each producer that writes a user reference now checks it first. It does onesys_userread by id under the opt-in, then refuses an unknown id with the engine's own answer:VALIDATION_FAILED, onereference_not_foundfinding, and the same message. The checked references are:\n - theactor_idofsys_notification(writeEvent), ofsys_inbox_message(the inbox send) and ofsys_setting_audit(the setting-audit writer);\n - theuser_idof a user-scopesys_settingrow, on the insert inSettingsService.upsertRow. The seat's verdict named the first three. This fourth is the same difference, which this PR introduced when it put the opt-in on that insert.\n- A write that names no user is unchanged. If the read cannot run, the write goes through, which matches the engine's fail-open. There is one extrasys_userread for each write that names a user.\n- Differential pins over a real engine hold each producer's answer equal to what the engine itself answers a context-less insert. Each check was also ablated: with the check dropped, its pin goes red; after the restore, the blob equals HEAD and the pin is green again.\n- The refusal has the same shape but is a different class. It is built byvalidationFailurefrom@objectstack/types, already a runtime dependency of both packages, so neither package stamps the code itself. Its name,code, message andfieldsare the engine's. Butinstanceofobjectql'sValidationErroris false: objectql is only a dev dependency here. The callers on these paths read the message or the code: the flow notify node, the inboxSendResult, and the audit writer's warn line. Every door maps the shape to400 VALIDATION_FAILEDby duck-typing.\n- One posture question was noted on a request-door read and is held off-thread. It was not measured.\n","tests": "At 57f738d, all under scripts/pm/os-verify-lock.sh: four packages built (exit 0). Typecheck of service-settings, service-messaging, service-datasource and plugin-webhooks exits 0. Both new pins are inside the tsc program (tsc --listFiles finds 1 each; neither tsconfig excludes tests). Unit suites: service-settings 618 passed, service-messaging 514, service-datasource 743, plugin-webhooks 165, all exit 0. Pins: 4 + 4 pass. Ablation: see
ablation. ESLint, narrowed: the 26 changed TS files, through the ESLint API with allowInlineConfig false, which is the --no-inline-config of pnpm lint. Evidence (1): the population is read from eslint's own config. isPathIgnored is false for all 26, and calculateConfigForFile gives each 4 to 6 active rules. Evidence (2): lintFiles returned 26 results, with 0 errors and 0 warnings. Evidence (3): the config enables no type-aware linting (no parserOptions.project, as its own header records). The only files it reads, scripts/slot-lookup-baseline.json and scripts/query-options-erasure-baseline.json, are not in this diff, and neither is eslint.config.mjs. So no verdict on an untouched file can move. The full pnpm lint run belongs to CI.","gates": {
"derived": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 57f738d: 105 commands (the list matches the one at a3c2209). Each ran at 57f738d. 105 exit 0.",
"ran": "✓ dispatch-gates --ran: 105 derived famil(ies) accounted for — 105 run, 0 NOT-MEASURED (a DERIVED zero — all 105 recorded an exit code and none of them is 3).",
"extra": [
"pnpm --filter @objectstack/spec check:error-code-provenance :: exit 0",
"pnpm check:pm-dispatch-gates :: exit 0"
],
"roster": "The 54 artifact-roster families that --commands prints outside its total, each run at 57f738d: 51 exit 0. 3 exited 2, which is NOT WIRED (no pull-request context in a local run) and so NOT MEASURED, not a finding: node scripts/check-closing-target-claim.mjs :: exit 2 | node scripts/check-partof-closing-keyword.mjs :: exit 2 | node scripts/check-single-claim-paths.mjs :: exit 2. Of those, check-partof-closing-keyword was then measured with PR_BODY set to the current PR body with line 2 replaced by 'Clause-②: yes (widening)': exit 0. closing-target-claim and single-claim-paths need PR_NUMBER and a token, so they stay NOT MEASURED and are left to CI. check:error-code-provenance is one of these 54, and it exits 0.",
"changeset": "On the committed changeset (unchanged since e1e6c51), against origin/main, re-read at 57f738d: check-changeset-no-major --base origin/main exits 0. It reports no major, and the level axis is NOT APPLICABLE because there is no pull_request payload. With --event and a synthetic pull_request payload whose body line 2 is 'Clause-②: yes (widening)', it exits 0: 'LEVEL AXIS: this PR declares clause-② yes (widening), and it grades a package whose packages//src/ it moves at minor or above', naming service-settings minor and service-datasource minor; messaging and webhooks stay patch and are not refused. check-adr-0087-registration --base origin/main exits 0: '1 non-breaking changeset(s) seen'.",
"earlier_this_round": "At a3c2209, before the provenance fix, the same 105 ran with 4 non-zero. Two were PREREQUISITE NOT MET (exit 3), which are not findings: check:skill-examples (spec dist stale after the merge) and check:dual-build-cjs-loads (8 package dists missing in the fresh worktree). Both were built, and both are green above. Two were real findings in my new settings pin, both fixed in 57f738d: check:objectql-double-limit (the double copied rows before applying the bound) and check:test-source-alias (platform-objects/identity resolved to dist; an anchored alias was added). pnpm check:pm-dispatch-gates exited 0 there as well (battery 831.8s)."
},
"addendum_21935": "#21935 merged on main as 9e33ee7. I merged origin/main into the branch as a merge commit (a3c2209, clean; the two incoming commits touch no file of this branch) and did not copy any of its packages/qa edits. pnpm check:pm-dispatch-gates: exit 0 at a3c2209 and exit 0 at 57f738d.",
"main_moved_after": "origin/main moved on to 412d7dd (#21939, #21937, #21943). None of it touches a file of this branch, so it was not merged; the order merges only when main touches these files. A throwaway test merge (git merge-tree, then a detached worktree, removed afterwards) is clean, and on the merged tree check-system-context-census and check-tenant-audit-census both exit 0. That matters because #21939 edits the isSystem census page.",
"ci_red_followup": "The coordinator reported CI red on e1e6c51 at check:error-code-provenance: 2 'assign' stamp sites of VALIDATION_FAILED, one in each actor-reference.ts. Fixed in 57f738d by reusing validationFailure (@objectstack/types). Its stamp site is held by the recorded types-to-runtime waiver, so no new stamp site exists. No packages/spec edit, no row, no waiver. pnpm --filter @objectstack/spec check:error-code-provenance: exit 0, 'every registered-code stamp site is listed under its own owner key or carries a recorded waiver (10 waiver(s), all live)'.",
"mcp_calls": "0",
"open_questions": [],
"out_of_scope_findings": [
"carrier: #21908 closure (the seat) · noted, not filed. Producers in this slice's packages that the card does not name still reach the engine with no context (static read; no run exercised them). service-settings: the plugin-built sys_secret store (insert/get/update) and SettingsService.readStoredHandle. service-messaging: Sql*Outbox enqueue/ack/list, the email and SMS channels' recipient reads, RecipientResolver.resolveRole/resolveTeam/resolveOwnerOf, the emit dedup lookup, and the template renderer read. resolveOwnerOf is not neutral to move. Dedupe words: principal-less producer, unnamed producer, messaging outbox ack enqueue, settings secret store, owner_of recipient",
"carrier: the maintainer's ruling on rows 15/16 · noted, not filed. More request-door producers act on the caller's own rows like rows 15 and 16: the inbox unread count, and mark-read / mark-all-read. Dedupe words: inbox mark read, unread count, read receipt, caller own rows"
],
"deviations": [
"Item 1 is extended past the three named writes to sys_setting.user_id on upsertRow's insert. It is the same row-23 difference, and this PR's own opt-in on that insert introduced it. It is in the changeset and the acceptance notes.",
"The refusal is shape-identical to the engine's but is not instanceof objectql's ValidationError (see route). Callers on these paths read the message or code, and the doors duck-type it.",
"Existing doubles were adjusted minimally to answer sys_user lookups, because they do not model sys_user and the new check reads it: messaging-service.test.ts (one test), inbox-channel.test.ts (a knownUsers helper, two tests) and settings-system-context.pin.test.ts (now expects 1 sys_user probe, still isSystem on every call).",
"service-settings vitest.config.ts gained one anchored alias (platform-objects/identity to src), which check:test-source-alias asks for, for the new pin.",
"My e1e6c51 push was red in CI at check:error-code-provenance. That gate sits in dispatch-gates' artifact-roster block, outside the 105 derived commands, and I had not run that block. This round all 54 roster commands ran at the final head (see gates.roster).",
"origin/main 412d7dd is not merged (no overlap; test merge and both census gates clean).",
"The PR body is not edited and no label is written. The seat patches line 2 and the Acceptance notes. The body's Tests and row-23 paragraphs still describe 62960ff; the seat may refresh them from this report."
],
"files_changed_this_round": [
"packages/services/service-messaging/src/actor-reference.ts (new)",
"packages/services/service-messaging/src/actor-reference.pin.test.ts (new)",
"packages/services/service-messaging/src/messaging-service.ts",
"packages/services/service-messaging/src/inbox-channel.ts",
"packages/services/service-messaging/src/messaging-service.test.ts",
"packages/services/service-messaging/src/inbox-channel.test.ts",
"packages/services/service-settings/src/actor-reference.ts (new)",
"packages/services/service-settings/src/settings-user-reference.pin.test.ts (new)",
"packages/services/service-settings/src/settings-service-plugin.ts",
"packages/services/service-settings/src/settings-service.ts",
"packages/services/service-settings/src/settings-system-context.pin.test.ts",
"packages/services/service-settings/vitest.config.ts",
".changeset/21913-principal-less-producers.md"
],
"commits_this_round": [
"e1e6c51816 (pushed earlier)",
"a3c2209a68 merge origin/main (#21935)",
"57f738dfb1 provenance + pin fixes"
],
"api_writes": "2 this round: git push of 57f738d (not REST, not relayed) and this os-dev-report comment on #21913 via scripts/pm/post-stamped.mjs through the fleet-write relay. No PR body edit and no label.",
"private_producers": "1 candidate, held off GitHub (class: a refusal reason that names a row the caller may not be allowed to read; NOT MEASURED; unchanged by this PR). The detail is in the final report to the seat only."
}
Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Landed · seat
domain:services#1·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-06T05:37Z. ⛔ Classes, positions and functions only.76fec88b, which is an ancestor oforigin/main(verified withgit merge-base --is-ancestorafter a fetch). Merged at 2026-10-06T05:36Z. The at-tier contract review PASSED on the landed head57f738dfb1(6009643000).Fixes #21913closed this cardcompleted. In this act the seat clears thepm:dispatchedstate label and the assigneeos-steve. Thedomain:services,area:access,securityandpriority:p1labels stay.service-settings,service-messaging,service-datasourceandplugin-webhooks(rows 7, 8 and 11 to 14 of security(spec, plugin-security): the AI tool contract says a context with no caller runs "RLS-on, sees-nothing", but plugin-security hands a principal-less context straight through, and on a hosted kernel it read and wrote more than a member may #21908's census) carries the explicit system opt-in. A user reference that names no user is still refused at the producer, with the engine's own answer. The changeset carriesClause-②: yes (widening):minorforservice-settingsandservice-datasource, whose exported engine types gain an optionalcontext;patchfor the other two.resolveOwnerOfis not neutral). Also the inbox unread and mark-read request-door producers, which join the maintainer's open ruling on rows 15 and 16.datasource-admin-plugin.ts). They stay serial with each other.Generated by Claude Code
fix(services): settings, datasource, webhook and messaging plumbing p…
fix(service-messaging, service-storage, service-settings, metadata-pr…