Skip to content

feat(spec,services): deployment-level state has no organization column — settings global rung, plumbing objects, the audit ledger, #12699 made total (ADR-0131 D7) #15207

Description

@hotlong

⛔ BLOCKED — the v18 development line is not open.

Blocked-by: #15193

History: this line read Blocked-by: #15193 until 2026-09-27, inside backticks, so no unlock scan could read it (the triage census counted it as a blocked card with no machine target). The same targets now sit one per line, undecorated; nothing else changed (triage seat, session_01W89enF2dYV7K4N2Fbfj33f).

Part of #15194 (ADR-0131 execution tree). ⛔ Do not claim, assign or dispatch this card while #15193 is open, whatever its other labels say.


In one sentence. Tables that belong to no organization — the job queue, delivery records, migration journals, the audit ledger, the notification inbox — lose their organization column; deployment-level runtime settings leave sys_setting; and cloud's "this deployment declares this object platform-global" switch becomes simply "on this deployment the object has no organization column".

Scope. (1) systemFields.tenant: false on the objects no writer attributes to an organization: sys_job, sys_job_run, sys_job_queue, sys_flow_dispatch, sys_migration, sys_migration_journal, sys_secret (scoped through its owning setting), sys_presence — each confirmed by a writer census with a firing control, not by the name looking infrastructural. ⛔ NOT sys_http_delivery (#13565 stamps it from the webhook's organization; redeliver() walls by tenant) and ⛔ NOT sys_email (#11741 / #11303 decision 2 stamp it at the producers) — both are tenant data. The sys_inbox_message / sys_notification* / sys_user_preference family is decided by its writer facts (recipient-anchored in cloud's reading) and recorded in the C7 inventory. ADR-0087 entry per removed column. (2) sys_audit_log: no injected organization column; the organization a row is about becomes a plain attribution field under a name the tenant-field resolver does not claim (not organization_id); RLS readers of the audit page filter on it explicitly. (3) sys_setting scope: 'global' rows leave the tenant-scoped object per §6 Q3 — configuration, or a tenant-less sys_platform_setting; settings-service.ts's user → tenant → deployment cascade reads the new source. (4) #12699: platformGlobalObjects becomes an input to resolveInjectedSystemColumns on the declaring deployment — no column injected, so Layer 0 and the driver have nothing to scope; the stand-down semantics retire.

Absorbs: #13433 (sys_activity.environment_id declared live with no writer) belongs to this census — read it and give that column a verdict in the same pass.

Acceptance. DDL for each listed object carries no organization_id; the settings cascade resolves deployment values from the new source (pinned); an audit row about a deployment-level action is written without refusal and is visible to platform admins; on a deployment declaring an object platform-global the table has no column (cloud pins this in C10).

Refs: ADR-0131 D7 · ADR-0007 · ADR-0057 · #12699 · #13565 · #11741 · #13636 (sys_audit_log specimen) · #13433 · #13564 read-side ledger U-A.

Activity

objectstack-fleet commented on Oct 6, 2026

@objectstack-fleet
Contributor

v18 pre-opening re-verification (C6): mostly HOLDS. One listed object is wrong; four small refinements

Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-06T14:38Z. ⛔ Not a claim, ⛔ not a dispatch. Read-only re-verification on objectstack main 6befe19c6e. The cut base was 2026-09-04, 3,298 commits earlier. The maintainer asked for this pre-opening preparation in the triage seat's chat: 「现在就可以处理吧」. Classes, positions and functions only. The claiming seat applies these corrections before building. The body is not rewritten.

Holds:

Corrections:

Day one: dispatchable once #15193 closes, with the corrections above.


Generated by Claude Code

objectstack-fleet commented on Oct 7, 2026

@objectstack-fleet
Contributor

Triage: unlocked, pm:blocked → pm:queue. The v18 line is open: #15193 was closed on the maintainer's word. C6, dispatchable first

Blocked-by: none

Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-07T12:36Z. ⛔ Not a claim, ⛔ not a dispatch.

The blocker is released:

At claim (as #15193 requires, because these premises were measured in 2026-09): the file surface is re-verified against the then-current main. At this write, every repository path the body names in backticks exists on main (879bd38c5b).

  • Order: C10 (cloud#1979) waits on this card, per its Blocked-by: line.

The release state:

  • main is not yet in Changesets pre mode; the opening card follows this unlock.
  • A breaking change landing before the opening is graded minor with its BREAKING banner and ADR-0087 disposition, under the launch-window convention. Once pre mode is in, major is open.
  • ⛔ chore: version packages #21988 is not merged.

10 remaining items

objectstack-fleet commented on Oct 8, 2026

@objectstack-fleet
Contributor

Claim: PM loop round 1 (stage: scope item (3), the settings global rung, of this card's three open items; item (1) landed as PR #22107, released 6045790111) · 2026-10-08T00:26Z
Session: session_01LAi5BVvQNiYzepSAcsoFLK
Account: os-litant (the seat's linked user as GET /user answers it; the card's assignee from this act)
Branch: claude/issue-15207-platform-setting-global-rung
Worktree: objectstack-issue-15207
Domain: domain:spec (the card also carries domain:services; this seat holds it whole, as seat 2 did)
Seat: domain:spec#1 (seat post #6017)
File surface (at origin/main 51290bca2c; stop on breach and explain in the report). Scope item (3) alone: sys_setting's scope: 'global' rung leaves the tenant-scoped object (ADR-0131 D7, §6 Q3).

objectstack-fleet commented on Oct 8, 2026

@objectstack-fleet
Contributor

os-dev-report
{
"issue": 15207,
"status": "done",
"branch": "claude/issue-15207-platform-setting-global-rung",
"pr": "#22166",
"session": "session_01LAi5BVvQNiYzepSAcsoFLK (subagent = the PM's id; resumed once after a container restart, same claim 6049595369, same branch and worktree)",
"premise_still_valid": true,
"summary": "Draft PR #22166 (Part of #15207, item (3) only) adds the tenant-less sys_platform_setting and moves the settings cascade's global rung onto it. The object has systemFields.tenant false, requiredPermissions manage_platform_settings, one row per (namespace, key) with unique 'global', sys_setting's value/encryption columns, and no scope/user_id/organization_id; settingsObjects registers it beside sys_setting. SettingsService writes a global-scope key there and reads the rung from there alone. Every sys_setting read now names its rungs ($or over tenant/user, or user_id/tenant), so a pre-v18 scope=global row is not a second source: no fallback read, no boot move. getMany reads the global rung once per call. The rank table, the lock check and source 'global' are unchanged. Also in the PR: (a) the CLI secret-reference union reads both settings holders, and an unreadable sys_platform_setting gaps the family (H3 closed; without it the orphan sweep deletes global-rung credentials); (b) a global-scope config_change audit row names sys_platform_setting via the new export CONFIG_CHANGE_GLOBAL_OBJECT_NAME; (c) sys_setting.scope retires 'global' (H6), while sys_setting_audit.scope keeps it; (d) one ADR-0087 D3 entry, sys-setting-global-rung-moved, plus a step-18 rationale fragment; (e) a minor + BREAKING changeset; (f) the tenancy census, the tenant-audit census and the erasure baseline are regenerated. No stored row moves (C7, #15211).",
"readings": {
"h1": "HOLDS. The built manifests have 109 global-rung keys in 7 namespaces: auth 29, ai 35, storage 11, mail 10, sms 10, knowledge 10, lifecycle 4. lifecycle.retention_overrides is the one tenant key. All 7 read and write under manage_platform_settings, i.e. live-edited in Setup. In-tree consumers re-read on change: plugin-auth and organizations (getNamespace plus subscribe), plugin-email, service-sms and service-storage (subscribe), lifecycle (on every sweep). ai and knowledge have no in-tree value reader beyond their runAction test handlers, which resolve live. No key is boot-read only, so nothing moves to configuration and no open_question is raised.",
"stop_condition_org_signal": "Did not fire. SettingsService.setMany is the only writer of a settings row. It writes under isSystem with no tenantId and a row that names no organization. sys_setting is unclassified, so resolveSystemInsertOrganization derives nothing. A pin writes a global key under ctx tenantId org_1, and the stored row has no organization_id. Two organization signals do sit on the global write path, but neither is row attribution: the CryptoContext tenantId (no in-tree provider binds it; decrypt passes none), and the config_change audit row's tenant_id (item 2's object).",
"h2": "Census of every non-test source naming sys_setting; the procedure fires on the union and on orphans.ts. Reads the global rung: cli/src/utils/secret-reference-union.ts (all rows, fixed here) and cli/src/commands/secret/orphans.ts lines 300-309 (the legacy-inline guard; OUTSIDE the surface, not edited). Do not read it: core/src/security/resolve-authz-context.ts:1696 (pins scope 'tenant'); metadata-protocol sys-setting-identity-index.ts (index maintenance only, no value read); mcp/src/plugin.ts and plugin-hono-server/src/current-user-endpoints.ts (comments only, they go through resolveLocalizationContext); cli sys-secret-orphan-sweep.ts and service-settings sys-secret-orphan-report.ts (pure, caller-supplied rows; the report has no in-tree caller).",
"h3": "TRUE, and closed in this PR. A handle held only in sys_platform_setting.value_enc is attributable and unreferenced by sys_setting, which is the deletable shape. Ablation B (the union reads sys_setting alone) turns three pins red: the union names the handle, an unreadable holder gaps the family, and the sweep decides referenced rather than deletable.",
"h4": "The LocalCryptoProvider v2 AAD is the 0xFF lead byte, a version label, then lp(scope) || lp(namespace) || lp(key), with scope 'settings'. No object name, no organization, no tenant. C7 copies value_enc verbatim and does not re-encrypt; the sys_secret row stays. A pin seals with tenantId org_1 in the context, holds the handle in a sys_platform_setting row, and resolves it. Recorded in the PR body.",
"h5": "Scratch harness, not committed: the real SecurityPlugin middleware, the registry-processed object and the shipped sets, posture none/single/isolated. organization_admin gets 403 PERMISSION_DENIED; admin_full_access is admitted with no filter; member gets 403; a system context is admitted. Control, the same object without requiredPermissions: organization_admin is admitted with NO filter on every posture. The settings door is unchanged: settings-admission-tenancy-posture and config-change-audit drive the plugin's own routes over a real ObjectQL with a global manifest, get 200, and the row lands in sys_platform_setting.",
"h6": "Half holds. No writer writes scope 'global' into sys_setting, so its option is retired with the ADR-0087 entry. sys_setting_audit.scope is still written with 'global' (buildSettingAuditWriter records entry.scope), so the mirror stays. The parity pin is re-premised: sys_setting.scope = SpecifierScopeSchema minus global; the audit object = SpecifierScopeSchema. SpecifierScopeSchema and source 'global' stay.",
"zone3_falsified": "The suggested pin 'a tenant and a user value still override it' contradicts the unchanged rank table. scopeRank puts global at rank 1 and the first non-null rung wins, so global OUTRANKS tenant and user, before and after this PR. The pin asserts the unchanged order instead: global from the new store outranks both, and tenant/user answer once it is empty. The 403 pin was measured by the scratch harness rather than committed, because a committed one would live in plugin-security, which is outside the surface (item (1)'s precedent).",
"c7_move": "For each sys_setting row at scope global: write sys_platform_setting with the same namespace/key, copying value, value_enc (verbatim), encrypted, locked, locked_reason and updated_by; remove the source. A pre-#8629 database can hold duplicate global rows for one (namespace, key); the new unique refuses the second, so the ceremony must pick one."
},
"tests": "Read at the merged head d047787. service-settings: full vitest, 37 files / 638 passed, including the new settings-global-rung.test.ts (8 cases, real ObjectQL). platform-objects: full vitest, 65 files / 1036 passed; the pre-fix run had 4 red in objects-es-es-echo-decisions.test.ts for the retired leaf, fixed. spec: vitest over src/system, src/migrations and api-methods-batch-conformance, 55 files / 1980 passed. cli: --project integration over the touched files (union, sweep, rewrap, src/commands/secret), 7 files / 91 passed. Read at the pre-merge head 1137107: cli --project unit, 263 files / 3874 passed; typecheck exit 0 for service-settings, platform-objects, spec and cli (the latter three include check:test-typecheck; service-settings' tsconfig includes src, tests included). Ablations, one-off, via scripts/ablation-replace.mjs in WRAP mode under the lock: each anchor 1 to 0 with a changed blob, each restore read blob == HEAD with an empty git diff HEAD, and git status read 0 lines afterwards. The subjects are imported by relative source path, so no dist is in the path. A (null-user read readmits global): 1 red. A2 (user read readmits global): 1 red. D (global rung read from sys_setting): 7 of 8 red. B (union reads sys_setting only): 3 red.",
"gates": "At d047787, dispatch-gates --commands --repo objectstack-ai/objectstack derived 125 families. All 125 ran, each exit code captured before any pipe, all 125 exit 0. --ran reconciles 125 derived / 125 run / 0 NOT-MEASURED / 0 UNRUN. Added vs the claim-time list: check-empty-changeset, check:engine-double-contract, check:objectql-double-limit, check:where-matcher, check:i18n-coverage, check:i18n-walk-parity, check:type-check-coverage, check:type-check-debt, check:yaml-examples, check:objectui-changeset, check:pm-changeset-deadline-census, release-rehearsal-clone and release-pending-publish self-tests. Red earlier and fixed: the census drift (two censuses), the objectql-double-limit double in the new test, and the query-options-erasure ratchet down (2 to 1). Earlier PREREQUISITE NOT MET on dual-build-cjs-loads and i18n-coverage was cured by building the missing packages; both are green. Lint, a proven narrowing at d047787: eslint --no-inline-config --format json over the 37 changed ts files gives 37 results, 0 errors, 0 warnings (none ignored); eslint.config.mjs enables no type-aware linting, so untouched files' verdicts cannot move; full pnpm lint is CI's. CI on #22166 at report time: 14 completed, 18 in_progress, 0 failed (in_progress, not waited on).",
"line_budget": "vs merge base 8fc50b7 (origin/main at the first merge; the second merge, ef1fcb2, made no change on this branch's side): 42 files, +1338 / -246 = 1584 changed lines, generated files included (translations, registry, censuses).",
"files_changed": [
".changeset/15207-settings-global-rung-platform-setting.md",
"content/docs/permissions/tenant-audit-census.mdx",
"docs/audits/2026-08-tenant-audit-write-call-sites.counts.md",
"packages/cli/src/commands/secret/rewrap.guards.test.ts",
"packages/cli/src/utils/secret-reference-union.test.ts",
"packages/cli/src/utils/secret-reference-union.ts",
"packages/cli/src/utils/sys-secret-orphan-sweep.test.ts",
"packages/cli/src/utils/sys-secret-rewrap.test.ts",
"packages/platform-objects/src/apps/translations/{en,es-ES,ja-JP,zh-CN}.objects.generated.ts, es-ES.source-hashes.generated.ts, objects-es-es-echo-decisions.test.ts",
"packages/platform-objects/src/system/{index.ts, sys-platform-setting.object.ts, sys-platform-setting.object.test.ts, sys-setting.object.ts, sys-setting.organization-unique.test.ts, sys-setting.scope-options.test.ts}",
"packages/services/service-settings/src/{settings-service.ts, config-change-audit.ts, index.ts, manifest.ts}",
"packages/services/service-settings/src/ tests: config-change-audit, manifest, settings-admission-tenancy-posture, settings-crypto-fail-closed, settings-engine-bind-window, settings-getmany, settings-global-rung (new), settings-loadrows-scope, settings-prebind-read-warning, settings-secret-rotation, settings-system-context.pin, settings-user-reference.pin, sys-secret-orphan-report",
"packages/spec/src/migrations/entries/semantic/18.sys-setting-global-rung-moved.ts, packages/spec/src/migrations/registry.ts (regenerated + one rationale fragment)",
"packages/spec/src/system/constants/platform-object-names.ts",
"scripts/platform-object-tenancy-census.json, scripts/query-options-erasure-baseline.json"
],
"deviations": [
"Files outside the claim's surface, all needed and named here and in the PR body. (1) service-settings config-change-audit.ts, index.ts and manifest.ts, in the same package: manifest.ts registers the object; config-change-audit names the store a global change landed in. (2) The CLI test fixtures rewrap.guards.test.ts and sys-secret-rewrap.test.ts: the union now asks for the second holder. (3) platform-objects translation bundles plus the es-ES echo ledger test: generated fallout of the retired option. (4) content/docs/permissions/tenant-audit-census.mdx (3 hand-written counts the gate binds) and docs/audits/...counts.md: regenerated census. (5) scripts/query-options-erasure-baseline.json: a ratchet down.",
"cli/src/commands/secret/orphans.ts (domain:cli) is NOT edited. It reads the global rung for the sweep's legacy-inline guard; the PM declares it, and it is noted for C7.",
"Changeset arm is Clause-② yes (narrowing), not the claim's yes (widening ...): measured, the diff both widens (new object and exports) and narrows (option retired, rung storage moved). The PR body carries the claim's line verbatim at line start, then the measured arm.",
"Zone 3 pin 'tenant and user override the global value' replaced by the unchanged-rank pin (falsified; see readings.zone3_falsified). The 403 pin was measured by a scratch harness, not committed.",
"A stray file /build1.pid (one line, a PID) was written at the filesystem root by a mis-scoped shell variable before the container restart. Removal was refused by the safety check; it needs a person's rm.",
"The commit trailer pair is the AGENTS.md model-free form (Co-authored-by: Claude), not the harness reminder's model-named one; the PR footer is the AGENTS.md session-URL form."
],
"mcp_calls": "0",
"api_writes": "3 relay writes, each POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create, i.e. POST /repos/objectstack-ai/objectstack/pulls (draft) giving #22166, body read back byte-identical; (2) label-write --assign os-litant, i.e. the issues/22166 assignees write, read back as matching; (3) this os-dev-report comment, POST /repos//issues/15207/comments via post-stamped. Plus git pushes, which are not REST writes.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: C7 #15211 · cli/src/commands/secret/orphans.ts lines 300-309 build the sweep's legacy-inline guard from sys_setting rows only; once C7 moves pre-Phase-3 global rows, inline ciphertext can sit in sys_platform_setting. Withhold-only guard; the union (which decides deletion) already reads both · noted, not filed · in PR Acceptance notes",
"carrier: C7 #15211 · metadata-protocol/src/migrations/sys-setting-identity-index.ts: two degraded-arm operator texts still say global-scope settings rows 'can still be created' in sys_setting; no writer creates one after this PR · noted, not filed · in PR Acceptance notes",
"carrier: 承接者:无 · settings-manifest.zod.ts module TSDoc names sys_setting as the store (and already omitted the global rung); no describe() names it, so it is untouched per the claim · noted, not filed · in PR Acceptance notes",
"carrier: 承接者:无 · SettingsService passes ctx.tenantId into CryptoContext on encrypt for every rung, while materialiseRow decrypts with none; no in-tree provider reads tenantId (zero pull) · noted, not filed · in PR Acceptance notes",
"carrier: 承接者:无 · reading only, NOT measured, and not in the PR body (the PR body is written once): the tenant/user rows SettingsService writes carry SETTINGS_SYSTEM_CONTEXT with no tenantId, and sys_setting is unclassified, so by reading the engine stamps no organization on a tenant row. A user-scoped key resolved with no userId reads every user's rows. Both pre-date this PR · dedupe words: sys_setting tenant row organization, settings system context tenantId, user-scope key without userId"
]
}

objectstack-fleet commented on Oct 8, 2026

@objectstack-fleet
Contributor

Claim revision (file surface), and ✅ ACCEPT on the diff: PR #22166 at d0477879af. Landing held for the at-tier contract review and CI

domain:spec seat 1 (#6017) · os-litant · session session_01LAi5BVvQNiYzepSAcsoFLK · 2026-10-08T03:44Z · holder of claim 6049595369; the review of record for the report 6051681390.

Claim revision. It amends 6049595369 with the files the dev measured as needed, each named in the PR body:

  • service-settings: config-change-audit.ts, index.ts, manifest.ts (same package).
  • domain:cli:
    • packages/cli/src/utils/secret-reference-union.ts and its test, the H3 hazard closed;
    • the fixtures rewrap.guards.test.ts, sys-secret-rewrap.test.ts and sys-secret-orphan-sweep.test.ts.
  • domain:engine: platform-objects' generated translation bundles and the es-ES echo-decisions test.
  • Generated / gate-bound: content/docs/permissions/tenant-audit-census.mdx (three counts the census gate binds), docs/audits/2026-08-tenant-audit-write-call-sites.counts.md, scripts/platform-object-tenancy-census.json, and scripts/query-options-erasure-baseline.json (a ratchet down, 2 → 1).
  • packages/cli/src/commands/secret/orphans.ts is read, not edited (see the out-of-scope list).

Checklist (read on GitHub and in the PR's own diff):

  • Form: draft, base main, assignee os-litant. Line 1 is Part of #15207, and no closing word sits beside any card number in the body. Items (2) and (4) stay open. Line 2 is Clause-②: yes, verbatim from the claim. The changeset says yes (narrowing): the dev measured both arms, and both readings are yes.
  • Size and surface: 42 files, +1338 / −246 (1,584 lines including generated). check-governed-merges: not governed.
  • The rulings, checked in settings-service.ts:
    • A global-scope write lands in sys_platform_setting.
    • The rung is read from that object alone (loadGlobalRows).
    • The sys_setting reads name their rungs in the query ($or over tenant / user), so a pre-v18 scope = 'global' row is never a second source. ⛔ There is no fallback read and no boot move (D14).
    • The rank table, the lock check and source: 'global' are unchanged.
    • One ADR-0087 D3 entry is registered: sys-setting-global-rung-moved.
  • The object: sys_platform_setting has systemFields: { tenant: false } and requiredPermissions: ['manage_platform_settings'] (D7: object permission, not the wall), one row per (namespace, key), and no scope / user_id. sys_setting.scope retires global (H6). sys_setting_audit.scope keeps it, because a global change is still audited there.
  • Readings accepted:
    • H1: 109 global keys in 7 namespaces, all live-edited, so none moves to configuration.
    • The stop condition: did not fire. No global write carries an organization.
    • H3 is true, and closed in this PR: the union reads both holders, and an unreadable holder gaps the family. Ablation B turns 3 pins red.
    • H4: the AAD binds scope, namespace and key only.
    • H5: organization_admin gets 403 and the platform admin is admitted (a scratch harness, recorded in the body). The control without the capability admits organization_admin with no filter.
  • ⚠️ The seat corrects its own dispatch. Zone 3 suggested pinning "a tenant and a user value still override it". That contradicts the unchanged rank table: scopeRank puts global at rank 1 and the first non-null rung wins, so the global value outranks tenant and user, before and after this PR. The dev's pin asserts that unchanged order. The dispatch was wrong; the PR is right.

Changeset, read sentence by sentence against the diff:

  • The headline and the "deployment-wide value" paragraph match.
  • The sys_platform_setting bullet (columns, tenant: false, requiredPermissions, writes only through the door) matches the object file.
  • The SettingsService bullet ("reads the cascade's global rung from there alone", "sys_setting reads exclude scope = 'global'", order and lock unchanged, CONFIG_CHANGE_GLOBAL_OBJECT_NAME) matches the service diff.
  • The sys_setting.scope bullet matches the option change and the audit mirror.
  • The os secret bullet matches the union diff.
  • Each of "What moves for consumers" four bullets states what the code does. The "no re-encryption" sentence matches H4.
  • The grade is minor + BREAKING + the ADR-0087 marker: .changeset/pre.json is absent on main.

Out of scope (report):

Landing, held until:

  1. A ## Contract review PASS at CONTRACT_REVIEW_TIER is on record for the landing head. The path limb (packages/spec/src/**) and the claim's Clause-②: yes both fire; it comes from an isolated at-tier reviewer.
  2. Every check is green on that head (17 of 32 were in_progress at this stamp).
  3. ⚠️ Release coupling: C6(3) and C7 (feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211) ship in the same release. Until C7's ceremony exists, an existing database's global settings answer from the next rung or the default. This is stated in the changeset and on feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211, and goes to the maintainer in the round report.

objectstack-fleet commented on Oct 8, 2026

@objectstack-fleet
Contributor

os-dev-report
{
"issue": 15207,
"status": "done",
"branch": "claude/issue-15207-platform-setting-global-rung",
"pr": "#22166",
"session": "session_01LAi5BVvQNiYzepSAcsoFLK (subagent = the PM's id); landing round on claim 6049595369, revised 6051733303",
"premise_still_valid": true,
"summary": "A PURE regeneration hop. The new head is 92f3370, fast-forward pushed from d047787 (d047787..92f3370). Its single merge commit has parents d047787 and 7ef50a4 (origin/main), and it was made by scripts/pm/os-regen-merge.sh, which reported no conflict, kept the branch's bytes of scripts/platform-object-tenancy-census.json (main did not touch it), and committed the merge before any regeneration. On the committed merge, the full regeneration chain (gen:migration-registry, gen:spec-changes, gen:upgrade-guide, then check:generated against a freshly built spec dist) wrote ZERO bytes: git status clean, so there is no separate regeneration commit. The merged registry already equals the generator's output: 396 semantic entries, including main's manifest-permissions-string-list-retired and flow-script-subflow-config-undeclared-keys-refused, beside this PR's sys-setting-global-rung-moved. No file was hand-edited, and the PR body is untouched. The PR's net diff against main is still 42 files, +1338 / -246.",
"regeneration": {
"tool": "bash scripts/pm/os-regen-merge.sh: step 1 git merge origin/main (registry.ts auto-merged, no conflict); step 2 no os-regen path to take from main beyond the merge result (the branch-only census kept); step 3 merge committed first; step 4 run by hand as below",
"generators_run_on_92f33709c8": "pnpm --filter @objectstack/spec gen:migration-registry (wrote registry.ts, 396 semantic / 247 retired-key / 218 retired-def, byte-identical), gen:spec-changes, gen:upgrade-guide: git status --porcelain empty afterwards",
"regeneration_commit": "none: the generators changed nothing, so there was nothing to commit"
},
"rationale_order": "Reported, not moved. main's flow-script-subflow-config-undeclared-keys-refused (from #22129) also takes order 87, the same number as this PR's sys-setting-global-rung-moved. That is a TIE, not a split: the list documents that two retirements in flight may take the same number and then render in id order, so main's fragment renders first and this PR's renders immediately after it, as the last two fragments of step 18. The list stays sorted by id (99 fragments). Ties already exist at 56, 60, 62, 66, 67, 74, 77, 85 and 86. The seat decides whether to renumber; this round leaves it as is.",
"tests": "At HEAD 92f3370: pnpm --filter @objectstack/spec exec vitest run --project repo --maxWorkers=2 scripts/step18-rationale-merge.test.ts scripts/conversions-major18-merge.test.ts gives 2 files / 21 tests passed (lock VERDICT command-exit 0). The spec dist was rebuilt on this head (pnpm --filter @objectstack/spec build, lock VERDICT command-exit 0) before check:generated.",
"gates": "All at HEAD 92f3370, each exit captured before any pipe: pnpm --filter @objectstack/spec check:generated exit 0 ('All 15 generated artifacts are up to date', against the dist built on this head; declaration stamp match); pnpm --filter @objectstack/spec check:migration-registry exit 0 ('src/migrations/registry.ts is current (396 semantic, 247 retired-key, 218 retired-def)'); node scripts/check-platform-object-tenancy-census.mjs exit 0 (84 objects, 50 in reach, 34 outside); its --self-test exit 0; node scripts/check-adr-0087-registration.mjs --base origin/main exit 0 (1 declared-breaking changeset, registered sys-setting-global-rung-moved); node scripts/check-tenant-audit-census.mjs exit 0 (233 sites, 23 prose figures held). CI on the new head is not waited on.",
"purity": {
"command": "comm -23 <(git diff --name-only d047787 92f3370 | sort) <(git diff --name-only ef1fcb2 7ef50a4 | sort) | grep -v -x -F -e packages/spec/src/migrations/registry.ts -e packages/spec/spec-changes.json -e docs/protocol-upgrade-guide.md -e scripts/platform-object-tenancy-census.json",
"output": "(empty)",
"counts": "files differing d047787 vs 92f3370: 60; files main changed ef1fcb2..7ef50a4: 60; the set difference BEFORE subtracting the regenerated outputs is already 0 lines; after subtracting: 0 lines. ef1fcb2 is an ancestor of 7ef50a4 (merge-base --is-ancestor exit 0).",
"branch_side": "Of the 42 files in the PR's net diff (git diff 7ef50a4 92f3370), 41 have the same blob at d047787 and 92f3370. The one that differs is packages/spec/src/migrations/registry.ts, which now also carries main's two new entries. The net diff is +1338 / -246, unchanged from d047787."
},
"mcp_calls": "0",
"api_writes": "1 relay write: this os-dev-report comment, POST /repos/objectstack-ai/objectstack/dispatches (fleet-write.yml as objectstack-fleet[bot]) executing POST /repos//issues/15207/comments via post-stamped. Plus 1 git push (fast-forward d047787..92f3370), which is not a REST write. The PR body and labels are untouched.",
"deviations": [
"No regeneration commit exists: the generators wrote nothing on the committed merge, so the hop is one merge commit only.",
"The worktree was recreated with git worktree add on the existing branch, then pnpm install (no tracked file changed)."
],
"files_changed": [],
"line_budget": "unchanged: vs origin/main 7ef50a4, 42 files, +1338 / -246",
"open_questions": [],
"out_of_scope_findings": []
}

objectstack-fleet commented on Oct 8, 2026

@objectstack-fleet
Contributor

os-dev-report
{
"issue": 15207,
"status": "done",
"branch": "claude/issue-15207-platform-setting-global-rung",
"pr": "#22166",
"session": "session_01LAi5BVvQNiYzepSAcsoFLK (subagent = the PM's id); landing round 2 on claim 6049595369, revised 6051733303",
"premise_still_valid": true,
"summary": "Regeneration hop with ONE hand edit, reported below. The new head is c3f5925, fast-forward pushed from 92f3370 (92f3370..c3f5925); the merged main tip is 6ed0c0f. It is two commits. 61edbce is the merge of origin/main 6ed0c0f made by scripts/pm/os-regen-merge.sh: it stopped on the two census files, which were brought to a committable state by taking main's side of each conflict hunk, and the script rerun skipped step 1, redid step 2 and left nothing to commit. c3f5925 is the regeneration: node scripts/tenant-audit-census.mjs --write on the merged tree, plus the one prose figure the gate still refused. The four translation bundles main also touched text-merged clean, and the PR's delta in each is byte-identical to before. The other regenerators (platform-object-tenancy-census --write, gen:migration-registry, gen:spec-changes, gen:upgrade-guide) wrote nothing. The PR's net diff against main is still 42 files, +1338 / -246.",
"hand_edit": {
"file": "content/docs/permissions/tenant-audit-census.mdx",
"line": 125,
"before": "forwarding shim cannot, and 54 of the 232 sites are spelled that way. A",
"after": "forwarding shim cannot, and 52 of the 232 sites are spelled that way. A",
"why": "The line is hand-written prose OUTSIDE the generated region, so --write does not touch it. It conflicted: this branch had '52 of the 233', main had '54 of the 232'. After taking main's side and running --write, check-tenant-audit-census refused exactly this figure: '[prose-count] the page states 54 for "sites whose options argument is unreadable (limits section)"; the census says 52'. The edit sets it to the census's value. 232 is main's total, and 52 reflects this PR's two settings-service sites that moved from unreadable to readable, the same -2 the PR carried at 92f3370. No other prose figure was refused: '60 sites' / '52 were simply unread' at lines 153-154 text-merged clean from this branch, and the gate holds all 23 prose figures."
},
"merge_resolution": "Both census files were brought to a committable state by taking main's side of each conflict hunk, as the os-regen-merge refusal prescribes for generated content, then rewritten by the census tool. The counts file is rendered whole by renderCountsFile, so the side taken does not survive --write. In the page, the three conflict hunks were two generated-region rows plus the line-125 prose above. Hand-written lines outside those hunks were text-merged by git, both sides' intents kept. The staged regeneration diff was inspected with git diff --cached before committing.",
"tests": "At HEAD c3f5925, after building the service-settings closure (lock VERDICT command-exit 0): pnpm --filter @objectstack/service-settings exec vitest run, exit 0, 38 files / 642 passed (main added one file); pnpm --filter @objectstack/platform-objects exec vitest run, exit 0, 65 files / 1036 passed. Each exit was captured per part.",
"gates": "All at HEAD c3f5925, each exit captured before any pipe: node scripts/check-tenant-audit-census.mjs exit 0 (232 write call sites certified, 23 prose figures held); node scripts/check-platform-object-tenancy-census.mjs exit 0 (84 objects, 50 in reach, 34 outside); pnpm --filter @objectstack/spec check:generated exit 0 ('All 15 generated artifacts are up to date', against a spec dist built on this head); pnpm --filter @objectstack/spec check:migration-registry exit 0 (396 semantic, 247 retired-key, 218 retired-def); node scripts/check-adr-0087-registration.mjs --base origin/main exit 0 (registered sys-setting-global-rung-moved). Extra, because main's commits touched the PR's four translation bundles: node scripts/check-i18n-bundles.mjs --filter=platform-objects exit 0 ('in sync (11 bundle(s))', with the CLI closure built on this head). CI on the new head is not waited on.",
"purity": {
"command": "comm -23 <(git diff --name-only 92f3370 c3f5925 | sort) <(git diff --name-only 7ef50a4 6ed0c0f | sort)",
"output": "(empty, 0 lines), BEFORE subtracting any regenerated output",
"counts": "92f33709c8 vs c3f5925: 74 files; main 7ef50a4..6ed0c0f: 74 files; 7ef50a4 is an ancestor of 6ed0c0f (exit 0); 92f3370 is an ancestor of c3f5925 (fast-forward).",
"branch_side": "Of the 42 files in the PR's net diff (git diff 6ed0c0f c3f5925), 36 have the same blob at 92f3370 and c3f5925. The 6 that differ are all files main also changed: (1) the four translation bundles, en / es-ES / ja-JP / zh-CN .objects.generated.ts, where the branch's own -U0 delta against main is byte-identical before and after (md5 of the delta lines equal per file); (2) the two census files, regenerated, plus the line-125 hand edit above. Net: +1338 / -246, unchanged."
},
"rationale_order": "Unchanged since round 1. main brought no new step-18 rationale fragment this round (396 semantic entries, as before). The order-87 tie with flow-script-subflow-config-undeclared-keys-refused stands as reported.",
"mcp_calls": "0",
"api_writes": "1 relay write: this os-dev-report comment, POST /repos/objectstack-ai/objectstack/dispatches (fleet-write.yml as objectstack-fleet[bot]) executing POST /repos//issues/15207/comments via post-stamped. Plus 1 git push (fast-forward 92f3370..c3f5925), which is not a REST write. The PR body and labels are untouched.",
"deviations": [
"One hand edit beyond the regenerators: tenant-audit-census.mdx line 125, the figure 54 to 52, to the census's value (see hand_edit). Reported for the seat's judgment on whether the PASS carries forward.",
"The merge needed a resolution commit (61edbce), as the os-regen-merge script prescribes when it stops on a not-driver-managed conflict; the regeneration is its own commit (c3f5925)."
],
"files_changed": [
"content/docs/permissions/tenant-audit-census.mdx (regenerated region + the line-125 figure)",
"docs/audits/2026-08-tenant-audit-write-call-sites.counts.md (regenerated)"
],
"line_budget": "unchanged: vs origin/main 6ed0c0f, 42 files, +1338 / -246",
"open_questions": [],
"out_of_scope_findings": []
}

objectstack-fleet commented on Oct 8, 2026

@objectstack-fleet
Contributor

os-dev-report
{
"issue": 15207,
"status": "done",
"branch": "claude/issue-15207-platform-setting-global-rung",
"pr": "#22166",
"session": "session_01LAi5BVvQNiYzepSAcsoFLK (subagent = the PM's id); landing round 3 on claim 6049595369, revised 6051733303",
"premise_still_valid": true,
"summary": "Census conflict sync, the same shape as round 2. The new head is 2d4170f, fast-forward pushed from c3f5925 (c3f5925..2d4170f); the merged main tip is 7d7943d. It is two commits. 0328884 is the merge of origin/main 7d7943d made by scripts/pm/os-regen-merge.sh: it stopped on the same two census files, which were brought to a committable state by taking main's side of each conflict hunk, and the script rerun skipped step 1, redid step 2 and left nothing to commit. 2d4170f is the regeneration: node scripts/tenant-audit-census.mjs --write on the merged tree, plus the one prose figure the gate still refused, restating the PR's -2 on main's new total. The other regenerators (platform-object-tenancy-census --write, gen:migration-registry, gen:spec-changes, gen:upgrade-guide) wrote nothing. git diff --shortstat 7d7943d 2d4170f: 42 files changed, 1338 insertions(+), 246 deletions(-), unchanged.",
"hand_edit": {
"file": "content/docs/permissions/tenant-audit-census.mdx",
"line": 125,
"before": "forwarding shim cannot, and 54 of the 234 sites are spelled that way. A",
"after": "forwarding shim cannot, and 52 of the 234 sites are spelled that way. A",
"why": "Hand-written prose OUTSIDE the generated region, which --write does not touch. It conflicted: this branch had '52 of the 232', main had '54 of the 234'. After taking main's side and running --write, check-tenant-audit-census refused exactly this figure: '[prose-count] the page states 54 for "sites whose options argument is unreadable (limits section)"; the census says 52'. The edit sets it to the census's value: main's new total 234, with the PR's -2 unreadable restated, the same shape as round 2. No other prose figure was refused; lines 153-154 ('60 sites' / '52 were simply unread') text-merged clean, and all 23 prose figures are held."
},
"tests": "At HEAD 2d4170f, after building the service-settings closure (lock VERDICT command-exit 0): pnpm --filter @objectstack/service-settings exec vitest run, exit 0, 38 files / 642 passed; pnpm --filter @objectstack/platform-objects exec vitest run, exit 0, 65 files / 1036 passed. Each exit was captured per part.",
"gates": "All at HEAD 2d4170f, each exit captured before any pipe: node scripts/check-tenant-audit-census.mjs exit 0 (234 write call sites certified, 23 prose figures held); node scripts/check-platform-object-tenancy-census.mjs exit 0 (84 objects, 50 in reach, 34 outside); pnpm --filter @objectstack/spec check:generated exit 0 ('All 15 generated artifacts are up to date', against the spec dist built on this head); pnpm --filter @objectstack/spec check:migration-registry exit 0 (396 semantic, 247 retired-key, 218 retired-def); node scripts/check-adr-0087-registration.mjs --base origin/main exit 0 (registered sys-setting-global-rung-moved). CI on the new head is not waited on.",
"purity": {
"command": "comm -23 <(git diff --name-only c3f5925 2d4170f | sort) <(git diff --name-only 6ed0c0f 7d7943d | sort)",
"output": "(empty, 0 lines), BEFORE subtracting any regenerated output",
"counts": "c3f59255c5 vs 2d4170f: 44 files; main 6ed0c0f..7d7943d: 44 files; 6ed0c0f is an ancestor of 7d7943d (exit 0); c3f5925 is an ancestor of 2d4170f (fast-forward, exit 0).",
"shortstat": "git diff --shortstat 7d7943d 2d4170f: 42 files changed, 1338 insertions(+), 246 deletions(-)",
"branch_side": "Of the 42 files in the PR's net diff, 40 have the same blob at c3f5925 and 2d4170f. The 2 that differ are the census page and the counts file: regenerated, plus the line-125 hand edit above."
},
"rationale_order": "Unchanged. main brought no new step-18 entry (396 semantic entries); the order-87 tie stands as reported in round 1.",
"mcp_calls": "0",
"api_writes": "1 relay write: this os-dev-report comment, POST /repos/objectstack-ai/objectstack/dispatches (fleet-write.yml as objectstack-fleet[bot]) executing POST /repos//issues/15207/comments via post-stamped. Plus 1 git push (fast-forward c3f5925..2d4170f), which is not a REST write. The PR body and labels are untouched.",
"deviations": [
"One hand edit beyond the regenerators: tenant-audit-census.mdx line 125, the figure 54 to 52, to the census's value (see hand_edit), the same shape as round 2.",
"The merge needed a resolution commit (0328884), as the os-regen-merge script prescribes when it stops on a not-driver-managed conflict; the regeneration is its own commit (2d4170f)."
],
"files_changed": [
"content/docs/permissions/tenant-audit-census.mdx (regenerated region + the line-125 figure)",
"docs/audits/2026-08-tenant-audit-write-call-sites.counts.md (regenerated)"
],
"line_budget": "unchanged: vs origin/main 7d7943d, 42 files, +1338 / -246",
"open_questions": [],
"out_of_scope_findings": []
}

objectstack-fleet commented on Oct 8, 2026

@objectstack-fleet
Contributor

Landed: scope item (3), PR #22166 → c52bfb417b. The card stays open: pm:dispatched → pm:queue for items (2) and (4)

domain:spec seat 1 (#6017) · os-litant · session session_01LAi5BVvQNiYzepSAcsoFLK · 2026-10-08T07:55Z · holder of claim 6049595369 (revised 6051733303), which this act releases.

Release: session_01LAi5BVvQNiYzepSAcsoFLK · why: a partial landing (Part of #15207); item (3) is done, and items (2) and (4) are separate claims · to: pm:queue, unassigned, for items (2) and (4).

Items (2) and (4), for the next claim. These are unchanged from the item (1) landing record 6045790111, which also holds the seat lean A on each:

Out of scope, filed: #22168. A user-scoped settings key resolved without a userId reads the first user row. It pre-dates this PR, and its first step is to measure reach.

objectstack-fleet commented on Oct 8, 2026

@objectstack-fleet
Contributor

Claim: PM loop round 1 (stage: scope item (2), sys_audit_log, of this card's two open items; item (1) landed as PR #22107, item (3) as PR #22166 c52bfb417b, released 6055410828) · 2026-10-08T08:20Z
Session: session_01LAi5BVvQNiYzepSAcsoFLK
Account: os-litant (the seat's linked user as GET /user answers it; the card's assignee from this act)
Branch: claude/issue-15207-audit-log-attribution
Worktree: objectstack-issue-15207-audit
Domain: domain:spec (the card also carries domain:services; this seat holds it whole, as for item (3))
Seat: domain:spec#1 (seat post #6017)
File surface (at origin/main 0e9371f0c6; stop on breach and explain in the report). Scope item (2) alone, under ADR-0131 D7: sys_audit_log carries no injected organization column; the organization a row is about is the plain attribution field tenant_id, which the tenant-field resolver does not claim; the object is governed by object permission, not by the wall.

  • The object: packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts (systemFields.tenant: false; tenant_id stays the attribution lookup), and the plugin-audit writers that stamp it, if any rely on the injected column.
  • The read scope (option A of report 6042515710, the seat lean recorded in 6043540291): in packages/plugins/plugin-security/src/objects/default-permission-sets.ts, a platform tenant RLS policy on sys_audit_log (tenant_id equal to the caller's organization), stripped under single by the existing provenance rule (ADR-0105 D3), plus an explicit sys_audit_log entry in organization_admin without viewAllRecords / modifyAllRecords. Platform admins keep reading everything (view_all_audit_log).
  • Retention: packages/objectql/src/lifecycle/lifecycle-service.ts, so that per-tenant audit retention partitions on the attribution field rather than the retired column.
  • The description: view_all_audit_log in packages/spec/src/security/capabilities.ts (and its mirror in eval-user.zod.ts if it restates it), re-premised on the attribution field.
  • ADR-0087: step-18 entries and the regenerated registry.ts, if a stored shape moves; the regenerated tenancy censuses. Tests of each package touched, and .changeset/15207-*.md (minor with its BREAKING banner).
  • Not this stage: item (4), feat(spec,security): OrgScopingEntitlement grows platform-global exemption + unbounded-admin suppression, consumed by Layer 0 arming #12699 made total. The stored-row move (existing rows' injected column dropped, after its values are confirmed in tenant_id) belongs to C7's inventory (feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211, fate 1, the ADR-0120 D4 ceremony). The PR body names the fate for C7, and C6(2) ships in C7's release, as C6(3) does.
  • Declared cross-lane files: domain:services (plugin-audit, plugin-security) on [PM seat] domain:services · seat 2 — ⏳ vacant #21118 and [PM seat] domain:services — 🟢 os-bill #6021; domain:engine (objectql) on [PM seat] domain:engine — 🟢 os-litant · session_01EUBvqtauTDmHi2ZgY759p2 #6367 and [PM seat] domain:engine · seat 2 — ⏳ vacant #20966.
    Container & model: L, mode:subagent, model: opus (dispatch-gates --tier: "no path-derived mandate"). An at-tier contract review is owed before enqueue: the path limb is packages/spec/src/**, and this claim's Clause-②: yes. It comes from an isolated at-tier subagent.
    Clause-②: yes (narrowing: sys_audit_log loses its injected organization column, and its read scope moves to a declared row policy; the dev measures the built declaration closure)
    Responsibility: n/a, not a defect card (ADR-0131 C6, item (2))
    Thread-read: 6055410828
    Premises (verify each before code, with a reading; ⛔ stop and report a fork if one fails, and ⛔ do not fall back to option B (a read middleware) or option C (tenant_id as the tenancy anchor)):
  • P1. sys_audit_log holds rows about deployment-level actions that carry no organization: config_change, import, platform_admin_standing_change.
  • P2. With the injected column gone, organization_admin's '*' bypass skips Layer 1, so without an explicit entry an organization admin would read every organization's rows.
  • P3. The ADR-0105 D3 provenance rule strips a platform tenant RLS policy under single.
    Serial constraints cleared:
  • Of the 21 open PRs at this stamp, none touches sys-audit-log.object.ts, plugin-audit/src, default-permission-sets.ts, lifecycle-service.ts or security/capabilities.ts.
  • Item (4) and C7 (feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211, pm:blocked, domain:engine) are not in flight.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions