Skip to content

fix(core): apiOperations offers only what the REST door serves — nothing on apiEnabled: false, export only where the export door admits it (#20135) - #20151

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20135-api-operations-parity
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-20135-api-operations-parity

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20135

Clause-②: no

apiOperations, the operation set /auth/me/permissions (and ISecurityService.getEffectiveObjectPermissions) attaches to each entry, now offers exactly what the REST door serves this subject. It used to disagree with the door in two places, both in the direction of offering an operation the door refuses:

  • enable.apiEnabled: false. The door answers 404 OBJECT_API_DISABLED for every verb. The annotation ignored the switch: it carried the object's whole closure, or no annotation at all (read by a client as default-allow) when the subject's export stays allowed on an otherwise unrestricted object.
  • The export slot (seat pointer 5831905897). annotateEffectiveApiOperations fell back to the MERGED '*' export bit (acc.allowExport ?? wildExport). So a private object reached only through a plain '*': { allowExport: true } was annotated export, and so was an object the exporting set itself names without the grant. The export door answers both 403 EXPORT_NOT_PERMITTED.

The fix is in the producer, annotateEffectiveApiOperations in @objectstack/core (packages/core/src/security/effective-object-permissions.ts), as triage directed: 「read the same resolver enforceApiAccess reads, so the two cannot diverge」. The REST door, packages/spec (resolveEffectiveApiMethods, effectiveOperationsArray, apiExposureDenialReason), checkObjectPermission, the seed and the folds are all untouched. No exported name or type changes.

What changes

The annotation asks the door's own two questions, entry by entry:

Which entries carry an annotation keeps the #18931 / #18990 rule: an unrestricted object whose every operation is still served, export included, gets none. What moved is the evaluation of "still served": an API-disabled object never qualifies.

H4: apiOperations: [], and the entry stays

Measurement

Door semantics used below. An operation is "served" when neither door refuses it: the object door (404 OBJECT_API_DISABLED / 405 OBJECT_API_METHOD_NOT_ALLOWED) and, for export, the export door (403 EXPORT_NOT_PERMITTED). The operations measured are the ones the REST door gates by name: get, list, create, update, delete, bulk, import, export. bulk counts as served when any of createMany / updateMany / deleteMany passes the door. "Offered" means the entry's apiOperations lists the operation, or the entry has no annotation (default-allow).

Real stack (H1, H2): the real GET /auth/me/permissions against real REST requests

This used a throwaway dogfood probe that is not committed. It ran bootStack with the real SecurityPlugin, orgContext: true, 61 registered objects, and one authored app. The app holds pq_open (public), pq_exposed (apiEnabled: true), pq_hidden (apiEnabled: false), pq_hidden_subset (apiEnabled: false + apiMethods: ['get','list']), pq_subset (apiMethods: ['get','list']), pq_private (access.default: 'private') and pq_private_hidden (private + apiEnabled: false). There are 0 such apiEnabled: false objects in examples/, so the reach is authored objects only. Every subject × every registered object × the 10 door requests above went through the real REST routes, with bodies that cannot mutate. Base is 16c5a33fdd; head is this branch.

subject (resolved sets) offered but refused, present entries: base → head served but hidden: base → head no-entry cells (unchanged) route == member
seeded platform admin (admin_full_access, organization_admin_no_bypass, member_default) 16 → 0 0 → 0 0 yes
admin_full_access + member_default + a plain '*': { allowExport } 21 → 0 0 → 0 0 yes
member_default 0 → 0 0 → 0 134 yes
member + explicit read on the app objects 16 → 0 0 → 0 101 yes
… + a plain '*': { allowExport } 20 → 0 0 → 0 101 yes
member + explicit read and export on the app objects 19 → 0 0 → 0 101 yes
member + a plain '*' with every bit and export 11 → 0 0 → 0 15 yes
member + '*': { viewAllRecords, allowExport } 19 → 0 0 → 0 0 yes
  • H1 holds. At base, pq_hidden read [get, list, create, update, delete, upsert, bulk, aggregate, search, import] for the platform admin, and had no annotation at all for export-allowed subjects. Meanwhile every REST verb on it answered 404 OBJECT_API_DISABLED. At head every API-disabled object reads [].
  • H2 holds. For admin_full_access beside a plain export-only '*', sys_secret read [get, list, aggregate, search, export] and GET /data/sys_secret/export answered 403 EXPORT_NOT_PERMITTED. The same holds for the authored private pq_private, which had no annotation (default-allow) and a 403 export. The member with explicit read beside the plain export wildcard shows the same on pq_private.
  • Entry-level diff, base-equivalent leg → head, all 8 subjects (the base-equivalent leg is the ablation below; its door readings equal true base in all 8 subjects): 0 entries added or removed, 0 allow* bits changed, 0 operations added to any annotation, 74 operations removed, 11 annotations added (9 × [], 2 × the closure minus export on pq_private).
  • No-entry cells are objects the subject's map carries no entry for (e.g. member_default on the sys_* objects it cannot read). Whether an entry exists is the seed's question, and this PR leaves it alone. See the Acceptance notes.

Unit fixture (H3): the committed parity column

plugin-security's get-effective-object-permissions.test.ts table covers PR #20145's 16 subjects plus 2 export-slot subjects. The 2 new subjects are admin_full_access beside a plain export-only '*', and one set whose exporting '*' sits beside an explicit crm_lead entry without the grant. Each subject is checked against every registered object its map carries (the 11-object REGISTERED fixture) and every door-gated operation. The oracle is canServeApiOperation for the object half, the door's decision function, since plugin-security takes no dependency on @objectstack/rest. For export it is the real registered svc.canExport. The subjects also run through the existing can() ↔ checkObjectPermission rows, green with KNOWN_OVER_GRANT untouched.

subject offered but refused: base → head served but hidden: base → head
wall-less org admin · viewer · one set: plain '*' + narrower entry · platform admin · platform admin + wall-less · walled org admin · bare modify-all · super-read + plain bits · one set: super-user '*' + narrower entry 7 → 0 each (crm_hidden × 7) 0 → 0
explicit entry beside a plain '*' · super-user '*' + export · super-read '*' + export · explicit entry beside a super-user '*' 8 → 0 each (crm_hidden × 8, export included) 0 → 0
platform admin beside a plain export-only '*' 10 → 0 (crm_hidden × 8, crm_secret.export, sys_secret.export) 0 → 0
one set: exporting '*' + explicit crm_lead without the grant 9 → 0 (crm_hidden × 8, crm_lead.export) 0 → 0
member · export-only '*' beside a reader · a '*' granting nothing 0 → 0 0 → 0

Entry-level over these 18 subjects: 0 entries added or removed, 0 allow* bits changed, 0 operations added to any annotation, 92 removed, 7 annotations added. The core pins also hold an export grant without read to no export, and read and export arriving from two different sets to export.

For domain:cli (the route) and domain:services (plugin-security): cross-lane

No code in plugin-hono-server or plugin-security changes, only their pins. The bytes /auth/me/permissions serves change for the affected objects, and getEffectiveObjectPermissions returns the same map:

  • an object declaring enable.apiEnabled: false reads apiOperations: [] in every entry. That includes an unrestricted one whose export stays allowed, which used to carry no annotation;
  • export leaves the annotation where the export door refuses it: a private object reached only through a plain wildcard export grant, an object named without the grant by the set whose wildcard carries it, and an entry granting export without read. A private, unrestricted object in that position gains an annotation, its closure minus export.

Nothing else moves: no entry, no allow* bit, no added operation. Shape, keys and route are unchanged, and so is every server decision. can() reads the same bits.

Clause-②

no, as the claim carries it. The served annotation narrows only toward what the door already refuses (0 operations added, measured above), and the door is untouched, so no request that succeeded now fails. No exported name or type changes. annotateEffectiveApiOperations keeps its signature. As an exported helper called standalone, it no longer reads the map's '*', a behaviour change named in the changeset. There are 0 non-test callers in this repo: every hit of git grep annotateEffectiveApiOperations outside tests is its definition, its re-export, the composition and comments. check-changeset-no-major --base origin/main exits 0 ("This diff introduces no major bump"), and check-adr-0087-registration --base origin/main exits 0 ("1 non-breaking changeset(s) seen"). The changeset grades @objectstack/core patch.

Tests run, at head f2904cd05b (core dist/ rebuilt from this source)

Each suite ran under the verify lock, and each line quotes its VERDICT command-exit 0:

  • pnpm --filter @objectstack/core test: 53 files, 1353 tests passed. typecheck exit 0, test layer included.
  • pnpm --filter @objectstack/plugin-hono-server test: 27 files, 323 passed. typecheck exit 0.
  • pnpm --filter @objectstack/plugin-security test: 135 files, 2718 passed. typecheck exit 0.
  • Dogfood, 18 files, against the dist/ closure built from this code (turbo run build --filter='@objectstack/dogfood^...', 64 tasks), in two runs:
    • 126 passed and 1 skipped: organization-update-door, me-apps-and-everyone-baseline, showcase-permission-projection, showcase-permission-seeding, showcase-permission-zoo, two-doors-permission, comments-permission-matrix, attachments-permission-matrix, authz-conformance;
    • 135 passed: showcase-private-owd, owner-anchor-and-bulk-writes, showcase-crud-persona-matrix, showcase-client-liaison-fixtures, showcase-fls-read-mask-strip, showcase-scope-depth, showcase-scope-depth-write, showcase-scope-depth-fallback, showcase-anonymous-deny-surfaces.
  • New and changed pins:
    • core effective-object-permissions.test.ts: 7 new cases in a [#20135] block;
    • plugin-security get-effective-object-permissions.test.ts: 2 subjects, the apiOperations column (one case per subject) and a reported-cases case;
    • plugin-hono-server current-user-endpoints-effective-objects.test.ts: a route byte-equality case over an API-disabled and a private object;
    • plugin-hono-server effective-api-operations.test.ts: 4 API-disabled cases. Four existing cases were rewritten: one is inverted on purpose, since it pinned the merged-'*' fallback itself, and three called annotate on a map that never went through the per-set passes. Those three now go through buildEffectiveObjectPermissions and assert the same outcomes.

Ablation

The mutation restored the base reading in one block: the merged-'*' export bit back, and the door filter off. A string marker kept the dist reading exact.

  • It went through scripts/ablation-replace.mjs (anchor 1 → 0, blob da81b319 → 33501e91). Core's dist/ was rebuilt, and ablation-dist-preflight found the marker in 2 built files.
  • Observed direction: red.
    • core: 5 failed, 22 passed. The two right-direction controls stayed green.
    • plugin-hono-server: 6 failed, 27 passed, over its two pin files together (effective-api-operations.test.ts, 29 cases, and current-user-endpoints-effective-objects.test.ts, 4 cases).
    • plugin-security: 16 failed, 36 passed. These are the 15 subject rows with an API-disabled or export mismatch, and the reported-cases case. The three subjects with no mismatch stayed green, as did every can() row.
  • The stack probe on the ablated build reproduced true base's door readings in all 8 subjects.
  • Restore. The blob equals HEAD, and git diff HEAD is empty (the tool's own verdict). Core was rebuilt, and ablation-dist-preflight --absent reports the marker absent from all 14 built files and a clean tree. All three suites are green again: 27, 33 and 52 passed. A second ablation leg, run to dump both legs' maps for the tables above, restored the same way.

Gates, at head f2904cd05b

  • The list comes from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 64 commands, derived from 6 paths against merge base 16c5a33fd. I ran each one and recorded its exit code. 64 of 64 exit 0.
  • One needed a second run. pnpm check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3, not measured) because 8 packages had no dist/. After turbo run build of those 8, all cache hits, it answered: "105 published require entry point(s) across 67 package(s) load; 660 emitted CommonJS file(s) parse".
  • dispatch-gates --ran: "64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN".
  • node scripts/check-issue-citations.mjs --base 16c5a33fdd: exit 0, 3 citations resolve.
  • Lint, as a proven narrowing. eslint --no-inline-config --format json over the 5 changed TypeScript files reports 5 files, 0 errors and 0 warnings. eslint --print-config resolves a config for each of them, and eslint ignores the changeset. eslint.config.mjs never enables type-aware linting (no parserOptions.project, no typed rules, as its own comment states), so this diff cannot move any untouched file's verdict. The repo-wide pnpm lint is CI's.
  • NOT MEASURED locally (CI owns them): Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance, the type-check lanes, and the families dispatch-gates names outside its 64.

DELIBERATE CORRECTION: three pending changesets (seat amendment 2, 5851881633)

Each change rewrites ONE sentence of a pending release note that this PR makes false, following the #20132 / #20145 precedent. Every other line of each file is byte-identical: git diff --numstat f2904cd05b HEAD reads 1 1 on each file, and with the changed line deleted from both sides diff exits 0. Check Changeset turns red on the foreign-changeset rule by design, naming exactly these three files.

1. .changeset/18931-me-permissions-unrestricted-export-annotation.md, line 13 (blob 6eac3716 → d49f56e3)

2. .changeset/18990-viewall-only-permissions-seed.md, line 12 (blob 261e412f → 727fbffa)

3. .changeset/20134-super-user-entries-every-bit.md, line 15 (blob a113da40 → d2ee6a58)

The new sentences are TRUE at this head. An object with enable.apiEnabled: false is annotated [] in every entry, and the REST door answers 404 OBJECT_API_DISABLED for every verb on it; both were measured on the stack above. Every other unrestricted object whose export stays allowed still carries no annotation. That holds for the controls pq_open and pq_exposed, and for crm_account in the core pins.

This card's own changeset follows. Its line 21 said those notes "otherwise describe [such an object] as carrying none", which the corrections make false. That one sentence now reads 「That includes an unrestricted object whose export stays allowed, which used to carry no annotation at all; this release's notes for #18931, #18990 and #20134 name that exception.」 git diff --numstat reads 1 1, and every other line is byte-identical.

Patch round 1, at head ba66a482b2 (contract review 5851878434, prose only)

Two commits sit on top of f2904cd05b: the three corrections, then this card's changeset sentence. They touch only those four .changeset files. Every code and test blob is identical to f2904cd05b: git diff --stat f2904cd05b HEAD -- . ':(exclude).changeset' is empty, and the five files carry blobs da81b3192b, cd0367c631, ab113b718a, 070606df33 and 0ea90cbfed at both heads. So the suites, ablation, stack and gate readings above stand for the code. The branch was not merged with main, and its merge base is still 16c5a33fdd.

Gates re-run at ba66a482b2, with --base 16c5a33fdd (the merge base) and this body as the --event:

  • node scripts/check-empty-changeset.mjs --base 16c5a33fdd: exit 1, as expected. It names exactly the three corrected files, each as "present on the merge base and CHANGED by this PR -- this is somebody else's release note": .changeset/18931-me-permissions-unrestricted-export-annotation.md, .changeset/18990-viewall-only-permissions-seed.md and .changeset/20134-super-user-entries-every-bit.md. Its first line is "✓ No empty-frontmatter changeset introduced by this diff (4 declaring changeset(s) added)". The class is DELIBERATE CORRECTION, not COLLISION, so the base text must not be restored; a same-head contract-review PASS is what confirms it.
  • node scripts/check-changeset-no-major.mjs --base 16c5a33fdd --event (this body): exit 0. It prints "✓ This diff introduces no major bump." and "✓ LEVEL AXIS: this PR declares clause-② no, so no package here is declared to have grown a published surface.", with declaration line Clause-②: no and no direction arm.
  • node scripts/check-adr-0087-registration.mjs --base 16c5a33fdd --event (this body): exit 0, "✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (4 non-breaking changeset(s) seen)."
  • node scripts/check-issue-citations.mjs --base 16c5a33fdd: exit 0, "✅ check-issue-citations: every citation this change adds resolves (or is a declared cross-repo reference)." (3 judged, 3 resolve).
  • origin/main has since moved one commit, to 8d1f7ab785 (feat!: retire the saved-report stack — /api/v1/reports, client.reports, IReportService, the reports capability, sys_saved_report / sys_report_schedule, @objectstack/plugin-reports #20125). It touches none of the six files in this diff, and in packages/core/src/security it touches only operation-private-keys.ts and its pin. The branch was not merged, as the seat directed.

Acceptance notes

Session session_01Bvd69VPa6puiNzzPUroDBx (the domain:engine seat 1 dispatch, round 22), on branch claude/issue-20135-api-operations-parity.


Generated by Claude Code

annotateEffectiveApiOperations now reads the door's own two questions per
entry: canServeApiOperation (the spec's apiExposureDenialReason, which
enforceApiAccess turns into its 404/405) for the object half, so an object
with enable.apiEnabled false is annotated [] instead of the full closure or
nothing; and objectPermissionGrants(entry, 'allowExport') for the user half,
so the export slot is the entry's own per-set, per-posture grant rather than
the merged '*' export bit.

Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx
Co-authored-by: Claude <noreply@anthropic.com>
plugin-security's parity table gains an apiOperations column: every
subject x every registered entry x every operation the door gates by
name, against canServeApiOperation (the door's own decision function)
and the security member's canExport. plugin-hono-server pins the route
bytes for an API-disabled and a private object, and its helper battery
reaches a wildcard's export grant through the composition, where the
per-set passes put it.

Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 25, 2026
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/permission-sets.mdx (via allowExport (literal, a string literal in a comment on a changed line; a string literal in annotateEffectiveApiOperations))
  • content/docs/permissions/permissions-matrix.mdx (via allowExport (literal, a string literal in a comment on a changed line; a string literal in annotateEffectiveApiOperations))

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

  • content/docs/releases/v17/17-0.mdx (via allowExport (literal, a string literal in a comment on a changed line; a string literal in annotateEffectiveApiOperations))
  • content/docs/releases/v17/17-1.mdx (via allowExport (literal, a string literal in a comment on a changed line; a string literal in annotateEffectiveApiOperations))

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
  • the SDK route bridge reached 54 of 207 client-bound route-ledger rows — the other 153 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 153: 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; 98 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 — 24 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 8d1f7ab78545dea1b46a0bab064676950007b815 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e8d53777fc33dc30bcdd305c3547916bc3ba6956 — the merge of head ba66a482b29b12d93224afba754447432724d1d6 into base 8d1f7ab78545dea1b46a0bab064676950007b815, 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 e8d53777fc33dc30bcdd305c3547916bc3ba6956 && git checkout e8d53777fc33dc30bcdd305c3547916bc3ba6956
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8d1f7ab78545dea1b46a0bab064676950007b815 ba66a482b29b12d93224afba754447432724d1d6 && git checkout -B drift-repro 8d1f7ab78545dea1b46a0bab064676950007b815 && git merge --no-ff ba66a482b29b12d93224afba754447432724d1d6

node scripts/docs-audit/affected-docs.mjs --json 8d1f7ab78545dea1b46a0bab064676950007b815

⚠️ 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 8d1f7ab78545dea1b46a0bab064676950007b815 → 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: f2904cd05b9ca0fb61a7b9aed1ea2addd2f6fb93

Scope: 6 files, +373/−41, 3 commits (fix / test / changeset). Head sits directly on merge base 16c5a33fdd = origin/main (re-read at close; unchanged). Producer hunks in packages/core/src/security/effective-object-permissions.ts: one import line (canServeApiOperation) and annotateEffectiveApiOperations docblock + body only (@@ -37, @@ -437..-466); seed, both folds, clamp and buildEffectiveObjectPermissions untouched. packages/spec/**, packages/rest/**, permission-evaluator.ts, security-plugin.ts: 0 paths touched. Other 5 files are tests + .changeset/20135-api-operations-rest-door-parity.md. git merge-tree --write-tree from a driverless bare clone, 16c5a33fdd × head → tree 7dc8be06, 0 conflicted paths (fast-forward); PR mergeable_state: clean, draft. Path leg of Clause-②: not hit. Governed surfaces: none. Review owed by the .changeset prose face.

① Derived judgments

  1. Public door — measured, not inferred. Real stack per leg (bootStack + real SecurityPlugin + real REST routes, orgContext: true, 61 registered objects) with an authored app: pq_open, pq_exposed (apiEnabled: true), pq_hidden (apiEnabled: false), pq_hidden_subset (apiEnabled: false + apiMethods: [get,list]), pq_subset (apiMethods: [get,list]), pq_private (access.default: 'private'), pq_private_hidden. 8 subjects × every present entry × get/list/create/update/delete/bulk(batch,createMany)/import/export against the real routes; refusal = 404 OBJECT_API_DISABLED / 405 OBJECT_API_METHOD_NOT_ALLOWED / 403 EXPORT_NOT_PERMITTED. Offered-but-refused base → head (served-but-hidden 0 → 0 in every row; route objects == getEffectiveObjectPermissions JSON in every row, both legs):

    • seeded platform admin (admin_full_access, organization_admin_no_bypass, member_default): 16 → 0 (pq_hidden ×7, pq_hidden_subset get/list, pq_private_hidden ×7)
    • same admin + plain '*': { allowExport }: 21 → 0 (+ pq_hidden.export, pq_hidden_subset.export, pq_private.export, pq_private_hidden.export, sys_secret.export)
    • member_default: 0 → 0 (134 door-refused no-entry cells, unchanged)
    • member + explicit read: 16 → 0 (101 no-entry unchanged)
    • member + explicit read + plain '*' export: 20 → 0 (incl. pq_private.export; 101)
    • member + explicit read+export: 19 → 0 (101)
    • member + plain '*' every bit + export: 11 → 0 (15)
    • member + '*': { viewAllRecords, allowExport }: 19 → 0

    Raw: admin on pq_hidden → every one of the 9 requests 404 OBJECT_API_DISABLED, base entry apiOperations: [get,list,create,update,delete,upsert,bulk,aggregate,search,import], head []; admin+plain-export on pq_hidden base had no annotation (default-allow), head []; pq_subset create/update/delete/bulk/import 405, export 403 EXPORT_NOT_PERMITTED, annotation [get,list,aggregate,search] both legs; sys_secret base […,export] → head minus export, door 403 EXPORT_NOT_PERMITTED; pq_private base no annotation → head closure minus export, door 403. Bar met: 0 offered-but-refused at head, 0 served-but-hidden added. Reproduces the dev's table cell for cell.

  2. No server decision moved. Door status+code byte-identical base vs head: 8 subjects × 61 objects × 9 requests, 0 differing cells. can() (formula ExpressionEngine over toEvalPermissions(route map)) vs PermissionEvaluator.checkObjectPermission over the resolved sets, 10 verbs × 61 objects = 610 cells per subject: 0 disagreements at base and at head, lists identical. Entry-level diff over the 8 subjects: entries added 0 / removed 0; allow*/bypass bits changed 0; operations added 0; operations removed 74; annotations added 11 (9 × [], 2 × closure-minus-export on pq_private); annotations removed 0. Only apiOperations moved, only by removal, [] or a narrower list.

  3. Disabled shape. apiEnabled: false entry reads apiOperations: [] and stays with its grants (head admin pq_hidden: allowCreate/Read/Edit/Delete/Transfer: true, apiOperations: []). 0 allow bits changed ⇒ can() unchanged (see 2). [finding] /auth/me/permissions is silent for a wildcard-only viewAllRecords principal too — the class #18931 fixed only for modifyAllRecords #18990 ruling (every reachable object keeps an entry): 0 entries removed; per-subject entry counts identical both legs (64/64/31/38/39/38/61/64). Spec contract "absent = default-allow" kept: an unrestricted, fully-served object carries no annotation (e.g. pq_exposed for the reader+plain-export subject). The .describe()'s "Present only when the object tightens exposure via apiMethods" was already inexact since 用户级 export 权限轴(接入 P1 预留的 userExportAllowed 槽) #3544 and is now also inexact for apiEnabled: false — packages/spec out of scope, noted.

  4. The pins discriminate. Ablation at head, my own mutation (export slot back to (acc.allowExport ?? objects['*'].allowExport) === true, served = closure; blob da81b319 → f00e0700; core dist/ rebuilt — ESM/CJS emitted with the base spelling in 2 dist files; the DTS pass failed TS6133 on the then-unused canServeApiOperation import, an artefact of the ablation, not of the PR). Red: core 5 failed / 22 passed (27); hono 6 / 27 (33, over effective-api-operations.test.ts + current-user-endpoints-effective-objects.test.ts); plugin-security 16 / 36 (52). Exactly the dev's counts. Restore: git checkout HEAD -- <file>, blob == HEAD da81b3192bbf6595e3699c9c1ed336a570f2b020, tree clean, core rebuilt exit 0, base spelling absent from dist, head spelling present; suites green again 27 / 33 / 52.

  5. Clause-② and semver (Q2). annotateEffectiveApiOperations is on both published . entries: @objectstack/core (security/index.ts) and @objectstack/plugin-hono-server (export * from './current-user-endpoints', which re-exports it). Signature unchanged; called standalone on a map not built by buildEffectiveObjectPermissions it no longer inherits the merged '*' export bit (pinned by the inverted hono case). In-repo non-test callers: 0 (hits = definition + the composition's call, core barrel re-export, hono re-export; the rest are tests, changesets, CHANGELOGs, spec/liveness/permission.json). Judgment: Clause-②: no with @objectstack/core patch is right — accept set unchanged (same signature, same inputs accepted), no published key/type/export added, output narrows only toward what the door already refuses (0 ops added, measured), same class as the finding(plugin-hono-server): /auth/me/permissions never seeds an unrestricted object for a wildcard-only principal, so the Console renders Export where the server answers 403 EXPORT_NOT_PERMITTED #18931 / [finding] /auth/me/permissions is silent for a wildcard-only viewAllRecords principal too — the class #18931 fixed only for modifyAllRecords #18990 precedents (no, patch, same channel). Not no (narrowing): nothing an author can write is removed or renamed and no accept set narrows; the standalone-caller behaviour change is disclosed in the changeset ("For a caller of the exported helper"). Gates at head, --base 16c5a33fdd, PR body as --event: check-changeset-no-major exit 0 — "✓ This diff introduces no major bump. / ✓ LEVEL AXIS: this PR declares clause-② no, so no package here is declared to have grown a published surface. · declaration line: Clause-②: no · direction arm: none declared"; check-adr-0087-registration exit 0 — "✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)."

  6. Q1 — the three pending changesets, read literally at head. Measured: resolveEffectiveApiMethods({ apiEnabled: false }).mode === 'unrestricted' (both legs; with apiMethods: [get,list] it is 'restricted'), canServeApiOperation({ apiEnabled: false }, 'list') === false; at head such an object carries apiOperations: [] for every subject whose export stays allowed (pq_hidden: base no annotation → head [] in 4 of the 8 subjects). So an apiEnabled: false object with no apiMethods is "unrestricted" in the resolver's own vocabulary, and each note uses that vocabulary (18931's own next bullet: "mode stays unrestricted either way"; 18990's "the predicate already shared with the modify-all class" and 20134's "skipped every unrestricted object whose export stays allowed" describe the old mode === 'unrestricted' && userExportAllowed predicate). The PR's own changeset concedes it: "That includes an unrestricted object whose export stays allowed, which this release's notes … otherwise describe as carrying none."

    • 18931-me-permissions-unrestricted-export-annotation.md line 13 (blob 6eac3716): "an object that is unrestricted and keeps export gets none." → FALSE (it gets []).
    • 18990-viewall-only-permissions-seed.md line 12 (blob 261e412f): "an unrestricted object whose export stays allowed still gets no apiOperations (…), because for it the client's default-allow path is already right." → FALSE (gets []; and default-allow is wrong for it — the door 404s).
    • 20134-super-user-entries-every-bit.md line 15 (blob a113da40): "That entry carries no apiOperations, exactly as the operation channel said nothing about the object before, so a client's default-allow path for the operation set is unchanged." → FALSE for such an entry (carries []; default-allow flips to deny-all).

    Not scoped-true: each states the annotation rule as a universal over the resolver's "unrestricted", and a CHANGELOG reader of that entry cannot reach the exception from it; a sibling note stating the exception is the erratum-elsewhere AGENTS.md forbids. Seat Q1 A does not hold under the literal reading the amendment itself asked for ⇒ must-change (below).

  7. Changeset 20135-* and the PR body, sentence by sentence. Every factual sentence checked TRUE at head against code and measurement: producer/consumers, "last pass", door codes, base behaviour (whole closure or no annotation; merged-'*' fallback), canServeApiOperation = boolean face of apiExposureDenialReason judged first and for every operation, identity filter for any other enable (verified over unrestricted / restricted / deny-all / null), export half = objectPermissionGrants(entry,'allowExport') = the evaluator's documented cross-set read ∧ grant, entry stays, [] on every API-disabled entry, export leaves the private-plain-wildcard and same-set-named cases, closure-minus-export on pq_private, 0 entries/bits/ops added, response shape/route unchanged, door unchanged ("no request changes its answer" — status+code identical), signature kept, 0 non-test callers, both gates exit 0. FALSE sentences: 0. Two flags, neither false: (a) "the coverage passes … have already put each set's '*' on exactly the entries that set reaches" is exact for the export bit; the read half still carries the pre-registered security: the effective permission map folds a super-user '*' over the same set's narrower explicit entries — a walled org admin gets can('sys_position','edit') true where enforcement refuses #20136 merged-fold read over-grant — scoped-true; (b) the body's hono "6 failed / 27 passed" spans two hono files without saying so — imprecise. The body's "Whether to correct those bullets is left to the seat" is answered by ⑥.

  8. Scope and merge. As in Scope: spec, rest, seed, folds, checkObjectPermission untouched (0 paths); merge-tree clean against origin/main (= base).

  9. CI at head f2904cd05b. 34 check runs: 31 success, 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke (opt-in)), 0 pending, 0 failures. Required seven all success: Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. Check Changeset success (no foreign changeset edited at this head). Failures with log reason: none.

② Semver level

@objectstack/core patch, Clause-②: no, no arm — consistent with the changeset's frontmatter, the claim, the amendment and both gates. The required prose-only patch round adds no bump (the three corrected files keep their own frontmatter: hono patch, hono patch, core minor).

③ Boundary flags

Implemented-by: claude/issue-20135-api-operations-parity
Reviewed-by: session_01Bvd69VPa6puiNzzPUroDBx

VERDICT: FAIL — prose only; the code half is PASS-ready as measured. Must-change (each one clause; every other line of each file byte-identical, git diff --numstat 1 1):

  1. .changeset/18931-me-permissions-unrestricted-export-annotation.md line 13: replace "an object that is unrestricted and keeps export gets none." → "an object that is unrestricted and keeps export gets none — unless its enable.apiEnabled is false, which is annotated [] since core: /auth/me/permissions apiOperations ignores enable.apiEnabled: false — an object the REST layer answers 404 for is annotated with the full operation list (annotateEffectiveApiOperations → resolveEffectiveApiMethods) #20135 because the REST door answers 404 for every verb on it."
  2. .changeset/18990-viewall-only-permissions-seed.md line 12: replace "an unrestricted object whose export stays allowed still gets no apiOperations (the entry itself is seeded since core: effective-map super-user entries never carry transfer (or a super-read wildcard's plain bits) — current_user.can(obj, 'transfer') is false for admin_full_access where enforcement answers true via modifyAllRecords #20134), because for it the client's default-allow path is already right." → "an unrestricted object whose export stays allowed still gets no apiOperations (the entry itself is seeded since core: effective-map super-user entries never carry transfer (or a super-read wildcard's plain bits) — current_user.can(obj, 'transfer') is false for admin_full_access where enforcement answers true via modifyAllRecords #20134), because for it the client's default-allow path is already right — except an object with enable.apiEnabled: false, annotated [] since core: /auth/me/permissions apiOperations ignores enable.apiEnabled: false — an object the REST layer answers 404 for is annotated with the full operation list (annotateEffectiveApiOperations → resolveEffectiveApiMethods) #20135 because the REST door refuses every verb on it."
  3. .changeset/20134-super-user-entries-every-bit.md line 15: replace "That entry carries no apiOperations, exactly as the operation channel said nothing about the object before, so a client's default-allow path for the operation set is unchanged." → "That entry carries no apiOperations — unless the object declares enable.apiEnabled: false, which is annotated [] since core: /auth/me/permissions apiOperations ignores enable.apiEnabled: false — an object the REST layer answers 404 for is annotated with the full operation list (annotateEffectiveApiOperations → resolveEffectiveApiMethods) #20135 — so for every other such object the operation channel says what it said before and a client's default-allow path is unchanged."

Re-review at the new head needs only: the three rewritten sentences judged, git diff --numstat 1 1 ×3, and the check-runs re-read (Check Changeset red on exactly those three names, everything else green).

One clause each, every other line byte-identical: an object with
enable.apiEnabled false is annotated [] rather than left without
apiOperations, so the 18931, 18990 and 20134 notes now name that
exception.

Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ba66a482b29b12d93224afba754447432724d1d6

Scope: delta review after FAIL 5851878434 (prose only) at f2904cd05b; seat amendment 2 (5851881633) ruled Q1 → B and added the three foreign changesets to the file surface. Two commits on top of f2904cd05b: f4cf2c591f (3 files: the three corrections, 2 +- each) and ba66a482b2 (1 file: this card's changeset, 2 +-). git diff --numstat f2904cd05b ba66a482b2 = 1 1 on exactly four files: .changeset/18931-me-permissions-unrestricted-export-annotation.md, .changeset/18990-viewall-only-permissions-seed.md, .changeset/20134-super-user-entries-every-bit.md, .changeset/20135-api-operations-rest-door-parity.md. git diff --stat f2904cd05b ba66a482b2 -- . ':(exclude).changeset' is empty. The five non-changeset blobs are identical at both heads: da81b3192b (effective-object-permissions.ts), cd0367c631 (its test), ab113b718a (current-user-endpoints-effective-objects.test.ts), 070606df33 (effective-api-operations.test.ts), 0ea90cbfed (get-effective-object-permissions.test.ts). This record carries forward, unchanged, every code judgment measured on those identical blobs in review 5851878434 (real-stack door parity 0/0 at head over 8 subjects, door status+code byte-identical base→head, can() ↔ checkObjectPermission 0 disagreements, 0 entries/bits/operations added, ablation 5/22 · 6/27 · 16/36 and blob restore, Clause-② no / core patch, 0 non-test callers). Merge base is still 16c5a33fdd; the branch was not merged with main. Governed surfaces: none. Fork: no. Draft; mergeable_state: unstable (the by-design Check Changeset red).

① Derived judgments

  1. Blob identity and byte-identity. Measured as above. For each of the four changeset files, with the one changed line deleted from both sides diff exits 0 (every other line byte-identical: YES ×4); line counts unchanged (16/14/17/23). Blobs: 18931 6eac3716 → d49f56e3 (line 13), 18990 261e412f → 727fbffa (line 12), 20134 a113da40 → d2ee6a58 (line 15), 20135 730f82c4 → fdd872df (line 21) — matching the body's stated blobs.

  2. The four rewritten sentences, literally, at this head (an apiEnabled: false, no-apiMethods object is mode: 'unrestricted' to the resolver and is annotated [] in every entry; the door answers 404 OBJECT_API_DISABLED for every verb — both measured on the stack in the prior review on the identical producer blob):

  3. PR body, changed sections, sentence by sentence (diff v1→v2: 126 → 161 lines; one edit, updated_at 02:17:30Z). Export-half paragraph: "For the export bit … the entry's allowExport already is the per-set, per-posture answer and the merged-bit fallback is gone" TRUE (both coverage passes skip objects the set names; plain skips private, super-user covers it); "The read half … still carries the merged fold's read over-grant pre-registered for security: the effective permission map folds a super-user '*' over the same set's narrower explicit entries — a walled org admin gets can('sys_position','edit') true where enforcement refuses #20136" scoped-true (the class — merged bypass folded onto a set's narrower explicit entry — is security: the effective permission map folds a super-user '*' over the same set's narrower explicit entries — a walled org admin gets can('sys_position','edit') true where enforcement refuses #20136's; no fixture row lists a read cell in KNOWN_OVER_GRANT); "It moved no cell of the parity column below" TRUE (0/0). Hono split "6 failed, 27 passed, over its two pin files (29 + 4 cases)" TRUE (my ablation: 33 = 29 + 4). DELIBERATE CORRECTION section: all 8 「」 quotes are verbatim substrings (3 Before → old files, 4 After → new files, 1 = the triage quote); blobs, 1 1, "diff exits 0 with the changed line deleted", "Check Changeset … naming exactly these three files", "the new sentences are TRUE at this head", "pq_open/pq_exposed/crm_account still carry no annotation" (for export-allowed subjects; measured) — all TRUE. Patch-round section: two commits / four files / five identical blobs / merge base 16c5a33fdd / not merged — TRUE; gate quotes TRUE (below); "8d1f7ab785 … touches none of the six files in this diff, and in packages/core/src/security only operation-private-keys.ts and its pin" TRUE. Acceptance-note replacement TRUE. Nits, not false: "see the section below" points upward (the section precedes Acceptance notes); check-adr-0087-registration does not read --event (inert flag; the verdict is unchanged). FALSE sentences: 0.

    Gates at ba66a482b2, --base 16c5a33fdd, final body as --event:

    • check-changeset-no-major: exit 0 — "✓ This diff introduces no major bump." / "✓ LEVEL AXIS: this PR declares clause-② no, so no package here is declared to have grown a published surface. · declaration line: Clause-②: no · direction arm: none declared — the declaration names no widening and no narrowing".
    • check-adr-0087-registration: exit 0 — "✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (4 non-breaking changeset(s) seen)."
    • check-empty-changeset: exit 1, as expected — "✓ No empty-frontmatter changeset introduced by this diff (4 declaring changeset(s) added). / This PR changes a changeset it did not add: .changeset/18931-me-permissions-unrestricted-export-annotation.md · .changeset/18990-viewall-only-permissions-seed.md · .changeset/20134-super-user-entries-every-bit.md — each 'present on the merge base and CHANGED by this PR -- this is somebody else's release note'", then the two-class text; the DELIBERATE CORRECTION arm applies ("do NOT restore it … get it confirmed"). Exactly the three names, no fourth. Also check-issue-citations --base 16c5a33fdd: exit 0, 3 citations resolve.
  4. origin/main moved to 8d1f7ab785 (feat!: retire the saved-report stack — /api/v1/reports, client.reports, IReportService, the reports capability, sys_saved_report / sys_report_schedule, @objectstack/plugin-reports #20125, "retire the saved-report stack"). git merge-tree --write-tree --name-only from a driverless bare clone, 8d1f7ab785 × ba66a482b2 → tree 25d00bb464, exit 0, 0 conflicted paths. Intersection of feat!: retire the saved-report stack — /api/v1/reports, client.reports, IReportService, the reports capability, sys_saved_report / sys_report_schedule, @objectstack/plugin-reports #20125's 126 paths with this PR's 9: empty. Annotate's inputs: api-derivation.ts, permission.zod.ts, effective-object-permissions.ts, permission-evaluator.ts, security-plugin.ts, current-user-endpoints.ts unchanged; rest-server.ts changed (−350/+9, reports routes) but apiAccessDenialFromEnable, enforceApiAccess, enforceExportPermission, loadObjectItems byte-identical and no hunk mentions the door codes; object.zod.ts changed by one comment line (sys_saved_report dropped from an example list) — not the enable block. The registry loses sys_saved_report / sys_report_schedule, which changes the object census, not the annotation logic. The measured code judgments stand across the move.

  5. Docs (content/docs/permissions/permission-sets.mdx, permissions-matrix.mdx). Neither states anything about the apiOperations / export annotation that this PR makes FALSE: permission-sets l.102–104 ("The same decision is published on /me/permissions as the object's effective apiOperations, which is what makes the client hide its Export button — the button and the refusal are one decision, not two") is made more true by this PR. Two pre-existing findings, not must-changes here: (a) both pages say the built-in admin_full_access / organization_admin sets "carry allowExport: true explicitly" (permission-sets l.84–85, matrix l.36–38) — FALSE since [security] org-admin sets ship object_permissions['*'].allowExport = true, so the 17.0 export gate cannot be denied for an org admin — and the sets are not_overridable (17.0.0 GA) #8681: default-permission-sets.ts at this head carries no allowExport on either wildcard (4 comment lines say so), and the stack measured the seeded admin's export door at 403 EXPORT_NOT_PERMITTED on pq_open/pq_subset; (b) permission-sets l.98–100 gates "a report rendered as csv or json … delivered by a schedule" — retired by feat!: retire the saved-report stack — /api/v1/reports, client.reports, IReportService, the reports capability, sys_saved_report / sys_report_schedule, @objectstack/plugin-reports #20125 on main. Both belong in a docs-only card.

  6. CI at ba66a482b2, read after completion (02:25Z). 41 check runs: 34 success, 5 skipped (Auto Label, Check PR Size, Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), 2 failure, 0 pending. All seven required contexts success: Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. The 2 failures are both Check Changeset (runs 36288053591 job 108532516764 and 36288076634 job 108532584510 — one per pull_request event: the push, then the body edit), each red by design with the log reason: "This PR changes a changeset it did not add: .changeset/18931-me-permissions-unrestricted-export-annotation.md / .changeset/18990-viewall-only-permissions-seed.md / .changeset/20134-super-user-entries-every-bit.md — present on the merge base and CHANGED by this PR -- this is somebody else's release note … DELIBERATE CORRECTION -- … do NOT restore it -- say so on the PR and get it confirmed" (Process completed with exit code 1; its MERGE_BASE env was 8d1f7ab785, same three names). No other failure.

② Semver level

Unchanged: @objectstack/core patch, Clause-②: no, no arm — consistent with the changeset frontmatter, the claim, both amendments and both gates. The four prose edits add no bump (18931/18990 keep hono patch, 20134 keeps core minor).

③ Boundary flags

Implemented-by: claude/issue-20135-api-operations-parity
Reviewed-by: session_01Bvd69VPa6puiNzzPUroDBx

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 02:38
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 27, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Merge-queue removal: disposition (one re-queue)

domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T02:59Z.

  • Signature. The queue entry gh-readonly-queue/main/pr-20151-8d1f7ab785 (commit 33013463d2) was removed with CI_FAILURE.
    • The only non-green check run is Lint & Repo Gates (job 108535775672), with conclusion cancelled.
    • The job ran 02:40:00 → 02:48:31. Steps 1–115 passed. Step 116, Response-envelope guard (pnpm check:route-envelope), was cancelled 3 s in, after printing only ✓ and pre-existing ⚠ ratchet #9559 lines.
    • The first error line is ##[error]The operation was canceled. There is no assertion.
    • The job has no timeout-minutes. The rest of its run (Type Check · …) and the whole CI run kept going: 25 of 26 check runs on 33013463d2 are success. Nothing was ahead of this entry in the queue.
    • merge-queue-triage.yml fires on CI failures only, so no machine triage comment exists for this removal.
  • Three facts: all hold.
    1. The failing step and the diff do not intersect. The guard audits route and response modules. This diff's only non-test source is packages/core/src/security/effective-object-permissions.ts, which builds a map and no response body. The same step is green on this PR head ba66a482b2.
    2. The queue base is green on the same job: Lint & Type Check on main at 8d1f7ab785 (run 36287340562) is success.
    3. The first error is a cancellation, not an assertion.
  • Action: auto-merge is re-enabled and the PR re-queued once. If the same signature appears again, the seat stops and hands it to the maintainer.

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/m tests tooling

Projects

None yet

2 participants