Repository navigation
runtime: any signed-in member can run a runAs: 'system' flow through POST /automation/:name/trigger — the door checks only anonymity, so an elevated sub-flow is an elevated door for everyone #22310
Description
Activity
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actions维护者速读(决策卡 · 安全)
问题: 流程的手动触发入口只拦匿名请求。任何已登录成员都能直接启动一个声明为
runAs: 'system'的流程,该流程随后以系统身份执行每个节点,包括自动执行的流程和本意只作为子流程调用的流程。hotcrm 实测:无任何权限集的成员,借此完成了自己被拒绝的写入。- 要修就要回答「哪些系统身份流程允许被用户直接启动」。这是安全边界,归维护者,席位与分诊都不能自裁。
- 分诊定为 p0、
security: 任何已登录成员都能经公开入口提权,出现在每个带系统流程的部署上(hotcrm 就带 24 个)。安全不等路(同 cloud#2680 的 p0 依据)。
选项 做法 代价 A 一律声明 Flow 新增一个声明键(谁可从入口启动);系统身份流程未声明即不可由非系统调用者从入口启动,屏幕流程也要声明 最紧;既有屏幕流程须补声明;spec 扩键( Clause-②: yes)B 按类型收紧(推荐) 入口拒绝非系统调用者启动非屏幕类的系统身份流程(自动执行、记录变更、定时);屏幕流程本就为用户设计,维持现状,后续需要时再加声明键 立刻堵住实测的两条路径,无需 spec 改动;屏幕流程的提权面仍由作者显式设计承担 C 只经子流程 系统身份流程一律不可从入口启动,提权只能经父流程的子流程节点(需要父运行令牌) 改动最大,屏幕流程用法被打断 推荐 B: 立即关掉实测的路径,不新增声明面;A 留给出现「屏幕流程也要限人」的真实需求时再做。
Triage:
needs-user-decision·security·priority:p0·domain:cli·area:workflowTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T14:52Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/runtime/src/domains/automation.ts(respondToFlowTrigger, the trigger door's caller check), pluspackages/spec(Flow) if A is chosen ⇒domain:cli; rationale:runtimeis that lane's. A spec key would go to the spec seat after the ruling.- Why a decision card: the fix sets who may start an elevated flow, a security boundary. No governing text answers it: the spec's
Flowhas no key for it. - ⛔ Disclosure: from here on, classes, positions and functions only, on this card and in the PR.
- After the ruling: the claiming seat measures the unmeasured flow types the card names before building, and pins each refused type with a positive control (a system caller and a sub-flow call still run).
- addedarea:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itbugSomething isn't workingSomething isn't workingpriority:p0Critical: blocker, must ship before MVPCritical: blocker, must ship before MVP
on Oct 8, 2026 objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsRuling: batch #294 item 1 · letter B · maintainer 「其他同意」 2026-10-08T23:00Z
Director seat, summon #35,
session_01VYToj6PQehTEKNrjGM9akg(GitHubos-zhuang; written asobjectstack-fleet[bot]via the relay). Presented in batch #294 from the triage seat's decision summary (6062563290) on the hotcrm measurement in the card body: A every elevated flow declares who may start it (a new Flow key; undeclared stays closed), B refuse by type at the trigger door, C sub-flow only. Triage recommended B; the director seat presented B with one clarification on theapitype; the maintainer answered 「其他同意」 (this card among the others). Thread-read: 6062563290. Freshness: no comment since the presentation. Premises re-read onorigin/main43fc50051c:respondToFlowTrigger(automation.ts:1790) checks existence and executes; the route's only caller check isshouldDenyAnonymous(:2253); the Flowtypeenum is['autolaunched', 'record_change', 'schedule', 'screen', 'api'](flow.zod.ts:1070).The ruling
B — refuse by type at the trigger door. A caller that is not the system principal may not start, through
POST /api/v1/automation/:name/triggerorflows.run, a flow declaredrunAs: 'system'whosetypeisautolaunched,record_changeorschedule: those flows run on their own triggers or as sub-flows, never as a door. The refusal is loud, in the ADR-0112 envelope (codeandstatus), and discloses nothing of the flow. Flows oftype: 'screen'andtype: 'api'keep today's behaviour: they are doors the author designed for users and API callers, and an elevated one is the author's explicit choice (ADR-0073 D2), which stays reviewable at publish. Asubflownode calling an elevated flow from its parent, and a system caller, still run. No spec key moves. ⛔ Not taken now: A (every existing screen flow would owe a declaration and the spec widens; it stays the end state for the day a real need to restrict a screen or api flow to named permission sets or positions appears, as its own card), C (screen flows broken).The platform's own rule-9 guidance (an elevated write lives in a dedicated system sub-flow called through a
subflownode) is honoured by B: those sub-flows areautolaunchedand stop being doors. hotcrm#2016 unblocks on this ruling's landing.Prior rulings read: ADR-0073 D2 (
systemis explicit opt-in), ADR-0138 D2b R (an anonymous flow door targetingrunAs: 'system'is refused at publish, the same direction), ADR-0121 D6, #14010 (hooks gainedrunAs, closed) → 4 hits; thread: 1 comment. 自检: 只看①偏 A(声明式终态),但 A 让既有屏幕流程补声明并扩键;②③④ 定 B 先行,不翻方向。置信缺口:screen与api类系统身份流程经入口的行为未实测;仓外部署未测。State
needs-user-decision→pm:queue(domain:cli,priority:p0) in this act; theRuled:line added to the body. The cli seat claims and dispatches: before building, measure each unmeasured flow type through the door; then the type × caller check inrespondToFlowTriggerwith the envelope, one pin per refused type and the two positive controls (a system caller; asubflowcall); disclosure by class and position only, on the card and in the PR.
Generated by Claude Code
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsMaintainer ruling: B, relayed by the
domain:cliseat. ⛔ Not a claimdomain:cliseat (#6024) ·session_01RWZbGvPFcRKvUqASZtunCU· 2026-10-08T23:02Z. The maintainer answered in the seat's session.- The option: B, a tightening by flow type. The trigger door refuses a non-system caller that starts a flow declared to run as system, unless it is a screen flow. Record-change, scheduled and autolaunched flows are covered. Screen flows stay as they are, and a declaration key (A) waits for a real need.
- Order: this card goes next in
domain:cli's queue, ahead of every other card. It does not run in parallel with a dev already running (the lane's concurrency stays 1). - Who builds it: the seat that ran today signs off after its in-flight landings. The next
domain:cliseat claims this card first. - What the builder owes: per triage
6062563290:- measure the flow types the card did not measure before building;
- pin each refused type with a positive control: a system caller and a sub-flow call still run.
- The refusal carries the ADR-0112 envelope.
- Disclosure: public text on this card and its PR stays at class level.
Triage moves the grade labels (
needs-user-decision→pm:queue).objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsTriage: ruled B. The labels were already moved to
pm:queueby the relay, and p0securitydomain:clistand. One reading of the carve-out is settled here before the claimTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-09T00:03Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Class level only, per the ruling's disclosure line.- The ruling of record is
6070744023(batch 🔗 Broken links detected in documentation #294 item 1, director seat, maintainer 「其他同意」). Thedomain:cliseat's relay6070761347carries the same letter. - One reading to settle before the claim. The relay summarises the carve-out as "unless it is a screen flow". The ruling text keeps both
screenandapiflows on today's behaviour, with the clarification onapithat the director presented. The builder follows the ruling text:- Refused: a non-system caller starting an elevated flow of the three self-triggered types (
autolaunched,record_change,schedule) at the trigger door, and at its in-process twin. - Unchanged:
screenandapiflows, a parent flow's sub-flow call, and a system caller.
- Refused: a non-system caller starting an elevated flow of the three self-triggered types (
- What the builder owes, per the ruling and triage
6062563290:- measure the flow types the card did not measure;
- pin each refused type with a positive control for each unchanged path;
- the refusal carries the ADR-0112 envelope and discloses nothing of the flow.
- Lane order, per the relay: next in
domain:cli's queue, ahead of every other card, at that lane's concurrency of 1.
- The ruling of record is
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_01BmsuLyUeuG5CNpZFMH1jzS
Account:os-elon-musk(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-22310-elevated-flow-trigger-door
Worktree:objectstack-issue-22310
Domain:domain:cli
Seat:domain:cli#1
File surface:packages/runtime/src/domains/automation.ts(the trigger door's caller check, besiderespondToFlowTrigger); new tests beside it underpackages/runtime/src/; one wire pin through@objectstack/verify'sflows.run(a new test file underpackages/verify/src/orpackages/qa/dogfood/test/);.changeset/22310-*.md(@objectstack/runtimepatch). Extended by this seat on the dev's blocked report (patch round 1), because these edits are this ruling's own consequence:content/docs/permissions/system-context.mdx(the census row for the newisSystemread, plus its generated counts;domain:devx's page, declared on its seat post);packages/qa/dogfood/test/flow-runas.dogfood.test.ts,packages/qa/dogfood/test/fixtures/flow-runas-fixture.tsandpackages/qa/dogfood/test/schedule-acting-organization.dogfood.test.ts(the pins that assert the pre-ruling door, flipped to the ruled one). Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: default (opus).dispatch-gates --tierprints "no path-derived mandate"; the floor is raised to default because this card implements a ruling on a permission boundary.
Clause-②: no
Responsibility:packages/runtime/src/domains/automation.ts(this lane's trigger door) produces the risk | none: the publish-time refusal of an anonymous flow door that targets a system flow (ADR-0138 D2b R) does not cover a signed-in non-system caller at this door | any signed-in member of a deployment that ships a self-triggered system flow reaches it; it is used today (a downstream app ships 24 system flows, and two paths were measured in the card body)
Thread-read: 6071477320
Serial constraints cleared: no open PR touchespackages/runtime/src/domains/automation.ts(REST file lists of all 14 open PRs, read at this claim). PR #22381 (#22301 stage 2) moves the dogfood showcase boots onto one helper; if it lands first, a new dogfood file from this card mergesmainand adopts that helper. PR #22380 and PR #22315 editservice-automation/src/engine.ts, which this card does not write. Extension: PR #22388 (#15196 S7, another lane) also editssystem-context.mdx; its counts are generated, so whichever PR lands second mergesmainand re-runspnpm gen:system-context-census. No open PR touches the three dogfood files (REST file lists of all open PRs, read at this amendment).Ruling-ref: 6070744023 (ruling B, retrieved in this act), with triage's carve-out reading 6071477320. This order is
priority:p0and goes ahead of every other card in the lane. Disclosure stays at class level on this card and in the PR. Contract-tier review: owed before enqueue. The card narrows what the door accepts, so an isolated at-tier reviewer reads it before the queue.
Generated by Claude Code
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 22310, "status": "blocked", "branch": "claude/issue-22310-elevated-flow-trigger-door", "pr": "https://github.com/objectstack-ai/objectstack/pull/22404", "session": "session_01BmsuLyUeuG5CNpZFMH1jzS — subagent of the domain:cli seat (parent's id, from the dispatch and the container's CLAUDE_CODE_REMOTE_SESSION_ID)", "premise_still_valid": true, "summary": "Ruling B is implemented in-surface on draft PR #22404 (head 0e92dbe4). respondToFlowTrigger now refuses, after the existence check and before dispatch, a non-system caller starting a flow declared runAs 'system' whose type is autolaunched, record_change or schedule. The answer is 403 PERMISSION_DENIED (an existing code, no mint), nothing is dispatched, and nothing of the flow is disclosed. The door reads the declaration through the service's own getFlow (H5 holds); H1-H4 hold. The measure-first before-table (commit cd46ffa4, real kernel via flows.run) shows a signed-in member started every runAs-system type (autolaunched, record_change, schedule, screen, api) and the elevated write landed each time. BLOCKED on the claim's file surface: the PR alone leaves two required checks red, and each fix is an edit outside the declared surface, so I stopped as the claim says. (1) Lint & Repo Gates: check-system-context-census requires content/docs/permissions/system-context.mdx to anchor the new isSystem read. The fix is to add #refusesElevatedSelfTriggeredStart to the automation-domain row's anchors, extend its sentence, then run pnpm gen:system-context-census for the 7 counts. (2) Dogfood Regression Gate: 3 tests assert exactly the starts the ruling refuses (flow-runas.dogfood.test.ts, its two system legs; schedule-acting-organization.dogfood.test.ts, control B on sqlite-wasm). A measured recipe for the first is in the PR body; the second's replacement is NOT MEASURED. Only the PM can extend the file surface to those 3 paths (plus fixtures/flow-runas-fixture.ts if the recipe edits the fixture), or route them to their owners. The card assignee was already os-elon-musk; the PR assignee is set to match.", "tests": "BEFORE-TABLE (measured first, commit cd46ffa4, packages/verify/src/automation-trigger-elevated-door.test.ts asserting it; real kernel, flows.run; neutral fixture with one object a fresh member is not granted, where the member's direct create is refused 403 PERMISSION_DENIED; one writer flow per type; row = elevated write landed, run = run-log entry): member x runAs-system autolaunched / record_change / schedule / screen / api: each 200, 1 row, 1 run. member x runAs-user autolaunched: 400 FLOW_FAILED, 0 rows, 1 run. member x a runAs-user parent whose subflow node calls the runAs-system autolaunched child: 200, child row, 1 run. platform admin x the 3 self-triggered system types: 200, row, run. system principal x the 3 self-triggered system types: 200, row, run. The verify run at cd46ffa4 (unmodified runtime dist): Tests 15 passed (15). AFTER (head 0e92dbe4): member and admin x the 3 self-triggered system types: 403 PERMISSION_DENIED, 0 rows, 0 runs. Every other row is unchanged. verify file: Tests 16 passed (16). runtime door file plus its 2 trigger-door neighbours: Test Files 3 passed, Tests 62 passed (before the typing fix; the file re-ran green in the full runtime suite). ABLATION (call site mutated to `if ((globalThis as any).__ABLATED_22310__ && await refusesElevatedSelfTriggeredStart(...))` via scripts/ablation-replace.mjs: anchor 1->0, blob 17cef8197b25 -> 3edb21bcdc2e; runtime rebuilt; ablation-dist-preflight: marker present in dist/index.js and dist/index.cjs): verify Tests 7 failed | 9 passed (16), exactly the 6 refused rows and the envelope pin; runtime Tests 6 failed | 22 passed (28), where the disclosure pin had passed vacuously over an empty message, so it was tightened in 12f88829 and re-ablated to Tests 8 failed | 20 passed (28). RESTORE: blob == HEAD 17cef8197b25 and git diff HEAD empty; rebuilt; ablation-dist-preflight --absent shows the marker absent from all 6 built files and the working tree clean against HEAD. FULL SUITES at 0e92dbe4: runtime 342 files, 4815 passed / 19 skipped; verify 20 files, 154 passed; runtime and verify typecheck exit 0; pnpm lint (full) exit 0. DOGFOOD (measured on the fix): flow-runas, schedule-acting-organization and flow-runas-schedule ran Tests 3 failed | 23 passed. The 3 failures are the door's new 403 (see summary). No integration-tier split applies: packages/cli is untouched.", "mcp_calls": "0 — no MCP GitHub tool called", "api_writes": "3 — all through the fleet-write relay (scripts/pm, transport dispatch as objectstack-fleet[bot]; each stroke is one repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (PR #22404, draft; read-back 11715 bytes identical); (2) label-write assign -> POST /repos/objectstack-ai/objectstack/issues/22404/assignees [os-elon-musk] (read-back matches; size/l was already on the PR from another actor, untouched); (3) this os-dev-report comment -> POST /repos/objectstack-ai/objectstack/issues/22310/comments. git push x5 (not REST). Reads: gh api GET only.", "gates": [ { "command": "node scripts/check-adr-0087-registration.mjs --base origin/main", "exit": 0, "verdict": "✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)." }, { "command": "node scripts/check-adr-0087-registration.mjs --self-test", "exit": 0, "verdict": "✓ check-adr-0087-registration --self-test: 441 assertions over real temp git repos (real scan()/assertInputs() path)" }, { "command": "node scripts/check-changeset-no-major.mjs --base origin/main", "exit": 0, "verdict": "· no `pull_request` payload was available to read a declaration from" }, { "command": "node scripts/check-changeset-no-major.mjs --self-test", "exit": 0, "verdict": "✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #7005 diff scoping over real te" }, { "command": "node scripts/check-ci-filter-parity.mjs", "exit": 0, "verdict": "OK: all 20 build input(s) turbo.json declares outside the packages (itself, 2 globalDependencies, and every `$TURBO_ROOT$` input of a build Build Core runs) and all 11 script(s) the `build-core` job's" }, { "command": "node scripts/check-closing-keyword-parity.mjs", "exit": 0, "verdict": "• packages/spec/src/migrations/registry.ts -- 2379111 bytes exceeds the sweep's 2097152-byte cutoff for UNREGISTERED files" }, { "command": "node scripts/check-closing-keyword-parity.mjs --self-test", "exit": 0, "verdict": "✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red." }, { "command": "node scripts/check-comment-mask-adoption.mjs", "exit": 0, "verdict": "OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/**, all 14 recorded and every recorded row still reached (13 unconverted, 1 specimen). A new one reds here" }, { "command": "node scripts/check-comment-mask-adoption.mjs --self-test", "exit": 0, "verdict": "PASS check-comment-mask-adoption --self-test (0 failure(s))" }, { "command": "node scripts/check-comment-mask-corpus.mjs", "exit": 0, "verdict": "✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 8497 files, 0 disagree, 0 unparseable, 160.4s (comparator self-test: 26 cases pass)." }, { "command": "node scripts/check-dts-emitted.mjs --self-test", "exit": 0, "verdict": "check-dts-emitted self-test: all assertions passed." }, { "command": "node scripts/check-empty-changeset.mjs --base origin/main", "exit": 0, "verdict": "✓ No changeset from the merge base modified or deleted by this diff (#17712)." }, { "command": "node scripts/check-empty-changeset.mjs --self-test", "exit": 0, "verdict": "✓ check-empty-changeset --self-test: 170 assertions over real temp git repos (real scan() path)" }, { "command": "node scripts/check-issue-citations.mjs", "exit": 0, "verdict": "✅ check-issue-citations: every citation this change adds resolves (or is a declared cross-repo reference)." }, { "command": "node scripts/check-keyed-text-bounds.mjs", "exit": 0, "verdict": "⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide. Reproduce the record: see this file's header." }, { "command": "node scripts/check-keyed-text-bounds.mjs --self-test", "exit": 0, "verdict": "PASS check-keyed-text-bounds --self-test (0 failure(s))" }, { "command": "node scripts/check-platform-object-tenancy-census.mjs", "exit": 0, "verdict": "✓ platform-object tenancy census matches the tree: 83 platform-namespace objects, 48 in the machinery's reach, 35 outside it, every exclusion explained by a declaration on its own schema." }, { "command": "node scripts/check-platform-object-tenancy-census.mjs --self-test", "exit": 0, "verdict": "✓ check-platform-object-tenancy-census self-test: all checks pass (83 objects, 35 outside the machinery)" }, { "command": "node scripts/check-plugin-teardown-shape.mjs", "exit": 0, "verdict": "✓ check:plugin-teardown-shape: 69 Plugin implementation(s) across 7921 source(s) under packages/**; every teardown-shaped method (stop / shutdown / close / dispose) sits beside a real destroy() (0 kno" }, { "command": "node scripts/check-plugin-teardown-shape.mjs --self-test", "exit": 0, "verdict": "✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay green, every roster name reds, every excluded name sta" }, { "command": "node scripts/check-registry-log-declared.mjs", "exit": 0, "verdict": "examples/app-showcase — S1 constructs a SchemaRegistry in its tests" }, { "command": "node scripts/check-registry-log-declared.mjs --self-test", "exit": 0, "verdict": "self-test OK: 16 cases + level-vocabulary read + population declaration + real-tree selection floor." }, { "command": "node scripts/check-rest-log-spy-declared.mjs", "exit": 0, "verdict": "OK: 30 of 270 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level." }, { "command": "node scripts/check-rest-log-spy-declared.mjs --self-test", "exit": 0, "verdict": "check-rest-log-spy-declared self-test reached its verdict: 23 case(s), 0 failure(s)." }, { "command": "node scripts/check-system-context-census.mjs", "exit": 1, "verdict": "check-system-context-census: 8 problem(s) over 137 anchors and 119 census sites. [site-without-a-row] automation.ts#refusesElevatedSelfTriggeredStart, plus 7 declared counts 118 -> 119 (fix is outside the claim's file surface; see summary)" }, { "command": "node scripts/check-system-context-census.mjs --self-test", "exit": 0, "verdict": "check-system-context-census --self-test: all cases passed" }, { "command": "node scripts/check-undeclared-dep-imports.mjs", "exit": 0, "verdict": "⚠ The delta is information, not a verdict — the floors are `>=` and cannot see an upward drift at all, which is why it is PRINTED. Reproduce the record: see this file's header." }, { "command": "node scripts/check-undeclared-dep-imports.mjs --self-test", "exit": 0, "verdict": "PASS check-undeclared-dep-imports --self-test (0 failure(s))" }, { "command": "node scripts/docs-audit/check-affected-docs.mjs", "exit": 0, "verdict": "→ the unreachable rows themselves: this command with --json" }, { "command": "node scripts/docs-audit/check-drift-comment.mjs", "exit": 0, "verdict": "✓ check-drift-comment: 66 cases pass across 5 fixture diff(s)." }, { "command": "node scripts/pm/release-rehearsal-clone.mjs --self-test", "exit": 0, "verdict": "✓ self-test passed" }, { "command": "node scripts/release-pending-publish.mjs --self-test", "exit": 0, "verdict": "✓ release-pending-publish self-test: 92 cases across 22 batteries pass." }, { "command": "pnpm --filter @objectstack/spec run check:duration-unit-keys", "exit": 0, "verdict": "✓ check:duration-unit-keys — 197 unit-declaring numeric key(s) across 2955 source file(s) all carry their unit in the key name (or in a sibling `unit`, or under a declared exemption: 6 declared durati" }, { "command": "pnpm check:changeset-gate-self-tests", "exit": 0, "verdict": "✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #7005 diff scoping over real te" }, { "command": "pnpm check:cross-package-test-inputs", "exit": 0, "verdict": "OK: 30 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split \"test:repo\" task); 13 walked root(s) judged, 3 on ACCEPTED_WALK_RADII; 2884 tes" }, { "command": "pnpm check:dispatcher-error-vocabulary", "exit": 0, "verdict": "[#15723] the ARGUMENT POSITION of new APIError( … ) and APIError.from( … ) IS now in this gate's population — the `apierrorarg` shape. `APIError.from` copies the record's `code` onto the body, so the " }, { "command": "pnpm check:doc-authoring", "exit": 0, "verdict": "✓ doc authoring guard: sibling-package prose ids hold the baseline — 0 pinned site(s) across 0 file(s), 89428 string(s) read in 1283 parsed source(s) (seen floor 40000 strings / 600 sources), no growt" }, { "command": "pnpm check:driver-memory-census", "exit": 0, "verdict": "check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states \"#6664 census: 2 ruled consumers\". This gate polices the census, never investmen" }, { "command": "pnpm check:dts-closure", "exit": 0, "verdict": "check-dts-closure: 35 built package(s) swept - 122/122 declared declaration file(s) present across 35 package(s); 0 built package(s) declare no declaration entry point and owe none." }, { "command": "pnpm check:dual-build-cjs-loads", "exit": 3, "verdict": "NOT MEASURED: PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ (exit 3; worktree built only the verify closure). CI owns it." }, { "command": "pnpm check:engine-double-contract", "exit": 0, "verdict": "⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide." }, { "command": "pnpm check:error-status-conformance", "exit": 0, "verdict": "✓ every derivable runtime status is documented, and every documented status is reachable." }, { "command": "pnpm check:gitlink-declared", "exit": 0, "verdict": "check-gitlink-declared: OK (10381 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declare)." }, { "command": "pnpm check:issue-citations", "exit": 0, "verdict": "✅ check-issue-citations --self-test: grammar narrowed, every spelling enumerated, qualifier a closed set of repositories, four 404 causes kept apart, both board strategies agree, diff scope red AND gr" }, { "command": "pnpm check:lean-entry-closure", "exit": 0, "verdict": "Admitted set held exactly (15 packages); 6 denied names absent." }, { "command": "pnpm check:logger-receiver-detach", "exit": 0, "verdict": "control corpus fired on all five detach shapes in this same run, and stayed silent on the measured `console` and options-callback populations -- so the zero above is a reading." }, { "command": "pnpm check:nul-bytes", "exit": 0, "verdict": "check-nul-bytes: OK (scanned 10372 text file(s) -- 10372 tracked, 0 untracked-not-ignored; skipped 9 binary; no raw ASCII control bytes)." }, { "command": "pnpm check:objectql-double-limit", "exit": 0, "verdict": "baseline key set verified against 117d34d: no files added." }, { "command": "pnpm check:objectui-changeset", "exit": 0, "verdict": "✓ objectui-range --self-test: all checks passed" }, { "command": "pnpm check:org-identifier", "exit": 0, "verdict": "check-org-identifier: OK (3256 author-facing source file(s), 18 session binding(s) resolved, no removed session.tenantId alias)." }, { "command": "pnpm check:page-declaration-shape", "exit": 0, "verdict": "blind spot: 1 computed carrier(s) no source scan can enumerate — examples/app-crm/objectstack.config.ts:86." }, { "command": "pnpm check:pm-changeset-deadline-census", "exit": 0, "verdict": "✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a reading, the verdict truth table including the inconclusive" }, { "command": "pnpm check:published-files", "exit": 0, "verdict": "✓ check:published-files — 69 publishable package(s) of 80 workspace member(s) declare a `files` whitelist that covers every entry point plus CHANGELOG.md and admits no test, test-harness config or bui" }, { "command": "pnpm check:query-options-erasure", "exit": 0, "verdict": "baseline key set verified against 117d34d: no files added." }, { "command": "pnpm check:refd-timer-probe", "exit": 0, "verdict": "1 code site(s), all inside the approved module, which is present and still reads it." }, { "command": "pnpm check:route-envelope", "exit": 0, "verdict": "read/write discriminator: 11 file(s) skipped as fetch readers (77 zero-argument `res.json()` call(s), none swept in)" }, { "command": "pnpm check:slot-lookup", "exit": 0, "verdict": "baseline key set verified against 117d34d: no files added." }, { "command": "pnpm check:sourcemap-no-sources-content", "exit": 0, "verdict": "check-sourcemap-no-sources-content: 35 built package(s) swept - 178 map(s), none embed source text." }, { "command": "pnpm check:test-source-alias", "exit": 0, "verdict": "check-test-source-alias OK — 73 packages with tests scanned; 60 registered as still resolving a workspace dep through `dist/`; 53 published subpath(s) resolved through every alias table." }, { "command": "pnpm check:tier-file-adoption", "exit": 0, "verdict": "@objectstack/cli — 82 file(s); imports readTierMode, selectTierFiles from scripts/nightly-tiers.mjs (via packages/cli/vitest-tiers.ts)" }, { "command": "pnpm check:type-check-coverage", "exit": 0, "verdict": "⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide." }, { "command": "pnpm check:type-check-debt", "exit": 0, "verdict": "surplus: none — every entry sits exactly at its measurement, so any new error is red." }, { "command": "pnpm check:watch-hint-literal", "exit": 0, "verdict": "✓ check-watch-hint-literal: 72 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 48, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECLARED_WATCH_HINTS 8 -- every one an array of quoted" }, { "command": "pnpm check:where-matcher", "exit": 0, "verdict": "baseline key set verified against 117d34d: no files added." }, { "command": "pnpm lint (full: eslint . --no-inline-config) at 0e92dbe4", "exit": 0, "verdict": "exit 0, no findings printed" }, { "command": "pnpm --filter @objectstack/runtime test", "exit": 0, "verdict": "Test Files 342 passed (342); Tests 4815 passed | 19 skipped (4834)" }, { "command": "pnpm --filter @objectstack/runtime typecheck", "exit": 0, "verdict": "check:test-typecheck: OK — @objectstack/runtime's test layer compiles under packages/runtime/tsconfig.test.json; 27 file(s) / 190 error(s) / 68 pinned signature(s) held (unchanged ledger)" }, { "command": "pnpm --filter @objectstack/verify test", "exit": 0, "verdict": "Test Files 20 passed (20); Tests 154 passed (154)" }, { "command": "pnpm --filter @objectstack/verify typecheck", "exit": 0, "verdict": "check:test-typecheck: OK — @objectstack/verify's test layer compiles; 0 file(s) / 0 error(s)" }, { "command": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran (with exit codes)", "exit": 0, "verdict": "Run reconciliation — 64 derived, 63 run, 1 NOT-MEASURED, 0 UNRUN" }, { "command": "packages/qa/dogfood: vitest run flow-runas.dogfood.test.ts schedule-acting-organization.dogfood.test.ts flow-runas-schedule.dogfood.test.ts (on the fix)", "exit": 1, "verdict": "Tests 3 failed | 23 passed (26): the 3 tests assert a member starting a runAs-system autolaunched/schedule flow at the trigger door; each now answers 403 PERMISSION_DENIED. Files outside the claim's surface, not edited." } ], "line_budget": "none set; +599 / -2 over 4 files (automation.ts +99/-2, runtime door test +167, verify wire test +313, changeset +20)", "files_changed": [ ".changeset/22310-elevated-flow-trigger-door.md", "packages/runtime/src/domains/automation.ts", "packages/runtime/src/domains/automation-trigger-elevated-door.test.ts", "packages/verify/src/automation-trigger-elevated-door.test.ts" ], "deviations": [ "Stopped at the claim's file surface: content/docs/permissions/system-context.mdx (census row plus counts) and two dogfood suites need edits for CI to go green. They were not touched, as the claim says. The PR body names both as knowingly red.", "Ran `pnpm --filter @objectstack/verify test -- --maxWorkers=2` once. The role file forbids the bare `--`, and the flag was dropped, so the whole verify suite ran (20 files, 154 passed). That was the intent of that run.", "Measured 2 throwaway probes as untracked files under packages/qa/dogfood/test (the H6 door probe and the flow-runas recipe probe). Both were deleted and never committed; git status is clean.", "Attribution: commits carry AGENTS.md's model-free trailer pair (Claude-Session plus Co-authored-by: Claude), and the PR body ends with AGENTS.md's session-URL footer. The harness reminder's model-named trailer and two-line PR footer were not used (the role file says harness attribution yields).", "Changeset declares plain `Clause-②: no` as the claim carries it, with @objectstack/runtime patch. The door's accept set does narrow for non-system callers (a security fix). Whether that wants the `(narrowing)` arm is the PM's call; I did not self-assign one." ], "open_questions": [ { "question": "How do the 3 out-of-surface edits that turn CI green get made: the census row plus counts, and the 3 dogfood tests that assert the pre-ruling door?", "options": [ "A: the PM extends this claim's file surface to content/docs/permissions/system-context.mdx, packages/qa/dogfood/test/flow-runas.dogfood.test.ts (plus fixtures/flow-runas-fixture.ts) and packages/qa/dogfood/test/schedule-acting-organization.dogfood.test.ts, and re-dispatches onto PR #22404", "B: route the dogfood edits to the owners of those proofs (authz-matrix row flow-run-as; the schedule-acting-organization card) as a Blocked-by of #22404, and extend the surface only for the census page" ], "recommendation": "A. The edits are consequences of this ruling and cannot land apart from it: the PR is red without them and they are red without the PR. The census row is mechanical. The flow-runas recipe is measured (subflow parent: 200, note touched-system; direct: 403). Control B alone needs a measured replacement." }, { "question": "Should the ruling's type x caller rule also reach the other two doors that start a flow by name (an action of type flow at POST /api/v1/actions/:object/:action plus the MCP run_action bridge, and a declared type-flow endpoint)? Measured on this head: a member still starts an elevated self-triggered flow through each, and the console's flow-action button (which goes through the trigger door) is now refused while the same declared action at /actions runs.", "options": [ "A: a new triage card deciding it (these are author-declared doors, so the ruling's screen/api reasoning may apply)", "B: leave it as accepted behaviour, recorded in the PR's acceptance notes" ], "recommendation": "A. One declared action now answers 403 at one door and 200 at another, and an in-repo example application ships an authenticated flow endpoint whose target is an elevated self-triggered flow (named in the scratch evidence file, withheld here per the disclosure ruling)." } ], "out_of_scope_findings": [ "class: a (security, the card's own privilege-escalation class at two other doors) · reach: public door measured on PR head 0e92dbe4 with a neutral fixture: POST /api/v1/actions/:object/:action for an action of type flow targeting a runAs-system autolaunched flow, and one targeting a schedule flow -> member 200, elevated write lands; a declared type-flow endpoint with authRequired true targeting a runAs-system autolaunched flow -> member 200, elevated write lands (anonymous 401); named producer: an in-repo example application ships such an authenticated endpoint (identity withheld from public text per the card's disclosure ruling; given to the seat in /tmp/claude-0/-home-user/297ae329-0faf-5db9-a494-f807d046b9f2/scratchpad/issue-22310/h6-evidence.txt) · evidence: scratch dogfood probe (deleted), readings in that file · related #22310 (ruling B scoped to the trigger door), not a sub-issue of it · dedupe words: flow action elevated door; actions route runAs system; declared flow endpoint runAs system; run_action flow elevation; door parity flow dispatch", "carrier: PR #22404 acceptance notes · the console's type-flow action buttons go through the trigger door, so after this PR an action button targeting an elevated self-triggered flow answers 403 for every user, platform admins included (ruled behaviour). In-repo corpus at 117d34de: no examples/ UI flow action targets one · noted, not filed", "carrier: PR #22404 acceptance notes · getFlow is optional on IAutomationService, and a service without it gets today's dispatch (the existence check's reading). The platform engine always implements it · noted, not filed" ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 22310, "status": "done", "branch": "claude/issue-22310-elevated-flow-trigger-door", "pr": "https://github.com/objectstack-ai/objectstack/pull/22404", "session": "session_01BmsuLyUeuG5CNpZFMH1jzS — subagent of the domain:cli seat (parent's id, from the dispatch and the container's CLAUDE_CODE_REMOTE_SESSION_ID)", "premise_still_valid": true, "summary": "Patch round 1 done on PR #22404, head 799c28789 (pushed). The amended claim 6072106844 names this branch, and every edit stays inside the extended surface. (1) Census: the automation-domain row of content/docs/permissions/system-context.mdx anchors #refusesElevatedSelfTriggeredStart with an extended sentence, and pnpm gen:system-context-census rewrote the counts (118 to 119). check-system-context-census is OK, and no other row was touched. (2) Pin sweep, repo-wide: of the 74 test files that reach the trigger door, exactly 3 pins in 2 files asserted the pre-ruling door, and all 3 are flipped with weight. flow-runas uses the measured parent-subflow recipe and adds a direct-start case (403 PERMISSION_DENIED, note untouched, no run). schedule-acting-organization control B keeps its subject (a door-launched run carries the session's organization, not the declaration's) through a door twin (same nodes and declaration, type screen, no cadence), and pins the declaring flow's refusal to the session (403, no run, nothing delivered). The system principal was measured as unreachable from dogfood: HttpDispatcher lives in @objectstack/runtime, which is not a dogfood dependency, and package.json is outside the surface. An ablation of the door check reddens exactly the 2 refusal pins; every subject leg stays green. #22407 is not touched, Clause-② stays plain no, and the PR body is not patched: replacement text is in pr_body_replacements.", "tests": "ROUND 1 evidence stands (before-table at cd46ffa4; wire and door pins; ablation; runtime full suite 342 files / 4815 passed at 0e92dbe4, runtime source unchanged since). ROUND 2 at 799c28789. flow-runas + schedule-acting-organization + flow-runas-schedule (verbose): Test Files 3 passed, Tests 27 passed. Before this round those files ran Tests 3 failed | 23 passed (26) on the fix; this round adds one case. ABLATION of the door check (scripts/ablation-replace.mjs anchor 1->0, blob 17cef8197b25 -> 3edb21bcdc2e; marker in runtime dist/index.js and dist/index.cjs) against the two flipped files: Tests 2 failed | 22 passed (24). The 2 red are flow-runas 'the trigger door refuses the member a DIRECT start ... 403' (AssertionError: direct trigger of runas_system_touch should be refused 403: 200) and schedule-acting-organization [sqlite-wasm] control B (AssertionError: a session started the runAs-system schedule flow at the trigger door: 200). RESTORE: blob == HEAD, git diff HEAD empty, runtime rebuilt, ablation-dist-preflight --absent shows the marker absent from all 6 built files and the tree clean; re-run 27 passed. GREP-HIT SWEEP: dogfood 9 door-driving files + authz-conformance.test.ts, Test Files 10 passed, Tests 168 passed; runtime 42 files, Tests 1026 passed; verify 7 files, Tests 57 passed. dogfood typecheck exit 0 (3 edited files in the program per --listFiles); full pnpm lint exit 0; census OK; 94/94 derived gates exit 0 (dispatch-gates --ran 94 derived, 94 run, 0 NOT-MEASURED).", "mcp_calls": "0 — no MCP GitHub tool called", "api_writes": "1 this round — this os-dev-report comment via the fleet-write relay (POST /repos/objectstack-ai/objectstack/issues/22310/comments, as one repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches, objectstack-fleet[bot]). No PR body PATCH and no label write this round. git push x1 (not REST). Reads: gh api GET only. Round 1 total was 3 (pr_create, PR assignee, report comment 6073361340).", "gates": [ { "command": "node scripts/check-adr-0087-registration.mjs --base origin/main", "exit": 0, "verdict": "✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)." }, { "command": "node scripts/check-adr-0087-registration.mjs --self-test", "exit": 0, "verdict": "✓ check-adr-0087-registration --self-test: 441 assertions over real temp git repos (real scan()/assertInputs() path)" }, { "command": "node scripts/check-changeset-no-major.mjs --base origin/main", "exit": 0, "verdict": "· no `pull_request` payload was available to read a declaration from" }, { "command": "node scripts/check-changeset-no-major.mjs --self-test", "exit": 0, "verdict": "✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #7005 diff scoping over real te" }, { "command": "node scripts/check-ci-filter-parity.mjs", "exit": 0, "verdict": "OK: all 20 build input(s) turbo.json declares outside the packages (itself, 2 globalDependencies, and every `$TURBO_ROOT$` input of a build Build Core runs) and all 11 script(s) the `build-core` job's" }, { "command": "node scripts/check-closing-keyword-parity.mjs", "exit": 0, "verdict": "• packages/spec/src/migrations/registry.ts -- 2379111 bytes exceeds the sweep's 2097152-byte cutoff for UNREGISTERED files" }, { "command": "node scripts/check-closing-keyword-parity.mjs --self-test", "exit": 0, "verdict": "✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red." }, { "command": "node scripts/check-comment-mask-adoption.mjs", "exit": 0, "verdict": "OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/**, all 14 recorded and every recorded row still reached (13 unconverted, 1 specimen). A new one reds here" }, { "command": "node scripts/check-comment-mask-adoption.mjs --self-test", "exit": 0, "verdict": "PASS check-comment-mask-adoption --self-test (0 failure(s))" }, { "command": "node scripts/check-comment-mask-corpus.mjs", "exit": 0, "verdict": "✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 8497 files, 0 disagree, 0 unparseable, 134.2s (comparator self-test: 26 cases pass)." }, { "command": "node scripts/check-doc-frontmatter.mjs", "exit": 0, "verdict": "✓ check-doc-frontmatter: 2 content root(s) verified, each against its own floor — content/docs 411, content/blog 4." }, { "command": "node scripts/check-doc-frontmatter.mjs --self-test", "exit": 0, "verdict": "✓ check-doc-frontmatter --self-test: 99 assertions — the card's own description observed failing with the parser's message and the FILE line, every other violation kind observed firing, five REFUSALS " }, { "command": "node scripts/check-doc-route-spelling.mjs --advisory", "exit": 0, "verdict": "✓ route-spelling guard (advisory): population clean — every shape-matched literal spells its ledger row." }, { "command": "node scripts/check-doc-route-spelling.mjs --self-test", "exit": 0, "verdict": "✓ check-doc-route-spelling self-test: extraction tidy-up, the variant relation (plural + pinned lexicon, no prefix heuristic), walk wiring (releases/ and node_modules/ out, both roots in), ledger pars" }, { "command": "node scripts/check-docs-section-name.mjs", "exit": 0, "verdict": "so it is carried by --self-test rather than by this corpus." }, { "command": "node scripts/check-docs-section-name.mjs --self-test", "exit": 0, "verdict": "✓ check-docs-section-name self-test: 85 cases pass (real temp trees on disk; both historical misses reproduced as RED, both arms driven RED, the duplicate-key and syntax-error boundaries pinned, every" }, { "command": "node scripts/check-dts-emitted.mjs --self-test", "exit": 0, "verdict": "check-dts-emitted self-test: all assertions passed." }, { "command": "node scripts/check-empty-changeset.mjs --base origin/main", "exit": 0, "verdict": "✓ No changeset from the merge base modified or deleted by this diff (#17712)." }, { "command": "node scripts/check-empty-changeset.mjs --self-test", "exit": 0, "verdict": "✓ check-empty-changeset --self-test: 170 assertions over real temp git repos (real scan() path)" }, { "command": "node scripts/check-issue-citations.mjs", "exit": 0, "verdict": "✅ check-issue-citations: every citation this change adds resolves (or is a declared cross-repo reference)." }, { "command": "node scripts/check-keyed-text-bounds.mjs", "exit": 0, "verdict": "⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide. Reproduce the record: see this file's header." }, { "command": "node scripts/check-keyed-text-bounds.mjs --self-test", "exit": 0, "verdict": "PASS check-keyed-text-bounds --self-test (0 failure(s))" }, { "command": "node scripts/check-platform-object-tenancy-census.mjs", "exit": 0, "verdict": "✓ platform-object tenancy census matches the tree: 83 platform-namespace objects, 48 in the machinery's reach, 35 outside it, every exclusion explained by a declaration on its own schema." }, { "command": "node scripts/check-platform-object-tenancy-census.mjs --self-test", "exit": 0, "verdict": "✓ check-platform-object-tenancy-census self-test: all checks pass (83 objects, 35 outside the machinery)" }, { "command": "node scripts/check-plugin-teardown-shape.mjs", "exit": 0, "verdict": "✓ check:plugin-teardown-shape: 69 Plugin implementation(s) across 7921 source(s) under packages/**; every teardown-shaped method (stop / shutdown / close / dispose) sits beside a real destroy() (0 kno" }, { "command": "node scripts/check-plugin-teardown-shape.mjs --self-test", "exit": 0, "verdict": "✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay green, every roster name reds, every excluded name sta" }, { "command": "node scripts/check-registry-log-declared.mjs", "exit": 0, "verdict": "examples/app-showcase — S1 constructs a SchemaRegistry in its tests" }, { "command": "node scripts/check-registry-log-declared.mjs --self-test", "exit": 0, "verdict": "self-test OK: 16 cases + level-vocabulary read + population declaration + real-tree selection floor." }, { "command": "node scripts/check-rest-log-spy-declared.mjs", "exit": 0, "verdict": "OK: 30 of 270 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level." }, { "command": "node scripts/check-rest-log-spy-declared.mjs --self-test", "exit": 0, "verdict": "check-rest-log-spy-declared self-test reached its verdict: 23 case(s), 0 failure(s)." }, { "command": "node scripts/check-section-landing-index.mjs", "exit": 0, "verdict": "✓ check-section-landing-index: 8 section index block(s) enumerate their meta.json pages, in order, both directions (ai, api, automation, data-modeling, kernel, permissions, plugins, ui); 27 landing pa" }, { "command": "node scripts/check-section-landing-index.mjs --self-test", "exit": 0, "verdict": "✓ check-section-landing-index --self-test: 31 assertions over synthetic inputs and a temp fixture (real judge()/run() path); every limb -- both shapes in sync, missing page, undeclared row, wrong orde" }, { "command": "node scripts/check-system-context-census.mjs", "exit": 0, "verdict": "check-system-context-census: OK — 119 elevation read sites in 20 packages across 55 files, living in 102 symbol(s); the page cites 115 symbol(s) against 115 required, over 138 anchors and 7 file-level" }, { "command": "node scripts/check-system-context-census.mjs --self-test", "exit": 0, "verdict": "check-system-context-census --self-test: all cases passed" }, { "command": "node scripts/check-undeclared-dep-imports.mjs", "exit": 0, "verdict": "⚠ The delta is information, not a verdict — the floors are `>=` and cannot see an upward drift at all, which is why it is PRINTED. Reproduce the record: see this file's header." }, { "command": "node scripts/check-undeclared-dep-imports.mjs --self-test", "exit": 0, "verdict": "PASS check-undeclared-dep-imports --self-test (0 failure(s))" }, { "command": "node scripts/docs-audit/check-affected-docs.mjs", "exit": 0, "verdict": "→ the unreachable rows themselves: this command with --json" }, { "command": "node scripts/docs-audit/check-drift-comment.mjs", "exit": 0, "verdict": "✓ check-drift-comment: 66 cases pass across 5 fixture diff(s)." }, { "command": "node scripts/pm/release-rehearsal-clone.mjs --self-test", "exit": 0, "verdict": "✓ self-test passed" }, { "command": "node scripts/release-pending-publish.mjs --self-test", "exit": 0, "verdict": "✓ release-pending-publish self-test: 92 cases across 22 batteries pass." }, { "command": "pnpm --filter @objectstack/lint run check:doc-formula-expressions", "exit": 0, "verdict": "#11673)." }, { "command": "pnpm --filter @objectstack/lint run check:doc-security-posture", "exit": 0, "verdict": "✅ 28 ObjectSchema.create example(s) in 230 marked block(s) across 254 prose file(s) in 2 root(s) carry an os validate-clean security posture" }, { "command": "pnpm --filter @objectstack/spec run check:docs", "exit": 0, "verdict": "✅ 225 generated files in sync with packages/spec" }, { "command": "pnpm --filter @objectstack/spec run check:duration-unit-keys", "exit": 0, "verdict": "✓ check:duration-unit-keys — 197 unit-declaring numeric key(s) across 2955 source file(s) all carry their unit in the key name (or in a sibling `unit`, or under a declared exemption: 6 declared durati" }, { "command": "pnpm --filter @objectstack/spec run check:empty-state", "exit": 0, "verdict": "✓ all classified (2 closed, 2 open, 4 output, 9 scope)" }, { "command": "pnpm --filter @objectstack/spec run check:liveness", "exit": 0, "verdict": "(not a completeness claim about the 526 child key(s) under the declared blanket verdicts above — those are recorded, not classified.)" }, { "command": "pnpm --filter @objectstack/spec run check:skill-examples", "exit": 0, "verdict": "first run exit 3 PREREQUISITE NOT MET (client-react unbuilt); after the full build, re-run exit 0: ✅ 262 prose examples type-check across 3 surface(s)" }, { "command": "pnpm --filter @objectstack/spec run check:strictness-ledger", "exit": 0, "verdict": "untriaged: 1203 object site(s) across 9 director(ies)" }, { "command": "pnpm --filter @objectstack/spec run check:variant-docs", "exit": 0, "verdict": "✓ variant/doc gate: 18 discriminated union(s) — 8 governed (every variant mentioned in a bound doc), 10 exempt." }, { "command": "pnpm --filter @objectstack/spec run check:yaml-examples", "exit": 0, "verdict": "↳ 18 component node(s) also judged against their ComponentPropsMap props schema; 1 skipped (no row for the type — SDUI blocks and custom.* are an open namespace)" }, { "command": "pnpm check:changeset-gate-self-tests", "exit": 0, "verdict": "✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #7005 diff scoping over real te" }, { "command": "pnpm check:corpus-claim-drift", "exit": 0, "verdict": "Ledger: 2 baselined file(s) in scripts/corpus-claim-drift-baseline.json." }, { "command": "pnpm check:cross-package-test-inputs", "exit": 0, "verdict": "OK: 30 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split \"test:repo\" task); 13 walked root(s) judged, 3 on ACCEPTED_WALK_RADII; 2884 tes" }, { "command": "pnpm check:dispatcher-error-vocabulary", "exit": 0, "verdict": "[#15723] the ARGUMENT POSITION of new APIError( … ) and APIError.from( … ) IS now in this gate's population — the `apierrorarg` shape. `APIError.from` copies the record's `code` onto the body, so the " }, { "command": "pnpm check:doc-anchors", "exit": 0, "verdict": "✅ check-doc-anchors: 468 internal #fragment link(s) across 417 source file(s) all resolve to a real heading" }, { "command": "pnpm check:doc-authoring", "exit": 0, "verdict": "✓ doc authoring guard: sibling-package prose ids hold the baseline — 0 pinned site(s) across 0 file(s), 89453 string(s) read in 1283 parsed source(s) (seen floor 40000 strings / 600 sources), no growt" }, { "command": "pnpm check:docs-audit-scope", "exit": 0, "verdict": "✓ scope injection is live: the workflow audits the list handed in as args.handwritten, and refuses an invocation that hands in no scope at all." }, { "command": "pnpm check:docs-redirects", "exit": 0, "verdict": "check-docs-redirects: OK (apps/docs/redirects.mjs: 98 entries -- 95 page destination(s) resolved against content/docs, 3 wildcard destination(s) resolved to a directory, 0 outside the /docs route spac" }, { "command": "pnpm check:docs-single-h1", "exit": 0, "verdict": "✓ check-docs-single-h1: 411 page(s) under content/docs/ carry no body-level `# ` heading (0 subtree(s) excluded, see --list)." }, { "command": "pnpm check:docs-spec-enumerations", "exit": 0, "verdict": "OK the hand-written spec enumerations agree with packages/spec/package.json -- 18 subpath(s) held ORDERED in content/docs/deployment/troubleshooting.mdx; 15 protocol namespace(s) [Data, System, Kernel" }, { "command": "pnpm check:docs-transcript-drift", "exit": 0, "verdict": "✓ check-docs-transcript-drift: 4 declared transcript value(s) across 411 page(s) under content/docs/ equal what the registry derives today, and no undeclared block quotes one." }, { "command": "pnpm check:driver-memory-census", "exit": 0, "verdict": "check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states \"#6664 census: 2 ruled consumers\". This gate polices the census, never investmen" }, { "command": "pnpm check:dts-closure", "exit": 0, "verdict": "check-dts-closure: 63 built package(s) swept - 161/161 declared declaration file(s) present across 63 package(s); 0 built package(s) declare no declaration entry point and owe none." }, { "command": "pnpm check:dual-build-cjs-loads", "exit": 0, "verdict": "first run exit 3 PREREQUISITE NOT MET (no full build); after `pnpm turbo run build --filter='!@objectstack/docs'` (72 tasks, 71 cached), re-run exit 0: provenance — entries/packages/cjsFiles/probes: this run 107/66/717/1 · floors 90/58/520/1" }, { "command": "pnpm check:engine-double-contract", "exit": 0, "verdict": "⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide." }, { "command": "pnpm check:error-status-conformance", "exit": 0, "verdict": "✓ every derivable runtime status is documented, and every documented status is reachable." }, { "command": "pnpm check:gitlink-declared", "exit": 0, "verdict": "check-gitlink-declared: OK (10381 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declare)." }, { "command": "pnpm check:issue-citations", "exit": 0, "verdict": "✅ check-issue-citations --self-test: grammar narrowed, every spelling enumerated, qualifier a closed set of repositories, four 404 causes kept apart, both board strategies agree, diff scope red AND gr" }, { "command": "pnpm check:lean-entry-closure", "exit": 0, "verdict": "Admitted set held exactly (15 packages); 6 denied names absent." }, { "command": "pnpm check:logger-receiver-detach", "exit": 0, "verdict": "control corpus fired on all five detach shapes in this same run, and stayed silent on the measured `console` and options-callback populations -- so the zero above is a reading." }, { "command": "pnpm check:merge-driver", "exit": 0, "verdict": "✓ check-regen-pending self-test passed." }, { "command": "pnpm check:nul-bytes", "exit": 0, "verdict": "check-nul-bytes: OK (scanned 10372 text file(s) -- 10372 tracked, 0 untracked-not-ignored; skipped 9 binary; no raw ASCII control bytes)." }, { "command": "pnpm check:objectql-double-limit", "exit": 0, "verdict": "baseline key set verified against 117d34d: no files added." }, { "command": "pnpm check:objectui-changeset", "exit": 0, "verdict": "✓ objectui-range --self-test: all checks passed" }, { "command": "pnpm check:org-identifier", "exit": 0, "verdict": "check-org-identifier: OK (3256 author-facing source file(s), 18 session binding(s) resolved, no removed session.tenantId alias)." }, { "command": "pnpm check:page-declaration-shape", "exit": 0, "verdict": "blind spot: 1 computed carrier(s) no source scan can enumerate — examples/app-crm/objectstack.config.ts:86." }, { "command": "pnpm check:pm-changeset-deadline-census", "exit": 0, "verdict": "✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a reading, the verdict truth table including the inconclusive" }, { "command": "pnpm check:published-files", "exit": 0, "verdict": "✓ check:published-files — 69 publishable package(s) of 80 workspace member(s) declare a `files` whitelist that covers every entry point plus CHANGELOG.md and admits no test, test-harness config or bui" }, { "command": "pnpm check:published-readme-links", "exit": 0, "verdict": "✓ check:published-readme-links — 222 outbound link(s) across 101 published markdown file(s): 0 root-relative, 0 non-canonical origin(s), 27 docs-site page(s) resolved (0 via redirect), 1 anchor(s) ver" }, { "command": "pnpm check:query-options-erasure", "exit": 0, "verdict": "baseline key set verified against 117d34d: no files added." }, { "command": "pnpm check:react-page-adapter-contract", "exit": 0, "verdict": "✓ check-react-page-adapter-contract: 21 app-showcase page module(s) + 1 content/docs react-page sample(s) (from 394 doc file(s), 2002 fenced block(s)) — every adapter query option is $-prefixed, every" }, { "command": "pnpm check:refd-timer-probe", "exit": 0, "verdict": "1 code site(s), all inside the approved module, which is present and still reads it." }, { "command": "pnpm check:role-word", "exit": 0, "verdict": "Ledger: 44 baselined file(s) still carrying it (117 occurrence(s)) in scripts/role-word-baseline.json." }, { "command": "pnpm check:route-envelope", "exit": 0, "verdict": "read/write discriminator: 11 file(s) skipped as fetch readers (77 zero-argument `res.json()` call(s), none swept in)" }, { "command": "pnpm check:skill-identifier-liveness", "exit": 0, "verdict": "check-skill-identifier-liveness OK — Leg 1: 457 citation(s) over 53 published file(s) checked against 122789 implementation word tokens (0 ledgered exemption(s)); Leg 2: 8 registered exhaustive sectio" }, { "command": "pnpm check:slot-lookup", "exit": 0, "verdict": "baseline key set verified against 117d34d: no files added." }, { "command": "pnpm check:sourcemap-no-sources-content", "exit": 0, "verdict": "check-sourcemap-no-sources-content: 60 built package(s) swept - 528 map(s), none embed source text." }, { "command": "pnpm check:test-source-alias", "exit": 0, "verdict": "check-test-source-alias OK — 73 packages with tests scanned; 60 registered as still resolving a workspace dep through `dist/`; 53 published subpath(s) resolved through every alias table." }, { "command": "pnpm check:tier-file-adoption", "exit": 0, "verdict": "@objectstack/cli — 82 file(s); imports readTierMode, selectTierFiles from scripts/nightly-tiers.mjs (via packages/cli/vitest-tiers.ts)" }, { "command": "pnpm check:type-check-coverage", "exit": 0, "verdict": "⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide." }, { "command": "pnpm check:type-check-debt", "exit": 0, "verdict": "surplus: none — every entry sits exactly at its measurement, so any new error is red." }, { "command": "pnpm check:vendor-version-stamps", "exit": 0, "verdict": "attestations. Re-verify one and you may restamp it; otherwise it stays a historical fact." }, { "command": "pnpm check:watch-hint-literal", "exit": 0, "verdict": "✓ check-watch-hint-literal: 72 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 48, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECLARED_WATCH_HINTS 8 -- every one an array of quoted" }, { "command": "pnpm check:where-matcher", "exit": 0, "verdict": "baseline key set verified against 117d34d: no files added." }, { "command": "node scripts/check-system-context-census.mjs (after pnpm gen:system-context-census)", "exit": 0, "verdict": "check-system-context-census: OK — 119 elevation read sites in 20 packages across 55 files, living in 102 symbol(s); the page cites 115 symbol(s) against 115 required, over 138 anchors" }, { "command": "pnpm lint (full: eslint . --no-inline-config) at 799c28789", "exit": 0, "verdict": "exit 0, no findings printed" }, { "command": "pnpm --filter @objectstack/dogfood typecheck", "exit": 0, "verdict": "tsc --noEmit exit 0; --listFiles shows the 3 edited dogfood files are in the program (3 hits)" }, { "command": "dogfood: vitest run flow-runas, schedule-acting-organization, flow-runas-schedule (verbose, at 799c28789)", "exit": 0, "verdict": "Test Files 3 passed (3); Tests 27 passed (27)" }, { "command": "dogfood: vitest run every grep-hit dogfood file (flow-runas, schedule-acting-organization, flow-runas-schedule, automation-toggle-tenant-scope, flow-durable-suspend, flow-function-effect, flow-node, showcase-anonymous-deny-surfaces, showcase-declarative-mcp) + authz-conformance.test.ts", "exit": 0, "verdict": "Test Files 10 passed (10); Tests 168 passed (168)" }, { "command": "runtime: vitest run --project local, the 42 grep-hit runtime test files", "exit": 0, "verdict": "Test Files 42 passed (42); Tests 1026 passed (1026)" }, { "command": "verify: vitest run, the 7 grep-hit verify test files", "exit": 0, "verdict": "Test Files 7 passed (7); Tests 57 passed (57)" }, { "command": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran (exit codes recorded) at 799c28789", "exit": 0, "verdict": "Run reconciliation — 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero). Derivation warns STALE TREE: 15 commits behind origin/main; the one changed family file is scripts/adr-anchors/packages__objectql__src__security-catalog-namespace.ts.json, unrelated to this diff; main not merged (CI on 799c28789 runs the merge ref)" } ], "line_budget": "none set; branch total +787 / -25 over 8 files. This round: system-context.mdx +8/-8, flow-runas-fixture.ts +57/-1, flow-runas.dogfood.test.ts +56/-3, schedule-acting-organization.dogfood.test.ts +67/-11", "files_changed": [ ".changeset/22310-elevated-flow-trigger-door.md", "content/docs/permissions/system-context.mdx", "packages/qa/dogfood/test/fixtures/flow-runas-fixture.ts", "packages/qa/dogfood/test/flow-runas.dogfood.test.ts", "packages/qa/dogfood/test/schedule-acting-organization.dogfood.test.ts", "packages/runtime/src/domains/automation-trigger-elevated-door.test.ts", "packages/runtime/src/domains/automation.ts", "packages/verify/src/automation-trigger-elevated-door.test.ts" ], "deviations": [ "Round 1 removed the worktree after the PR opened, as the role file says. This round re-created it on the existing local branch (sha equal to origin, 0e92dbe4), re-installed and rebuilt the closure. Nothing was lost.", "Control B uses a session and a screen-typed door twin, not the system principal the seat suggested: the system principal reaches the door only in-process through @objectstack/runtime's HttpDispatcher, and @objectstack/dogfood does not depend on runtime (its package.json is outside the surface, and check:undeclared-dep-imports would red). The subject was kept, and the reason is recorded in the test's docblock.", "origin/main was not merged: the branch is 15 commits behind, and none of them touches the 8 files of this diff. The seat reported CI green on 799c28789's merge ref. dispatch-gates flags the tree STALE over one unrelated family file (an objectql adr-anchor JSON).", "PR body not patched (seat's instruction 4). The replacement text is in pr_body_replacements, keyed by section." ], "pr_body_replacements": { "callout (replace the blockquote that begins with 'Draft, and knowingly incomplete.')": "> **Draft.** The two required checks the first round left red are now addressed on this head (patch round 1, with the claim's file surface extended by the seat): the census row and the ruling's pin sweep. The contract-tier review is still owed before the queue.", "What changed (append these two bullets after 'No spec key moves...')": "- `content/docs/permissions/system-context.mdx`: the automation-domain row anchors `#refusesElevatedSelfTriggeredStart` and names the new bypass (the in-process system principal starting an elevated self-triggered flow at the trigger door). The declared counts are regenerated by `pnpm gen:system-context-census` (118 to 119 read sites). No other row is touched.\n- The ruling's pin sweep, repo-wide (see **The pin sweep** below): the only pins that asserted a non-system caller starting an elevated self-triggered flow at the trigger door are 3 dogfood tests in 2 files, and each now asserts the ruled door.", "Pins (append this bullet)": "- `packages/qa/dogfood/test/flow-runas.dogfood.test.ts` with `fixtures/flow-runas-fixture.ts`, and `packages/qa/dogfood/test/schedule-acting-organization.dogfood.test.ts`: the flipped pins (see The pin sweep).", "Tests and gates (replace the whole section, heading included)": "## Tests and gates (head `799c28789`)\n\n- `pnpm --filter @objectstack/runtime test` at `0e92dbe4` (runtime source unchanged since): 342 files, 4815 passed, 19 skipped. At `799c28789`, the 42 runtime test files that drive the trigger door: 1026 passed.\n- `pnpm --filter @objectstack/runtime typecheck` and `pnpm --filter @objectstack/verify typecheck`: exit 0. `pnpm --filter @objectstack/dogfood typecheck`: exit 0, with the 3 edited dogfood files in the program.\n- `pnpm --filter @objectstack/verify test` at `0e92dbe4`: 20 files, 154 passed. At `799c28789`, the 7 verify files that drive the door: 57 passed.\n- Dogfood at `799c28789`: every dogfood file that drives the trigger door (9) plus `authz-conformance.test.ts`: 10 files, 168 passed.\n- `pnpm lint` (the full run): exit 0 at `799c28789`.\n- `node scripts/check-system-context-census.mjs`: OK, 119 read sites, 115 of 115 required symbols cited.\n- The dispatch-derived gate list, re-derived with no paths at `799c28789` (94 families): all 94 exit 0. `pnpm check:dual-build-cjs-loads` and `check:skill-examples` first answered `PREREQUISITE NOT MET` and went green after a full build. `dispatch-gates --ran`: 94 derived, 94 run, 0 NOT MEASURED.", "Not in this PR (replace the whole section, heading included)": "## The pin sweep\n\nA repo-wide search over every test that reaches the trigger door (`/trigger` in either spelling, `flows.run`, `handleAutomation`, `automation.trigger`: 74 files across `packages/runtime`, `packages/verify`, `packages/qa/dogfood`, `packages/client`, `packages/services/service-automation`, `packages/spec`, `packages/lint` and `packages/objectql`, with no hits in `examples/` or `apps/`) found exactly 3 pins whose meaning the ruling reverses: a non-system caller starting a `runAs: 'system'` flow of a self-triggered type through the door. The refusal's code and message appear nowhere else. Each flipped pin asserts the new meaning's substance, and each test keeps its subject.\n\n- `flow-runas.dogfood.test.ts` (the authz-matrix proof `flow-run-as`): the two elevation legs now reach each system flow through a `runAs: 'user'` parent whose `subflow` node calls it (`runas_system_touch_via_parent`, `runas_system_read_via_parent`, added to the fixture). The write leg still stamps the admin's note `touched-system`. The read leg now also asserts that the row read is the note it asked for. A new case pins the door: the member's direct start of either system flow answers `403 PERMISSION_DENIED`, with no inner `data`, the note left at `new` and no run recorded.\n- `schedule-acting-organization.dogfood.test.ts`, control B on sqlite-wasm (subject: which organization a door-launched run carries, the session's and never the declaration's): the session now drives the declaring flow's door twin. The twin has the same nodes and the same `organization` declaration, `type: 'screen'` and no cadence, and is derived from the same builder. The delivered row still carries the session's organization and never the declared one. Before that, the control pins that the declaring flow itself is refused to the session: `403 PERMISSION_DENIED`, no run recorded, nothing delivered. The memory-driver branch is unchanged. The system principal could not be used here: it is reachable only in-process through `@objectstack/runtime`'s `HttpDispatcher`, which `@objectstack/dogfood` does not depend on, so the session-and-twin shape keeps the subject without a new dependency.\n\n**Ablation of the door check against the flipped pins** (the same call-site mutation, marker present in `dist/index.js` and `dist/index.cjs`): exactly the 2 refusal pins went red (the direct-start case in `flow-runas`, and control B on sqlite-wasm). The 22 other tests in the two files stayed green, including the parent-route elevation legs and control B's twin delivery. Restore: the blob equals HEAD, `--absent` reports the marker gone from all 6 built files, and the tree is clean. All 27 tests across the three files pass after the restore.", "Acceptance notes (replace the last bullet, the one beginning 'Two other doors start a flow by name')": "- **Two other doors start a flow by name and were measured, not changed** (mechanism hypothesis H6, outside this ruling): an action of `type: 'flow'` served at `POST /api/v1/actions/:object/:action`, and a declared endpoint of `type: 'flow'` with `authRequired: true`. On this branch, a member with no grant on the target object still starts an elevated self-triggered flow through both: the action door with an `autolaunched` and a `schedule` target, and the endpoint door with an `autolaunched` target. Each answered 200 and the elevated write landed. An anonymous request at the endpoint answers 401. The seat filed this as #22407, and it is not decided here." }, "open_questions": [], "out_of_scope_findings": [ "carrier: the owner of packages/qa/dogfood/test/authz-conformance.matrix.ts (outside this claim's surface) · row 'anonymous-deny-automation', `enforcement` prose: it says 'the execution doors (trigger / execute) sit outside all of them' (the per-route capability predicates). Since this PR, the trigger door carries its own caller × flow check. Suggested replacement for that clause: 'the execution doors (trigger / execute) sit outside all of them, except that the trigger door refuses a non-system caller a flow declared runAs system of a self-triggered type (refusesElevatedSelfTriggeredStart, 403 PERMISSION_DENIED)'. Prose only: authz-conformance.test.ts passes (10 files / 168 tests run) · noted, not filed", "carrier: PR #22404 acceptance notes · the action door and the declared flow endpoint (H6) are filed by the seat as #22407 and not touched here", "carrier: PR #22404 acceptance notes · console flow-action buttons go through the trigger door, so admins are refused elevated self-triggered targets too (ruled). getFlow is optional on IAutomationService. Both are unchanged from round 1 · noted, not filed" ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsACCEPT: PR #22404 (head
799c28789), ruling B as ruleddomain:cliseat (#6024) ·session_01BmsuLyUeuG5CNpZFMH1jzS· 2026-10-09T04:18Z. Reviewed against GitHub, not against the report. ⛔ Class level, per the ruling's disclosure term.-
PR shape: draft, base
main; the first line isFixes #22310, the only closing keyword in the body, and the body carries a line-initialClause-②: no. Eight files,+787 / -25, all inside the claim's surface as amended for patch round 1 (6072106844):automation.ts;- a runtime door test;
- a verify wire test;
- the census page;
- the three dogfood files;
.changeset/22310-elevated-flow-trigger-door.md(@objectstack/runtimepatch).
No
packages/spec, no governed path, nothing unrelated. -
The door check, read line by line:
refusesElevatedSelfTriggeredStartsits after the existence check (an unknown name keeps its404) and beforeexecute(a refused start dispatches nothing).- It refuses exactly when the caller is not
executionContext.isSystem, the flow declaresrunAs: 'system', and itstypeisautolaunched,record_changeorschedule. - That type set is compile-bound to the spec's
Flow.typeenum. - The declaration is read through the service's own
getFlow, the probeflowIsUnknownalready uses. No second loader exists, and elevation stays the engine's. - The refusal is
403PERMISSION_DENIED(an existing code, the one this domain's other permission refusals answer). Its message names nothing of the flow.
- It refuses exactly when the caller is not
-
Ruling and triage reading met: the three self-triggered types are refused at the door and at its in-process twin (
flows.run).screenandapiflows are unchanged. A parent'ssubflownode (engine-sideengine.execute, never the door) and the system principal still run. No spec key moves. -
Owed work met:
- The measure-first before-table (
cd46ffa4, through the real kernel) shows every unmeasured type was open to a member before the fix. - Each refused type is pinned with
code+status+ "never dispatched". - There is a positive control for each unchanged path.
- The card's own ablation reddens exactly the refusal and envelope pins.
- The measure-first before-table (
-
Patch round 1 (the ruling's pin sweep, which my dispatch order omitted, a seat error):
- a repo-wide search of 74 door-driving test files found exactly 3 pins in 2 dogfood files asserting the pre-ruling door; all 3 are flipped with weight, and each test keeps its subject;
- the census row is anchored and its counts are regenerated (118 → 119), and
check-system-context-censusis OK.
-
Disclosure: the diff and PR body hold 0 hits for any downstream app's flow, object or field name.
-
CI on
799c28789: 33 success, 2 skipped, 0 failed (read at this ACCEPT). -
Correction, on my own claim: the claim said a contract-tier review was owed before enqueue. Under the contract-review reference it is not owed: this PR declares
Clause-②: noand touches neitherpackages/specnor governed text, so the review of record is this ACCEPT plus CI. A runtime permission change is not Clause ② (execution-duties.md); its human floor is the maintainer's ruling B, which this PR implements. -
Accepted deviations:
- schedule-acting-organization's control B keeps its subject through a
screen-typed door twin, because the system principal is unreachable from@objectstack/dogfoodwithout a new dependency; getFlowstays optional: an implementation without it dispatches as before, as the existence check does, and the platform engine always implements it;- a platform admin's session is refused too, as ruled.
- schedule-acting-organization's control B keeps its subject through a
-
Out of scope:
filed #22407: the two other doors that start a flow by name. Triage graded it p0securityneeds-user-decision, and the maintainer's letter is pending.Acceptance notes: the console's flow-action buttons start flows through this door, so an elevated self-triggered target answers403there for every user. That is ruling B's own outcome; triage notes that under [finding] runtime(security): two other doors that start a flow by name still let any signed-in member start a self-triggered system flow, after #22310 closes the trigger door #22407's letter A it holds at every door.Acceptance notes: theflows.mdxstatus table gains a403row best once [finding] runtime(security): two other doors that start a flow by name still let any signed-in member start a self-triggered system flow, after #22310 closes the trigger door #22407 settles; today it lists dispatch outcomes, and the door's caller refusals sit outside it (carrier: [finding] runtime(security): two other doors that start a flow by name still let any signed-in member start a self-triggered system flow, after #22310 closes the trigger door #22407's PR).Acceptance notes:authz-conformance.matrix.ts's prose (carrier: the next PR to edit that matrix).
-
PR body: the seat applied the dev's replacement text, written once by the dev, and read it back byte-identical (14,247 bytes).
-
Next: a fresh
merge-treeagainstmain, then ready and auto-merge into the queue.
Generated by Claude Code
-
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsLanded: PR #22404 →
ca135dcc40, a single-parent queue squashdomain:cliseat (#6024) ·session_01BmsuLyUeuG5CNpZFMH1jzS· 2026-10-09T04:50Z. ⛔ Class level, per the ruling's disclosure term.- Landing shape:
ca135dcc40has one parent (git rev-list --parents -n 1gives 2 fields) and is an ancestor oforigin/main. It entered the merge queue at 2026-10-09T04:27Z and merged at 2026-10-09T04:49Z, on that first entry. - Content on
origin/main:packages/runtime/src/domains/automation.tscarriesrefusesElevatedSelfTriggeredStart(4 hits at the landed commit). The runtime door test, the verify wire test, the flipped dogfood pins, the census row, and.changeset/22310-elevated-flow-trigger-door.md(@objectstack/runtimepatch) are present. - Review of record: ACCEPT
6074173738at799c28789, after patch round 1,Clause-②: no. No contract-tier review was owed (corrected in that ACCEPT). All 35 checks were green on the head before the ready flip. - What is now true: at the trigger door and its in-process twin, a caller that is not the system principal cannot start a
runAs: 'system'flow of a self-triggered type. It gets403 PERMISSION_DENIED, and nothing runs. - State: the card closed
completedthroughFixes #22310;pm:dispatchedis stripped in this act. - Follow-up in flight: [finding] runtime(security): two other doors that start a flow by name still let any signed-in member start a self-triggered system flow, after #22310 closes the trigger door #22407 (ruling A
6074046403) extends this same check to the two other doors that start a flow by name. Its dev builds on this landed predicate. - Unblocks downstream: ruling B (
6070744023) recorded that hotcrm#2016 unblocks on this landing; the release that carries@objectstack/runtimewith this changeset is the pin it waits for.
Generated by Claude Code
- Landing shape:
- added a commit that references this issue
on Oct 9, 2026
Ruled: 6070744023 · letter B · 2026-10-08T23:08Z
Filing gate: ① product defect with reach measured. Class (a), security: privilege escalation. reach: the public door
POST /api/v1/automation/:name/trigger, measured once with a wrong result on@objectstack/*17.7.0. It was driven in process through@objectstack/verify'sflows.run, which answers through the samerespondToFlowTrigger.Who acts on it: the objectstack triage seat routes it; the fix lands in
packages/runtime(src/domains/automation.ts) and, if a declaration is the answer,packages/spec(Flow). Found by the dev of objectstack-ai/hotcrm#2016 (sessionsession_012zh91QzFgePbkmuHnugLN3); therepo:hotcrmseat confirmed the code path. ⛔ Not a claim. hotcrm WAITs for it (hotcrm AGENTS.md §2).What happens
respondToFlowTrigger(packages/runtime/src/domains/automation.ts, from:1790at 17.7.0, the same onmain6729e107) checks that the flow exists and then callsautomationService.execute(flowName, …). The only caller check on the route isshouldDenyAnonymous. Nothing asks whether THIS caller may start THIS flow, and a flow declaredrunAs: 'system'then runs every node elevated. The spec'sFlowhas no key that keeps a flow off this door, for example "callable only as a sub-flow" or "only these permission sets may trigger", and flow templates expose no parent-run token a sub-flow could check.Measured (hotcrm
9451b6de, 17.7.0)hooks.run('crm_case', 'update', …)→PERMISSION_DENIED;flows.run('case_escalation_stamp', { recordId })→ ok, and the case now storesis_escalated: true.case_escalation_stampis a shipped hotcrm flow (src/service/flows/case-escalation-stamp.flow.ts) declaredrunAs: 'system'.runAs: 'system'autolaunched sub-flow (the AGENTS.md rule-9 pattern: "a write that genuinely needs elevation is a dedicatedsystemsub-flow called through asubflownode"):crm_accountinsert (PERMISSION_DENIED);crm_accountwithannual_revenue: 999, owned by that member.Reach in a real app: hotcrm ships 24 flows declaring
runAs: 'system'(record-change, schedule, screen and autolaunched). Two cases were measured: the autolaunched stamp and the prototype sub-flow. Whether every other type is startable through the door the same way is NOT MEASURED. The platform's own guidance tells apps to put elevated writes in exactly these sub-flows, so the guidance itself produces doors every member can open.Consequence for apps: hotcrm#2016 (a sales rep's lead conversion fails on a field the rep may not edit) cannot take the rule-9 fix without publishing a new elevated create door. It waits on this card.
Acceptance
runAs: 'system'flow cannot be started through the trigger door by a caller the flow does not admit.code+status).subflownode calling the flow from its parent still works.Duplicate check
gh searchis refused in this container (GraphQL and REST search answer 403). So all objectstack issues were listed (/issues?state=all; the index runs to #22292, and every issue opened since 12:30Z today was read by title) and matched case-insensitively:trigger runAs system: 23, all closed. [security] a flow's inbound-hook secret (config.secreton the start node) is served in cleartext by the flow-definition read; after #20529 every armed hook carries one #20552 (inbound-hook secret), finding: the flow and hook readonly rules still carry the superseded "INSERT is engine-exempt" premise as a scan gap — a non-system create_record / ctx.api.insert of a readonly column is now a silent no-op nothing reports at build time #15394, runtime: execute the declarative row-leveloperation: 'update'action — the platform action route performs one data-plane update of the current record as the caller (runtime half of #14092) #15079 and finding(skills):skills/objectstack-automationdocumentsrunAsfor flows only — a hook-siderunAsparagraph is owed once PR #14915 lands #14966 are different.automation trigger elevation: 9, closed, none this.runAs system trigger door: 3, closed (runtime: execute the declarative row-leveloperation: 'update'action — the platform action route performs one data-plane update of the current record as the caller (runtime half of #14092) #15079, finding: aflow-type action's AutomationContext gets the same stampedrecordstub when the caller cannot read the row — and the flow face has norecordLoadDenied#14244, Epic: hotcrm as a single-DB multi-org SaaS (isolated posture) — tenant customization surface + tenant readiness #12701), none this.subflow-only: 0.flow-level permission: 0.None is this defect. Related: #14010 (hooks gained
runAs, closed).Generated by Claude Code