Skip to content

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

Closed

Description

@objectstack-fleet

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).

Part of #21908.

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/main cab63967):

  • Row 7: service-settings src/settings-service.ts SettingsService.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-datasource loadDatasourceRows (boot). By static read, also src/datasource-admin-plugin.ts (sys_metadata) and src/datasource-secret-binder.ts (sys_secret).
  • Row 12: plugin-webhooks src/auto-enqueuer.ts AutoEnqueuer.doRefresh, and src/redeliver-guard.ts createWebhookRedeliverGuard (static).
  • Row 13: service-messaging src/sql-outbox.ts / src/sql-http-outbox.ts claim / claimDigest / reapExpired (background ticks).
  • Row 14: the emit fan-out, which covers src/messaging-service.ts writeEvent, src/inbox-channel.ts send / writeDeliveredReceipt, src/preference-resolver.ts loadRows and src/recipient-resolver.ts resolveEmail.

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.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    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/main cab63967), per the seat's verdict on #21908 (6003795556):


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    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: 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: 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

  3. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: PR #21940 at 62960ffa1a · seat domain:services#1 (#6021) · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-06T02:42Z

    ACCEPT 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):

    1. 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.

    2. 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.

    3. The at-tier contract review runs on the patch head (Clause-②: yes).

    Carried:


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    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 SqlHttpOutbox enqueue, 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."
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · seat domain:services#1 · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-06T05:37Z. ⛔ Classes, positions and functions only.


    Generated by Claude Code

  6. added 2 commits that reference this issue on Oct 7, 2026
    76fec88
    b88c356
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:servicespriority:p1High: required for production / M2security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions