Skip to content

Security: a runAs system flow writes identity-assignment rows (sys_user_position, sys_invitation) past the delegated-administration gate #25251

Closed

Description

@objectstack-fleet

Ruled: 6354966423 · letter B · 2026-10-23T01:05:47Z

Filing gate ①: a measured authorization defect (reproduction withheld from this public card: possible privilege exposure). Filed by repo:cloud#1 (seat post #6026, session session_01FSg7JRUiTPo8GrgT38tAWP) from a local-stack measurement the maintainer's ruling on objectstack-ai/cloud#3149 asked for (item 5, the two 「to verify」 reads). ⛔ Not a claim.

Acting reader: the maintainer first (disclosure and severity call; the full reproduction, flows, rows and log lines are on objectstack-ai/cloud#3149, a private repository), then the domain:services / security lane at dispatch.

What is read (code, framework pin 9d13b5fc)

  • A runAs: 'system' flow's data context is { isSystem: true, … } (packages/services/service-automation/src/runtime-identity.ts:270–:277).
  • packages/plugins/plugin-security/src/security-plugin.ts:2405–:2407 returns next() for isSystem before delegatedAdminGate.assert(opCtx) (:2546).
  • So the ADR-0090 D12 / ADR-0105 D8 checks never run for such writes: the anchor refusal, the org-admin boundary, adminScope containment, the placement and role cap, and the granted_by stamp (delegated-admin-gate.ts).
  • The gate's header says system writes short-circuit by design (seeders and similar). Tenant-authored flows fall into the same bypass.

What was measured (local stack only; details withheld here)

  • A system flow assigned a non-reserved position to another user, including a position bound to a full-access permission set. The row carried no granted_by, and the assignment took effect in that user's session. A scheduled system flow did the same with no acting user at all.
  • A system flow wrote a sys_invitation row straight into the environment. better-auth then treats it as a real invitation, without the invite door's email, placement or role-cap checks.
  • The only check that still applied was the row-level refusal of the four reserved identity names (sys-user-position.object.ts:256–:264).

Not measured

  • Which principals can author or save a runAs: 'system' flow on a hosted environment. The measurement authored as a platform admin. Severity depends on this read, and it comes first at dispatch.

Wanted

  • The governed identity-assignment objects (at least sys_user_position, sys_invitation, sys_member) are not writable by tenant-authored system flows past the D12 gate.
    • Either the gate also runs on isSystem contexts that originate from authored flow metadata (with the flow's author or owner as the judged principal), or those objects refuse writes from such contexts outright.
    • Platform seeders keep their path.
  • A provenance stamp (granted_by / the flow) on whatever path remains.

Acceptance

On a local stack, a tenant-authored runAs: 'system' flow that writes sys_user_position or sys_invitation is refused, or is judged by the D12 gate as its author would be. A platform seeder's write still succeeds.

Dedupe words: runAs system sys_user_position delegated-admin gate bypass; granted_by unstamped system flow; sys_invitation raw write system flow; isSystem short-circuit RBAC assignment.

Activity

  1. added
    bugSomething isn't working
    priority:p1High: required for production / M2
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Oct 22, 2026
  2. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Decision card: needs_decision fork review (project review thread 「#25251 披露口径裁决」) · letters A / B / C · recommends B · maintainer 「对于这种类似的问题,能否你自己开一个单独的 fable thread 做评审,对于自己有把握决定的问题,自己决定,不能决定的开决策卡?」 2026-10-22T19:41Z

    Reviewed by ruling session session_01K89MJzkZf3kKA7pY3ECfWH (project thread https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuYJtf7LmCbKh7net8LXW4cR), opened by the domain:services seat 1 project when this card reached dispatch. Marker: needs-user-decision (not auto-adjudicated). The fix direction narrows a published contract (FlowSchema.runAs says a system flow keeps system privileges) and chooses between product capabilities, so under the confidence gate (SKILL.md 〈升级与决策〉; triage-duties.md ①–③, floors 已发布契约语义 / 产品能力取舍) it is the maintainer's letter, not this thread's. State: pm:queue → needs-user-decision in the same act. ⛔ Nothing below is a reproduction; the recipe stays on objectstack-ai/cloud#3149 per the disclosure rule quoted under Governing text.

    维护者速读

    一句话问题:环境管理员写的 runAs: 'system' 流程,给别人分配岗位(含满权限岗位)、直接写邀请行时,不经过 ADR-0090 D12 委托管理门,行上也没有 granted_by;这是按 ADR-0073「system = god-mode」的设计,还是要像 2026-10-03 对 sys_metadata 那样,把「应用作者写的自动化」从 system 提权里再切出一块?

    Governing text

    • packages/spec/src/automation/flow.zod.ts:1163 (FlowSchema.runAs describe, the published contract): "system = elevated (bypasses RLS) — system flows keep system privileges". Re-check: grep -n "system flows keep system privileges" packages/spec/src/automation/flow.zod.ts → 1 hit.
    • docs/adr/0073-automation-execution-identity.md Amendment (2026-07-28, The #1888 user-less fail-open is wider than the lint that guards it — record-change flows fired by a system write run UNSCOPED, unlinted #3760): "The security middleware's isSystem short-circuit precedes its package-managed-row / system-row / audience-anchor / delegated-admin gates, all of which a principal-less context still has to clear — so "unscoped" was never equivalent to "system", and elevating would have widened these runs (e.g. letting them write sys_user_position) rather than preserving the status quo." and M2: "runAs:'system' stays god-mode throughout." Re-check: grep -n "stays god-mode throughout" docs/adr/0073-automation-execution-identity.md → 1 hit.
    • docs/adr/0096-execution-surface-identity-admission.md:56: "| Flow (runAs:'system') | explicit elevation | ADR-0049/[P0][security] Flow runAs never switches execution identity #1888, by design |".
    • packages/plugins/plugin-security/src/delegated-admin-gate.ts header: "System/boot writes carry isSystem and short-circuit the security middleware before this gate (seeders, publish materializer, better-auth reconciliation are unaffected)."
    • Precedent, the maintainer's one carve-out of authored-flow reach from system elevation: packages/spec/src/kernel/stored-metadata-body-objects.ts:74-78 STORED_METADATA_BODY_PRESCRIPTION: "Elevation (runAs, a system context) does not change this."; its D3 entry 18.flow-write-node-stored-metadata-target-refused.ts: "The maintainer ruled (2026-10-03) that app-authored work may not write those tables: the metadata protocol is their only writer … and a flow is app-authored automation." Re-check: grep -rn "a flow is app-authored automation" packages/spec/src/migrations/entries/semantic/ → 1 hit.
    • .claude/skills/pm-dispatch/references/lanes/services.md: 「安全与权限边界的放宽是人工地板,恒升级不代裁。」「安全族卡的披露纪律:复现配方不落任何公开面(卡、PR、评论)。证据以抽象描述或私有通道承载。」
    • .claude/skills/pm-dispatch/references/filing-gate.md: 「可达面定上界:security 标签不抬级;仅最高权限可达且无现用者至多 p3,可记录不修。」「协议类:修复改 spec 接受/声明面 … 此类任何车道皆 p1 + 在研大版 target: … 维护者裁决,本节余条不压。」
    • Disclosure, maintainer ruling 2026-08-18 on skills: five principle gaps the #9296 wave exposed in checklist-test / checklist-author / dogfood-verification #9387 (docs/qa/platform-checklist/RUNNER.md:80-91): "Never publish a reproduction for an authentication or authorization hole anywhere on GitHub … Existence published, recipe withheld, is a complete and actionable report".

    Is the protocol changed? Letters A and B narrow what FlowSchema.runAs declares, so each is 协议类 (one describe sentence + an ADR-0087 semantic entry in packages/spec, domain:spec, Clause-②: yes (narrowing)); letter C changes no declaration.

    Premises (each with its re-check; read on origin/main 2069b81 / HEAD 72fad92)

    1. The system short-circuit precedes the gate: grep -n "if (opCtx.context?.isSystem)" packages/plugins/plugin-security/src/security-plugin.ts → line 2613 returns next(); delegatedAdminGate.assert sits below it.
    2. A runAs: 'system' run's data context is { isSystem: true, actor: 'svc:flow:<name>', flowRunId, userId? }: grep -n "svc:flow:" packages/services/service-automation/src/runtime-identity.ts → 1 hit (~line 270). flowRunId is provenance, never a principal (packages/spec/src/kernel/execution-context.zod.ts:400-411), so the gate CAN tell a flow-origin system context from a seeder's without a new key.
    3. Flow authoring requires manage_metadata: grep -n "FLOW_AUTHORING_CAPABILITY" packages/runtime/src/domains/automation.ts → = 'manage_metadata' at 380; /meta save doors the same (packages/rest/src/meta-collection-routes.ts:452).
    4. manage_metadata is platform-exclusive: grep -n "name: 'manage_metadata'" packages/spec/src/security/capabilities.ts → scope: 'platform'; grep -n "PLATFORM_ADMIN_ONLY_CAPABILITIES" packages/plugins/plugin-security/src/security-plugin.ts → lists it; organization_admin withholds it by design (default-permission-sets.ts:341).
    5. granted_by is stamped only on admitted NON-system writes: grep -n "Stamp the writer into" packages/plugins/plugin-security/src/delegated-admin-gate.ts → line 199.
    6. Blind spot, not measured: a hand-authored set carrying manage_metadata without the * superuser bit is PLATFORM_ADMIN for Layer 0 but not "tenant-level" for D12 (gate header's definition), so such a holder could reach assignment rows through a system flow that it cannot write directly. No built-in set has that shape (default-permission-sets.ts), so it is a residual, not the measured case.

    Options × real cost × effort

    选项 做什么 客户可感知后果 工作量
    A 按作者判 D12 门也跑在「源自流程元数据」的 system 上下文上,以流程作者/owner 为被判主体 管理员写的流程照常生效;作者权限不够时流程失败(运行期才知道);需要把「作者身份」带进运行上下文——新的判定模式 services + spec(ExecutionContext 加作者位 + runAs 声明);无同形先例;比 B 大
    B 拒绝(推荐) 源自流程元数据的 system 上下文(isSystem 且带 flowRunId)写 sys_user_position / sys_user_permission_set / sys_invitation / sys_member 一律 PERMISSION_DENIED + 处方:改 runAs: 'user' 让 D12 门按操作人判,邀请走邀请门;seeder / better-auth / 系统服务(无 flowRunId)不动 想用 system 流程自动分配岗位的管理员收到响亮拒绝和处方;没有静默分支 services 运行期一 PR(先例 #21649:+465/−4,4 files)+ spec 声明一 PR(先例 #21687:+658/−23,13 files,Clause-②: yes (narrowing));两 PR 顺序同 #21624→#21654
    C 记录不修 维持 ADR-0073「god-mode throughout」;可达仅最高权限 ⇒ 降 p3;只补留痕:system 流程写入时把流程 actor 打进 granted_by 行为不变;审计能看到是哪条流程给的;邀请门检查仍被跳过 services 一 S/M PR;无先例,改动在 delegated-admin-gate.ts 的 stamp 分支;0 spec

    业务含义直译:A = 「自动化以写它的人的身份管人」;B = 「人事/权限分配这类治理动作,自动化不准以超级身份做,要么以操作人身份走门,要么走邀请流程」(参照:Salesforce 把权限集分配等 Setup 对象与业务数据的 system-mode 旁路分开治理);C = 「超级管理员写的脚本本来就是超级权限,记清楚是谁就行」。

    四维分析(从业务立场)

    ① 项目长远合理性(≥50%):两年后的平台样子是 ADR-0073 D2——应用自动化以 automation 主体、RLS 受约束地跑,system 是平台代码的提权,不是应用作者的常态。维护者 2026-10-03 已经为元数据表切过一次「应用作者写的流程 ≠ 平台代码,提权不改变这一点」(#21520 A → #21624/#21654)。身份分配表是另一组有自己门的受治理表(D12 门、邀请门)。B 把这条规则从一张表扩到同族,是一条规则覆盖全族,缩小特例;A 新增一种判定模式(以作者为主体),是契约增生;C 把已知的留痕缺口留在平台上。只看①选 B。
    ② 实际业务拉动:今天撞上的是 cloud 的实测(cloud#3149 第 5 项),不是客户投诉;没有量到任何依赖「system 流程分配岗位」的生产流程(本仓、examples、skills、docs 对身份表的写节点未做普查——执行时先做,同 #21649 的 census 段)。零拉动的能力默认 remove,与 B 同向;C 的留痕是真实审计需求,B 顺带满足(拒绝后不存在无 granted_by 的行)。
    ③ 防 AI 犯错:AI 写元数据 app 最容易的错就是「不会就 runAs: 'system'」(ADR-0138 D2b 已为此拒过一次匿名门)。B 让这种流程在碰身份表时响亮拒绝并给处方;A 在作者是管理员时静默放行、作者不够时运行期才失败;C 静默成功。B 最优。
    ④ 创业阶段不扩散:B 删能力不加键;A 加「作者主体」概念并永久维护;C 不动。④ 的「新增门禁默认否」:B 不是新门,是既有 D12 门的一条臂 + #21654 已立的「流程写受治理表拒绝」类的同族扩展;点名的维护者原话即上文 D3 entry 引文。不翻转。
    自检行:只看①选 B;②③④ 是否翻转:否。

    os-decision-facets
    ① 项目长远合理性:B 缩小特例——「应用作者的自动化不以提权碰受治理表」从元数据表扩到身份分配表,一条规则;A 扩大契约(新判定主体);C 留缺口。
    ② 实际业务拉动:零拉动(无生产流程依赖 system 流程分配岗位;实测来自 cloud 本地栈),默认 remove;审计留痕为真实需求。
    ③ 防 AI 犯错:B 响亮拒绝 + 处方;A 对管理员静默放行;C 静默成功。
    ④ 创业阶段不扩散:B 删能力不加键,非新门(既有 D12 门臂 + #21654 同族);A declare-and-maintain;C 维持。
    Prior rulings read: runas,issystem,delegated-admin,sys_user_position,sys_invitation,elevation → 19 hits; ADR-0073 Amendment (2026-07-28) + ADR-0096 D5 table + ADR-0103 D2 + ADR-0090 D12 (amended 2026-10-17, 6228728551) + ADR-0131 D3 read; tool's top hit ADR-0058 D8 read, off-topic; thread: none; repo: objectstack-ai/objectstack

    Recommendation and confidence gaps

    B. Not A: it mints a second judging principal (the author) that no ADR names, and it hides the failure until run time. Not C: it keeps a measured audit gap on a security table and leaves the invitation door's checks skippable, for a capability nobody was measured using. Gaps, stated and not used to change the letter: (i) no census yet of write nodes aimed at the four identity objects across the repo, examples, skills, docs and hotcrm (B's PR does it first, as #21649 did; a non-zero count is reported, not worked around); (ii) premise 6's residual shape is unmeasured; (iii) sys_user_permission_set is added to the card's three objects because the gate governs it beside sys_user_position (gate header) — the maintainer may strike it.

    Execution after the letter (so the maintainer rules direction only)

    • B → priority stays priority:p1 (协议类), two PRs in order, mode:thread, effort xhigh, services seat 1 dispatches: PR1 domain:services run-time arm in plugin-security (refuse when context.isSystem && context.flowRunId and the object is one of the four; PERMISSION_DENIED; prescription names runAs: 'user' and the invitation door; seeders unaffected; pins for refused / seeder-control / runAs: 'user'-judged-by-D12 cases; ablation; census first; disclosure rule: pins describe the behaviour, no recipe). PR2 domain:spec (one sentence on the runAs describe + ADR-0087 D3 semantic entry, Clause-②: yes (narrowing)), filed as a hand-off comment on the spec seat since this project does not hold domain:spec. The card parks pm:blocked + Blocked-by: on PR2's card after PR1 lands, as security(automation): flow create_record and update_record nodes write the stored-metadata family tables directly, outside the metadata protocol (measured; the unruled neighbour of #21519) #21624 did.
    • A → one services PR plus the spec ExecutionContext key and runAs sentence; the maintainer names which identity is "the author" (creator vs last editor) in the same word.
    • C → relabel priority:p3, one S/M services PR stamping the flow actor into granted_by on the system path; the card closes on it.
    • Any letter: dispatch as a security card thread (SKILL.md: 席位是项目线程时 security 一律 thread); the reproduction stays on cloud#3149 and off every public surface.

    Related: cloud#3149 (private; the measurement), #21520 / #21624 / #21654 (the precedent carve-out, PRs #21649 and #21687), #23960 (D12 amendment, 6228728551), #9387 (disclosure ruling), ADR-0073, ADR-0090 D12, ADR-0096, ADR-0103 D2, ADR-0131 D3.


    Generated by Claude Code

  3. os-elon-musk commented on Oct 23, 2026

    @os-elon-musk
    Collaborator

    Ruling: batch #2 item 3 · letter B · maintainer 「25251 B hotcrm B」 2026-10-23T01:05:47Z

    Recorded by the director seat (project thread 「待决策汇总」, session session_01Bf4vhia9fHNXpqC1PCfxie, posting from the maintainer's own account). The brief the maintainer read is card 3 of the batch #2 document, built on the review above (6349680816). Premises re-read on origin/main 9dc1f42d50 before the brief: the isSystem short-circuit still precedes delegatedAdminGate.assert (security-plugin.ts:2611), FLOW_AUTHORING_CAPABILITY = 'manage_metadata' (automation.ts:380), and flow.zod.ts:1164 still reads "system flows keep system privileges".

    Letter B, as the review's table states it: a system context that originates from flow metadata (context.isSystem && context.flowRunId) writing sys_user_position, sys_user_permission_set, sys_invitation or sys_member is refused with PERMISSION_DENIED and a prescription (runAs: 'user', so the D12 gate judges the acting user; invitations go through the invitation door). Seeders, better-auth reconciliation and system services without a flowRunId are unchanged. The maintainer did not strike sys_user_permission_set, so the four objects stand.

    Workload of the chosen letter (the card's workload column): one services run-time PR (precedent #21649: +465/−4, 4 files) + one spec declaration PR (precedent #21687: +658/−23, 13 files, Clause-②: yes (narrowing)), in the #21624 → #21654 order. Priority stays priority:p1 (协议类).

    Execution (the review's "B" line): domain:services seat 1 dispatches a security card thread, mode:thread, effort xhigh. PR1 (domain:services): the run-time arm in plugin-security; census of write nodes aimed at the four objects first (a non-zero count is reported, not worked around); pins for refused / seeder-control / runAs: 'user'-judged-by-D12; ablation; pins describe behaviour and never carry a recipe. PR2 (domain:spec): one sentence on the runAs describe + an ADR-0087 D3 semantic entry, handed to the spec seat; this card parks pm:blocked + Blocked-by: on PR2's card once PR1 lands. The reproduction stays on objectstack-ai/cloud#3149 and off every public surface.

    State: needs-user-decision → pm:queue in the same act (the six states are exclusive; one replace). ⛔ Ruled, not dispatched.


    Generated by Claude Code

  4. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    and removed
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Oct 23, 2026
  5. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: card thread · domain:services seat 1 (seat post #6021, project run by the Sales account, 由Sales账户的项目负责) · p1 · security · PR1 of ruling 6354966423 (letter B), the run-time arm · 2026-10-23T15:06Z
    Session: session_01YAdmc3d4UL4jFaffAor6eu
    Account: os-sales (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-t123rb (the harness-pinned branch for this session; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match — this claim is the identity record)
    Worktree: objectstack-issue-25251
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (read on origin/main b2c075290; stop on a breach and explain it in the report):

    • packages/plugins/plugin-security/src/flow-system-identity-write-refusal.ts (new): the predicate and the refusal. A context with isSystem and a non-empty flowRunId that inserts, updates, deletes, transfers, restores or purges a row of sys_user_position, sys_user_permission_set, sys_invitation or sys_member is refused with PERMISSION_DENIED (403). The prescription is the ruling's: set the flow's runAs to 'user' so the delegated-administration gate judges the acting user; invitations and memberships go through the organization doors (the invitation door). Reads are untouched.
    • packages/plugins/plugin-security/src/security-plugin.ts: one import and one call in the data middleware, immediately before the isSystem bypass. Nothing else in the file moves.
    • packages/plugins/plugin-security/src/delegated-admin-gate.ts: comment only. The header and the GRANTED_BY_OBJECTS docblock say every system write short-circuits before the gate; they gain the flow-origin exception.
    • packages/services/service-automation/src/runtime-identity.ts: comment only, the resolveRunDataContext docblock sentence that says a system run "reads/writes with full access".
    • Tests: a new pin file in plugin-security (the refusal against the real SecurityPlugin middleware: every governed verb on each of the four objects, the seeder control without flowRunId, the runAs: 'user' context handed to the D12 gate, reads and other objects unaffected), and a new integration pin in service-automation (real kernel, real SecurityPlugin, real AutomationEngine CRUD nodes: a runAs: 'system' flow is refused with no row written; a runAs: 'user' flow is judged by the D12 gate, admitted with granted_by stamped for a tenant-level administrator and refused for a plain member; a platform seeder's write still succeeds). Pins describe behaviour only; no reproduction is carried.
    • One minor changeset for @objectstack/plugin-security.
    • Not packages/spec (PR2 of the ruling: the runAs describe sentence and the ADR-0087 D3 semantic entry, handed to the spec seat), not an ADR, not the docs pages.
      Census (taken before any edit, read-only): create_record / update_record / delete_record / upsert_record node literals in objectstack at b2c075290 and hotcrm at c14ffde5f (TS, JS, JSON, YAML, MD, MDX; node_modules and dist excluded): 317 nodes, 0 of them aimed at any of the four objects. 78 carry no literal target near the node literal; each was read by hand (node descriptors, docs fragments, one showcase purge of its own object), none names the four. One delete_record on sys_user exists, in a CLI migration fixture that is never run as a flow. The count is zero, so nothing is worked around.
      Container & model: M, card thread (in-session executor), model: default tier (dispatch-gates.mjs --tier on this surface: "no path-derived mandate"), effort: xhigh (p1, security)
      Clause-②: no (narrowing)
      Metadata-protocol: semantic
      Basis for the two lines: no because no published accept set or public surface widens. (narrowing) because a published behaviour is withdrawn: FlowSchema.runAs says system flows keep system privileges, and a runAs: 'system' flow's write to the four objects is refused where it ran. semantic because the meaning of the authorable key runAs: 'system' changes for those writes while every flow still saves; that takes minor. The ADR-0087 D3 semantic entry is the spec lane's half (PR2); this changeset carries a written disposition beside the BREAKING banner.
      Responsibility: plugin-security's data middleware returns next() for isSystem ahead of the D12 gate, and a runAs: 'system' run's data context is isSystem plus flowRunId (service-automation resolveRunDataContext) | no platform path covers it: the D12 gate and the identity write guard both stand down for a system context, so only the row-level refusal of the reserved identity names applies | authoring or saving a flow needs manage_metadata, a platform-admin-only capability (FLOW_AUTHORING_CAPABILITY, packages/runtime/src/domains/automation.ts), so the reach is the environment's top administrator; the census above found no shipped flow using it
      Thread-read: 6354966423
      Serial constraints cleared:
    • PR refactor(plugin-security): move the read filters, field readers and object admissions out of security-plugin.ts (#25223 cut 9) #25456 ([maintainer] refactor(plugin-security): split src/security-plugin.ts (10,746 lines, +2,803 in 30 days) along its SecurityPlugin seams, one move-only slice per PR #25223 cut 9, draft) edits security-plugin.ts's import block and the read-filter, field-reader and object-admission regions; this card adds one import and one call in the data middleware ahead of the isSystem bypass, outside its hunks. Whichever lands second merges main.
    • PR refactor(service-automation): move the registration-time checks into flow-registration-checks.ts (#25222 slice 1) #25270 ([maintainer] refactor(service-automation): split src/engine.ts (13,735 lines, +2,199 in 30 days) along its AutomationEngine seams, one move-only slice per PR #25222 slice 1, draft) edits service-automation/src/engine.ts; this card touches runtime-identity.ts comments only.
    • No open PR declares delegated-admin-gate.ts or runtime-identity.ts.
      Resume-from: none

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25251,
      "status": "done",
      "branch": "claude/project-thread-t123rb",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25483",
      "session": "session_01YAdmc3d4UL4jFaffAor6eu",
      "premise_still_valid": true,
      "summary": "PR1 of ruling 6354966423 (letter B), the run-time arm. plugin-security's data middleware refuses a governed write (insert, update, delete, transfer, restore, purge: the D12 gate's GOVERNED_OPERATIONS) to sys_user_position, sys_user_permission_set, sys_invitation or sys_member from a context carrying isSystem AND a flowRunId, immediately before its isSystem bypass, with PERMISSION_DENIED (403) and the ruling's prescription (runAs 'user' for the two grant tables, the organization endpoints for invitations and memberships). Seeders and other system writers without a flowRunId, reads, other objects and runAs 'user' runs are unchanged; a runAs 'user' run reaches the D12 gate as the user it acts for. Census first: 317 write-node literals across objectstack b2c075290 and hotcrm c14ffde5f, 0 aimed at the four. PR2 (spec runAs describe sentence plus the ADR-0087 D3 semantic entry) is handed to spec seat 1 by pointer 6368756448 on #6017; after this PR lands the card parks pm:blocked with Blocked-by the spec card.",
      "tests": "New pins: plugin-security/src/flow-system-identity-write-refusal.test.ts (44: predicate over every governed verb on each object; real SecurityPlugin middleware over real ObjectQL plus SqlDriver for refused insert/update/delete from user-triggered and user-less flow system contexts with code/status/details asserted and the store unmoved; seeder control; reads and other objects; runAs user admitted for a tenant admin with granted_by stored, refused by the D12 gate for a plain member) and service-automation/src/runas-system-identity-write-refusal.integration.test.ts (9: real kernel, SecurityPlugin and AutomationEngine CRUD nodes; innerRefusal.code PERMISSION_DENIED and no row for runAs system create/update/delete; runAs user granted_by stamped for the tenant admin and refused by D12 for a member; platform-writer and system-flow read/other-object controls). Package suites under the lock: plugin-security 215 files, 4509 passed, 45 skipped; service-automation 199 files, 2628 passed (VERDICT command-exit 0). pnpm typecheck in both packages exit 0 including check:test-typecheck. eslint --no-inline-config on the 6 changed TS files exit 0. Ablation via scripts/ablation-replace.mjs on HEAD a6bc84f36e: the middleware call disabled behind the planted marker __ablation_25251__ (anchor 1 to 0, blob 68258e636f8b to 50a6c58e75f3); plugin-security rebuilt and ablation-dist-preflight found the marker in 2 built files before the service-automation leg. Under ablation: plugin-security pin 8 failed, 36 passed (the 8 middleware refusals); service-automation pin 5 failed, 4 passed (4 system creates plus update/delete). Restore: blob equals HEAD, git diff HEAD empty, rebuilt, marker absent from all 6 built files, pins 44/44 and 9/9. A first ablation run also reddened the platform-writer control as a knock-on (the ablated flow wrote the same sys_member row the control inserted); the control now writes its own non-member (commit a6bc84f36e) and the second run is the one quoted.",
      "gates": "Final head 648d903049 (merged origin/main ea33919f56): closure build turbo --filter=@objectstack/dogfood^... --filter=@objectstack/service-automation... VERDICT command-exit 0; dispatch-gates --commands derived 100 families, 100/100 exit 0 (each line recorded as cmd :: exit N); dispatch-gates --ran on that record: 100 derived, 100 run, 0 NOT-MEASURED, 0 UNRUN, a DERIVED zero. First pass on cf48a538e (70 families) had 3 non-zero: check-system-context-census exit 1 (new isSystem read site without a page row; fixed by row 1b in content/docs/permissions/system-context.mdx), check-plugin-teardown-shape --self-test exit 3 (shallow clone; git fetch --unshallow), check:dual-build-cjs-loads exit 3 (7 prerequisite dists built). node scripts/check-adr-0087-registration.mjs --base origin/main exit 0 on a committed head. PR check-runs on 648d903049: 42 runs, all success or skipped, including Check Changeset success, Lint and Repo Gates success, Type Check workspace success, Test Core 1-6 of 6 success, Build Core and Build Docs success, The card this PR closes must claim this branch success, Part-of PR must not also close its card success; Dogfood Regression Gate shards skipped by the CI selector (the local battery below covers them).",
      "dogfood_battery": "648d903049 · os-verify-lock VERDICT command-exit 0, held 801s · Test Files 290 passed, 3 skipped (293) · Tests 2494 passed, 18 skipped (2512) · no failures",
      "mcp_calls": "read-only GitHub MCP calls: pull_request_read (get #25439 for the body shape, get_check_runs #25483, plus get_files on #25456 before context compaction), issue_read (get and get_comments on #18549, plus card and comment reads before compaction), search_issues x2, list_issues x1 (pm:seat posts), list_pull_requests and search_pull_requests before compaction; the exact pre-compaction count was not retained. No MCP write tool was used. subscribe_pr_activity (claude-code-remote) on #25483.",
      "api_writes": "7 relay strokes via scripts/pm/ (fleet-write dispatch, objectstack-fleet[bot]): claim comment 6368019403 on #25251 (post-stamped); label-write on #25251 (pm:queue to pm:dispatched, assignee os-sales, read back MATCHES); pr_create #25483 (draft, assignee os-sales; 9485 bytes sent and stored identical); pr_request_reviewers #25483 os-sales; hand-off pointer 6368756448 on #6017 (post-stamped); this report comment; plus git push (not a REST write).",
      "deviations": [
        "File surface: delegated-admin-gate.ts was claimed as comment only; GOVERNED_OPERATIONS also gains export and a ReadonlySet type so that the refusal and the D12 gate read one verb list. The package index is unchanged.",
        "File surface: content/docs/permissions/system-context.mdx was outside the claim; check-system-context-census requires a row for every isSystem read site, so row 1b was added (its text avoids row-number references, which the gate refuses).",
        "The first commit carried a card relation and a model-named co-author trailer; the pre-push check:commit-card-trailers refused it before anything was published, and the unpublished commit was reworded to the model-free pair."
      ],
      "open_questions": [
        {
          "question": "ADR-0073's sentence that a runAs 'system' flow stays god-mode throughout is now false for the four identity objects. Who amends it?",
          "options": [
            "A: the spec lane's PR2 carries the one-line amendment beside the runAs describe sentence and the D3 entry",
            "B: a separate Tier H docs/adr PR",
            "C: leave it"
          ],
          "recommendation": "A, because PR2 already states the same change of meaning and the precedent run-time PR (#21649) left its ADR to the spec half too; the coordinator or the spec seat decides, not this thread."
        },
        {
          "question": "The ruling's predicate is isSystem AND flowRunId. Other app-authored elevations reach the same tables without a flowRunId, by reading (not measured): a hook declared runAs 'system' fired by a human request (its ctx.api context is a system context without a run) and an action body's caller-plus-isSystem envelope. Should the refusal extend to them?",
          "options": [
            "A: file a finding for the maintainer to rule, naming the provenance marker each path would need",
            "B: record only, no card"
          ],
          "recommendation": "A, because the ruling's reason (app-authored automation; runAs is a posture, not an identity) applies alike, but extending the predicate goes beyond the letter, so it is the maintainer's call and this PR does not do it."
        }
      ],
      "out_of_scope_findings": [
        "noted, not filed: content/docs/automation/flows.mdx line 76 (the runAs row) and the access-recipes/approvals pages still describe runAs 'system' as elevated, bypassing RLS, with no mention of the identity-object refusal; true for ordinary records, so not wrong; carrier: the spec lane's PR2 (its describe sentence) or a devx docs pass · dedupe words: runAs system identity assignment refused docs",
        "consequence, stated in the PR: the refusal applies at any depth under the flow's context, so a cascade or set_null leg of a system flow's delete of a parent row (sys_user, sys_business_unit, sys_organization, sys_team) is refused too; census of such deletes in shipped flows: 0"
      ]
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Blocked-by: the spec lane's card for PR2 of ruling 6354966423 (the FlowSchema.runAs describe sentence and the ADR-0087 D3 semantic entry) (not filed yet)

    Parked · domain:services seat 1 card thread (session_01YAdmc3d4UL4jFaffAor6eu, claim 6368019403) · 2026-10-23T17:06Z · pm:dispatched → pm:blocked, assignee os-sales kept: this card waits only for PR2, so nothing here is released for another claimant.

    What landed. PR1, the run-time arm, merged as #25483 (a285ff29a1 on main). A runAs: 'system' flow's governed writes to sys_user_position, sys_user_permission_set, sys_invitation and sys_member are refused with PERMISSION_DENIED; seeders and better-auth are unchanged. Seat ACCEPT: 6369636974. Contract review: 6369623098 (PASS).

    Why a target-less line. The spec card for PR2 does not exist yet, and this project files no card in the spec lane. The line above carries no issue reference on purpose: naming the seat post would point the unlock sweep at an issue that never closes. When the card number comes back on this issue, the line is rewritten to the canonical Blocked-by: form naming that card.

    The hand-off that asks for it: #6017 (comment) (spec seat 1).

    Carried forward from the ACCEPT:

    • PR2 must ship in the same release as PR1's minor, so no release cut should precede it.
    • The contract review answered the first open question: PR2 also carries the ADR-0073 "god-mode throughout" amendment. The pointer above is amended to say so, naming ADR-0073 M2 and the ADR-0096 D5 table row, once that answer is confirmed.
    • The second open question (app-authored isSystem elevations without a flowRunId) is with the maintainer and does not block PR2.

    The card closes when PR2 merges.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed PR1 · domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · 2026-10-23T17:41Z

    reading value
    PR #25483 MERGED through the queue at 2026-10-23T17:05:18Z (CONTRACT_REVIEW_TIER record 6369623098 PASS and the seat's ACCEPT are on the PR), head 648d903049
    origin/main carries a285ff29a1

    State: the card stays open as Part of, parked pm:blocked for PR2 (the spec lane's FlowSchema.runAs describe sentence plus the ADR-0087 D3 semantic entry, hand-off 6368756448 on #6017).

    • ⛔ Release condition: PR1 ships minor with !. ADR-0087 says "same release", so PR2's D3 entry must ship in the same release. No release is cut between the two.
    • Two open questions are with the maintainer for ruling:
      • Which lane amends ADR-0073 M2 and the ADR-0096 D5 row.
      • Whether runAs-system hooks and actions that carry no flowRunId get the same refusal.

    Generated by Claude Code

  9. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Answer to the hand-off 6368756448 from domain:spec seat 1 · 2026-10-24T00:11Z: PR2 of ruling 6354966423 is card #25554 (domain:spec, p1). Rewrite this card's Blocked-by: to name it. The spec seat is dispatching it now so it ships in the same release as #25483. Apologies for the late answer.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 card thread (session_01YAdmc3d4UL4jFaffAor6eu, claim 6368019403) · 2026-10-24T04:05Z

    reading value
    PR1 #25483 (plugin-security, the run-time arm) MERGED 2026-10-23T17:05Z; origin/main carries a285ff29a1
    PR2 #25564 (FlowSchema.runAs description and the ADR-0087 D3 entry), card #25554 MERGED 2026-10-24T02:06Z; origin/main carries 025f6de3d7; #25554 closed completed
    ADR edit #25587 (ADR-0073 amendment and the ADR-0096 surface-table row), Part of #25554 MERGED 2026-10-24T02:43Z; origin/main carries 0951a00315
    Release condition: PR2 ships in the same release as PR1's minor the latest release is 18.0.0 (2026-10-20), which carries neither; the 18.1.0 notes (content/docs/releases/v18/18-1.mdx) carry both

    State: pm:blocked removed and the card closed as completed. Ruling 6354966423's letter B is delivered in full, PR1 and PR2. The target-less Blocked-by: line of the parking comment 6370398250 is void with this close.

    The two open questions from 6371045455:


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruled: A on follow-up question 2 (do app-authored hooks and action scripts with elevation but no flowRunId fall under ruling 6354966423?) · recorded by domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · 2026-10-24T13:14Z

    The maintainer's answer, verbatim (Sales, the project's standing ruling thread, 2026-10-24T13:10Z): 「25251 同意」. That accepts the follow-up decision card's recommendation for question 2. Question 1, the ADR-0073 M2 sentence, was overtaken earlier by spec PR #25587.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority: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