Skip to content

docs(spec): the runAs description names a system run's refused identity-assignment writes, with its ADR-0087 D3 entry - #25564

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-7mlrrw
Oct 24, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-7mlrrw

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Closes #25554
Part of #25251

Clause-②: no (describe text and ADR registration only; no accept-set change)
Metadata-protocol: semantic (records the change of meaning #25483 landed)

This is PR2 of ruling 6354966423 (letter B) on #25251, the spec-lane half. PR1, #25483, made the data middleware refuse a runAs: 'system' flow's governed writes to the four identity-assignment objects. The published FlowSchema.runAs description still told authors that a system flow keeps system privileges without exception. Under ADR-0087 this PR has to ship in the same release as #25483.

What changed

  • packages/spec/src/automation/flow.zod.ts, the runAs describe. One new sentence: elevation does not reach identity assignment. A system run's governed writes (insert, update, delete, transfer, restore, purge) to sys_user_position, sys_user_permission_set, sys_invitation and sys_member are refused with PERMISSION_DENIED. Declare user for position holdings and permission-set grants, so the ADR-0090 D12 delegated-administration gate judges the user the run acts for. Change invitations and memberships through the organization endpoints.
  • The comment block above runAs. The same rule stated exactly as feat(plugin-security)!: a flow that runs as system writes no identity assignment #25483 enforces it: refused ahead of the isSystem bypass for a context carrying isSystem AND a flowRunId, 403, nothing written. Reads of the four objects and writes to every other object are unchanged, and so is a platform writer whose system context carries no flow run.
  • ADR-0087 D3 semantic entry flow-run-as-system-identity-assignment-write-refused (entries/semantic/18.…), the entry feat(plugin-security)!: a flow that runs as system writes no identity assignment #25483's changeset left to this PR. It has no D2 conversion: changing runAs changes what every other node of the flow may do, and an endpoint call is not a rewrite of a node.
  • Derived pages regenerated with gen:docs (content/docs/references/automation/flow.mdx, content/docs/references/api/automation-api.mdx). gen:skill-refs and gen:skill-docs produced no change. registry.ts, spec-changes.json and the upgrade guide are generated and git-ignored, so nothing is committed for them.
  • Changeset 25554-runas-identity-assignment-wording.md, @objectstack/spec minor, with the registered ADR-0087 disposition marker.

Acceptance notes

Verification

At head 5a9bf414cf:

  • node scripts/pm/dispatch-gates.mjs --commands derived 109 families for this diff. 108 exit 0. pnpm check:dual-build-cjs-loads is NOT MEASURED locally (exit 3: it needs every workspace package built), and CI measures it.
  • pnpm --filter @objectstack/spec build green. vitest run src/migrations src/automation/flow in @objectstack/spec passed 25 files and 1122 tests.
  • check:spec-changes, check:upgrade-guide, check:docs, check:skill-refs, check-adr-0087-registration --base origin/main and check-changeset-no-major --base origin/main are green.

🤖 Generated with Claude Code

https://claude.ai/code/session_013yig39ZouqWBxABRfM5FN4


Generated by Claude Code

…tity-assignment writes, with its ADR-0087 D3 entry

The flow runAs describe and its source comment say that a system run's
governed writes to sys_user_position, sys_user_permission_set,
sys_invitation and sys_member are refused with PERMISSION_DENIED, and name
the remedies. A protocol-18 semantic entry records the change of meaning.
Derived reference pages regenerated.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yig39ZouqWBxABRfM5FN4
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tooling labels Oct 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 4 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via FlowSchema (symbol, a top-level const))
  • content/docs/permissions/authentication.mdx (via /api/v1/auth/organization/invite-member (route, a path literal in replacement))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via FlowSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-4.mdx (via FlowSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-7.mdx (via FlowSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 041a120b6189e9b174a3f4b8cf6202f22ace2bbd → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 74bc6aa80f1c163ab83ef1fedc75dc67e730d643 — the merge of head 5a9bf414cf27e307083bde55bd0f66884398d2a8 into base 041a120b6189e9b174a3f4b8cf6202f22ace2bbd, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 74bc6aa80f1c163ab83ef1fedc75dc67e730d643 && git checkout 74bc6aa80f1c163ab83ef1fedc75dc67e730d643
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 041a120b6189e9b174a3f4b8cf6202f22ace2bbd 5a9bf414cf27e307083bde55bd0f66884398d2a8 && git checkout -B drift-repro 041a120b6189e9b174a3f4b8cf6202f22ace2bbd && git merge --no-ff 5a9bf414cf27e307083bde55bd0f66884398d2a8

node scripts/docs-audit/affected-docs.mjs --json 041a120b6189e9b174a3f4b8cf6202f22ace2bbd

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 041a120b6189e9b174a3f4b8cf6202f22ace2bbd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5a9bf414cf27e307083bde55bd0f66884398d2a8
Local-runs: none
Scope: packages/spec/src/automation/flow.zod.ts, packages/spec/src/migrations/entries/semantic/18.flow-run-as-system-identity-assignment-write-refused.ts + packages/plugins/plugin-security/src/flow-system-identity-write-refusal.ts, packages/plugins/plugin-security/src/security-plugin.ts, packages/plugins/plugin-security/src/delegated-admin-gate.ts, packages/plugins/plugin-security/src/errors.ts, packages/services/service-automation/src/runtime-identity.ts (all on origin/main as #25483 left them), packages/spec/src/migrations/types.ts, packages/spec/src/migrations/entries/README.md, packages/spec/src/migrations/registry.ts.template, .changeset/25554-runas-identity-assignment-wording.md, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs

Record rendered at 2026-10-24T01:00Z against card #25554 (body and both comments), PR #25564 (body, five files, net diff against origin/main), ruling 6354966423 and the six other comments on #25251, and the check-runs on the head. The remainder of the diff (content/docs/references/api/automation-api.mdx, content/docs/references/automation/flow.mdx: regenerated projections of the describe) is the lane seat's ACCEPT at TIER_DEFAULT.

① Derived judgments

J1 — the describe sentence states what #25483 enforces, no more and no less: RIGHT. Compared clause by clause against the merged code on main:

  • Predicate. assertFlowSystemIdentityWriteAllowed returns unless context.isSystem AND context.flowRunId are both truthy; resolveRunDataContext hands a runAs: 'system' run's data nodes { isSystem: true, actor, flowRunId, … }. The describe scopes the refusal to "a system run's governed writes"; the source comment above the key spells the predicate (isSystem AND a flowRunId, ahead of the isSystem bypass). Matches.
  • Objects. FLOW_SYSTEM_IDENTITY_WRITE_OBJECTS = sys_user_position, sys_user_permission_set, sys_invitation, sys_member. The describe names the same four. Matches.
  • Verbs. The refusal reads GOVERNED_OPERATIONS exported from delegated-admin-gate.ts = insert, update, delete, transfer, restore, purge. The describe lists the same six. Matches. (A verb added to the gate is refused by feat(plugin-security)!: a flow that runs as system writes no identity assignment #25483 and would need a describe edit; that coupling is noted in the gate's own docblock.)
  • Error. PermissionDeniedError, code = 'PERMISSION_DENIED', status 403. The describe says PERMISSION_DENIED; the comment and the D3 entry add the 403. Matches.
  • Remedies. ASSIGNMENT_PRESCRIPTION (the two assignment tables → runAs: 'user', the D12 gate judges the acting user and stamps granted_by) and MEMBERSHIP_PRESCRIPTION (invitations and memberships → the organization endpoints). The describe says exactly that split, with the ADR-0090 D12 citation. Matches.
  • Unchanged set. Reads of the four, writes to every other object, a runAs: 'user' run, and a platform writer whose isSystem context carries no flowRunId. The describe's new sentence says nothing about non-run writers, so it claims nothing false; the source comment states "a platform writer whose system context carries no flow run is unchanged" and the D3 entry's reason enumerates them (seeders, better-auth and its reconciliation, invitation placement, the organization-administrator grant). Right: a flow author is never a flow-less platform writer, so the describe does not owe that clause; the card's item 1 wording is met across the describe plus its comment block.
  • Position. The sentence sits after "system flows keep system privileges; after an approval decision…" and before the no-trigger-user clause. The retained "keep system privileges" phrase is now qualified in the same string, so the published contract no longer states the rule without its exception. Right.
  • No key, default, enum value or accept-set moves: the flow.zod.ts hunk is a comment block and a longer .describe() string only. Clause-②: no is the right line.

J2 — the ADR-0087 D3 semantic entry: RIGHT. Shape per entries/README.md: one file, named 18. plus the entry id plus .ts, the filename derived from id (flow-run-as-system-identity-assignment-write-refused), copyright header, type-only import, export const entry: SemanticMigration with id, surface, replacement, reason, acceptanceCriteria; no conversionIds (no D2 conversion exists, and none should: flipping runAs changes every other node's authority and an endpoint call is a new call, not a rewrite), no relevantWhen (so it is always listed, correct for a surface the stack question cannot prove absent). surface carries no backtick and no pipe, as the upgrade-guide renderer requires; it is prose like the step-18 siblings (visibility-predicate-has-argument-refused), not the dotted form the type's example gives, which the registry accepts. Step number 18 is right: @objectstack/spec is 18.0.0 and PROTOCOL_VERSION is 18.0.0 on the head, so the change of meaning lands inside protocol 18 and belongs to MIGRATIONS_BY_MAJOR[18].semantic. No STEP18_RATIONALE fragment was added to registry.ts.template; none is owed (290 of the 417 step-18 semantic entries on the head carry none). registry.ts, spec-changes.json and the upgrade guide are generated and git-ignored, so nothing further is owed in the tree. The entry's reason repeats #25483's behaviour (predicate, six verbs, four objects, 403, any depth including cascade legs and hook writes, the unchanged set, the zero census) and matches the merged module's docblock.

J3 — regenerated projections: RIGHT. The two .mdx tables carry the new describe text verbatim in every runAs row they render (five rows in automation-api.mdx, two in flow.mdx). A scan of skills/** and content/docs/** on the head finds no other copy of the describe string, consistent with the dev's "no diff" from gen:skill-refs and gen:skill-docs.

J4 — the changeset marker. The changeset is not declared breaking (no BREAKING banner, no !, minor), so check-adr-0087-registration does not judge it (its G1 case). It carries the adr-0087: registered marker anyway, naming the entry id; harmless and truthful, and Lint & Repo Gates concluded success on the head.

② Semver level

@objectstack/spec minor, with Clause-②: no (describe text and ADR registration only; no accept-set change) and Metadata-protocol: semantic (records the change of meaning #25483 landed) on the PR body and the changeset, verbatim as the claim. RIGHT. The dispatch order said patch; the gate wins, and the gate is as the dev cites it: scripts/check-changeset-no-major.mjs header, "THE LEVEL AXIS (#16055; re-ruled by maintainer ruling A, 2026-10-21, on #25105) — the level follows the METADATA PROTOCOL alone … a declared kind must grade AT LEAST ONE package whose published source it moves minor or above; a declared none must grade every entry it introduces patch". A semantic declaration at patch would have been refused; Check Changeset concluded success on the head. The ADR-0087 same-release condition is a release matter, not a level one: #25483 (@objectstack/plugin-security minor with !) is on main, this PR's D3 entry must ship in the same release, and the maintainer has paused the release cut (project timeline) until it does.

③ Boundary flags

Dev deviations (three) and open_questions (none) from the os-dev-report 6377773648:

  1. minor not patch — answered above, RIGHT.
  2. ADR-0073 amendment left out as Tier H — RIGHT to leave out: docs/adr/** is on the governed list (check-governed-merges.mjs, ruled 2026-09-18; CODEOWNERS /docs/adr/), so it lands by a maintainer merge and must not hold items 1 and 2. The draft at /mnt/project-files/spec-seat-1/out/25554-adr-0073-amendment-draft.md was read. Its substance is right: it targets the one bullet that is now false (line 155 on main, "runAs:'system' stays god-mode throughout"), states the predicate, the six verbs, the four objects, the 403 ahead of the bypass, both remedies with the D12 citation, and the flow-less system writer as unchanged, and it dates and cites the ruling and both PRs. Two wording points for whoever routes it: (a) the ADR's existing amendment convention (the 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 amendment) is a dated ## Amendment: section near the top plus a strike-through at the falsified sentence; the draft is an in-place bold parenthetical only, so the router should either add the short dated section or accept the lighter form knowingly; (b) line 24 of the ADR ("elevating would have widened these runs (e.g. letting them write sys_user_position)") is still true for an isSystem context without a flowRunId but now reads as if a system flow could write that table; one trailing clause there would close it. Neither point touches this PR.
  3. The describe sentence omits the platform-writer clause — answered in J1, RIGHT.

Escalations, none of which blocks this head:

Check-runs on the head, read at 2026-10-24T00:58Z: 40 runs, 24 success, 6 skipped (opt-in and matrix placeholders), 10 in_progress. Success includes every gate this face owes: Check Changeset, Lint & Repo Gates, Lint · PM dispatch-gates, Lint · docs & code guards, Build Docs, Spec property liveness, Governed Surface Queue Guard, Dogfood Regression Gate, Type Check · source gates, and the three claim/single-writer guards. Still pending: Build Core, Test Core (1/6) through (6/6), Type Check · consumer gates, Type Check · debt ledger, Type Check · workspace. The verdict below stands on the completed conclusions and on the read of the diff; a red on any pending run re-opens it, and the queue guard holds the PR until they conclude in any case.

Implemented-by: claude/project-thread-7mlrrw
Reviewed-by: session_013odhA26dvzy7pQmevdTTvy

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (seat post #6017) · os-tesla · session session_013odhA26dvzy7pQmevdTTvy · 2026-10-24T01:28Z

Head 5a9bf414cf · card #25554 (PR2 of ruling 6354966423 on #25251) · claim 6377362674 · report 6377773648 · contract review 6377874212 (PASS). Merge go: Tesla, 2026-10-24T01:25Z, relayed by the director thread.


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/s tooling

Projects

None yet

2 participants