Skip to content

refactor(plugin-security): move the authorization middleware out of security-plugin.ts (#25223 cut 12) - #25545

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-3fzya4
Oct 24, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-3fzya4

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Part of #25223
Clause-②: no
Metadata-protocol: none
Ruling-ref: 6353073966

⛔ Tier H on both counts, so this is a human-governed landing. (1) Size: 4,369 changed lines (+2,284 / −2,085), over the 3,000-line line. (2) Path: one path in one anchor of docs/adr/0055-master-detail-controlled-by-parent.md (ruling 6353073966 item 5, the start()-local re-anchor), in its own commit. A governed approver (os-zhuang) approved this PR and enabled auto-merge at 00:20Z on its first head. That head no longer merged once #25541 landed, so the cut was re-derived (below). The seat re-measures this head before it lands.

Cut 12 of the security-plugin.ts split is the second part of the start() row. The claim is 6375757728. The seat agreed the order on the plan (R2 in cut 11, then this cut claimed afresh on the merged main) and cleared it after cut 11 merged as c15fd9a72b. This PR moves one statement of start(), the authorization middleware's registration, into one new module, unedited:

  • packages/plugins/plugin-security/src/security-plugin-authz-middleware.ts: registerAuthzMiddleware, the ql.registerMiddleware(…) call whose handler every engine operation passes through, with the two module-level declarations only the handler reads.

The change is move-only, and the card stays open for R4. The branch is claude/project-thread-3fzya4, the harness-pinned branch for this session. The branch name names no issue, so the claim comment is the identity record.

Re-derived after #25541, and what happened to its hunk

The first head (1b2378c299, on 0fab7baf51) yielded to #25541, as its claim said, because #25541 inserted lines into the moved statement. #25541 merged at 23:59Z as 45f996231e. The cut was then re-derived on that main by the same scripted move, and every measurement below is of the re-derivation.

  • feat(security): the external organization role is the EXTERNAL principal — owner-only org-wide default, explicit grants only #25541's hunk now lives in the module. Its 16 lines are the EXTERNAL rung's grant stamp in step 2.6 (if (isExternalAudienceContext(sc)) { … stampExternalGrant(…) … }). They sit inside the moved statement, so they moved with it, unedited, into registerAuthzMiddleware, where they follow the access-depth stash exactly as they did in start().
  • Why the history has a merge, not a rewrite. AGENTS §3 forbids a force-push here: the open PR carries reviewers and an approval (④), and the branch is not claude/issue-* (①). So the branch keeps its two first commits, and one merge of origin/main (9c43e19f65) resolves the conflict by taking the re-derivation's tree.
    • The merge's tree is byte-identical to the scripted re-derivation merged onto that main: tree 8704c395d0.
    • The re-derivation itself is 6feaeb7274 (the move and readers) and 7e3f354475 (the ADR re-anchor alone) on 45f996231e. Both are kept as local refs and were measured there.
    • The diff of this head against origin/main is the same as that re-derivation's diff (sha256 of both diffs: 78b9e39a…).
    • The seven commits main gained after 45f996231e touch no file this cut touches.

Commits:

  1. b7b408e502 moves the statement and the two declarations on the first base, and re-points the system-context page rows and the census ledger row.
  2. 1b2378c299 is the ADR-0055 re-anchor alone: one path in one anchor, docs/adr/0055-master-detail-controlled-by-parent.md:24.
  3. fa9e98b036 merges origin/main and carries feat(security): the external organization role is the EXTERNAL principal — owner-only org-wide default, explicit grants only #25541's hunk with the statement, as above.

How the statement moves

The same form cut 11 used for one statement of start(). Base line numbers here and below are at 45f996231e.

  • The statement is :2251-:4207 (1,957 lines). It closes over three of start()'s locals and nothing else: ctx, ql and metadata. It uses no arguments, super, new.target or bare this. It has no return, await, break or continue at its top level that would act on start(); the handler's own returns and awaits are inside the arrow and move with it. All of that was measured on the AST.
  • So the function takes the three locals as parameters, typed as the statement saw them there: PluginContext, IObjectQLEngine and IMetadataService. Its body is the statement, re-indented one level and otherwise unedited. It returns void.
  • start() keeps the statement's position, the comment above it (// Register security middleware, :2250) and the log line after it (:4209). In its place is one BLOCK: the getter reads, one host literal annotated with the Pick of AuthzMiddlewareHost, and registerAuthzMiddleware.call(host, ctx, ql, metadata);.
  • Registration is still synchronous and in the same place in start()'s order: after the security service registration (cut 11's stub), before the log line and the position-catalog middleware. start()'s three other middlewares (:4219, :4538, :4561) stay.

What moved

code base lines host members
the authorization middleware registration (one statement of start()) :2251-:4207 25 methods: addressedByIdWriteId, addressedPredicateWrite, assertControlledByParentWrite, callerCannotReadAddressedRow, collectRLSPolicies, composeWriteTenantWall, computeControlledByParentFilter, computeForbiddenFieldWrites, computeLayeredRlsFilter, computePartialMaskRules, computeQueryGuardFieldPerms, computeRlsFilter, computeWriteCheckFilter, delegatorOwnWriteFloorOptions, extractSingleId, getCallerPreImage, getObjectFieldNames, maskOperationResult, raiseWriteWallRefusal, readableRowsScopeForPredicateWrite, resolveCallerPosture, resolveOperationPrincipals, resolvePermissionSetsForContext, resolvePreImageFloorDrop, writeCheckFieldOptions; by value: engineOperationScope; getters: delegatedAdminGate, epoch, fieldMasker, logger, permissionEvaluator, ql
WriteImageCheckSeam (module interface, with its docblock) :407-:442 none
userFacingDenialMessage (module function, with its docblock) :1144-:1197 none
  • WriteImageCheckSeam gains export and nothing else. security-plugin.ts imports it back type-only, because the staying WriteWallRowVerdict docblock links to it ({@link WriteImageCheckSeam}, :491). Cut 6's type-only principalLessDenial import is the precedent. userFacingDenialMessage moves unchanged.
  • Import names that only the moved code reads leave security-plugin.ts, 23 in all. The module imports each from the specifier security-plugin.ts imports it from (the proof checks every pair):
    • recordNotFoundError and isExternalAudienceContext (@objectstack/core), PUBLIC_FORM_SERVER_MANAGED_FIELDS (@objectstack/spec/security), markFilterSubtreeProvenance (@objectstack/spec/data), renderOperationMessage (@objectstack/spec/system), resolveEngineUpdateDispatch and the type EngineUpdateDispatchData (@objectstack/metadata-core), crossFieldClassRefusalCarriedBy and findCrossFieldClassRefusal (@objectstack/formula);
    • assertFlowSystemIdentityWriteAllowed, intersectDelegatedScope, jsonColumnCheckRefusalCarriedBy, storedFormCheckJudge, compiledPolicyNameOf, intersectTenantLayer0Verdicts, rlsOperationForVerb, unresolvedPostureDenialMessage, unresolvedPostureLogLine, assertReadableQueryFields, MaskedValueWriteError, assertEngineOwnedWriteAllowed, grantRowPolicySpeaks and stampExternalGrant, each from its ./ module.
  • Seven comment lines describe three of those imports, and they move with them, verbatim: [#7414] above renderOperationMessage, [#19989] above the @objectstack/metadata-core import, and the [#8220] paragraph above the @objectstack/spec/data import, which names markFilterSubtreeProvenance alone. The [#21829] paragraph under it describes MAX_BULK_PER_ROW_HOOK_ROWS, which stays, and it stays.
  • Two exported functions stay, and the module imports them by value: isGuestEnvelope, which the principal resolution reads too, and requiredCapsForOperation, which packages/spec/liveness/object.json anchors by symbol in that file. Each is read only inside the handler, at call time, never while either module evaluates.
  • RlsFilterOptions keeps its one declaration in security-plugin.ts. The module names it through the class's own signature, computeLayeredRlsFilter's fifth parameter, as cuts 9 and 10 did.
  • Staying prose that names a moved import is in backticks (assertEngineOwnedWriteAllowed, intersectDelegatedScope, resolveEngineUpdateDispatch, each in a staying docblock), not a link, so nothing dangles.
  • The module header lists the handler's steps. Its step 2.6 entry names the grant stamp too ("the access depth and an external principal's grant stamp (2.6)").

The shape (#23050 shape B, as cuts 1 to 11 applied it)

  • AuthzMiddlewareHost is new. It types every member as the class member itself (SecurityPlugin['m']). The function types its this as the Pick of the host over exactly the members its code reads, and the stub annotates its literal with the same Pick. There is no cast, and no member becomes public or protected.
  • Methods go in as late-bound closures that call this.m(...a) on the plugin at call time. So every step still reaches the plugin's method on the plugin, and a spy or test double set on the plugin is still the one the handler reaches.
  • Fields follow the rule cut 1 set, judged on each field's own declaration:
    • engineOperationScope is declared readonly, so it goes by value: the same AsyncLocalStorage.
    • delegatedAdminGate, epoch, fieldMasker, logger, permissionEvaluator and ql are not, so they go as getters. Each read happens when the handler makes it, on every operation, as before: a gate, epoch source or logger assigned after start() is the one the next operation sees.
    • None is function-valued: each is an object whose own methods the handler calls, or a value it reads. No receiver changes.
    • The host declares every field readonly, so moved code cannot assign a member through it. The typecheck passes, so none does.
  • Nothing re-exports the module, so the package entry does not move.

Three readers key on the moved lines, and all three are re-pointed in this PR

Each was run on the re-derivation with its edit reverted, and each fails without it.

  • content/docs/permissions/system-context.mdx, rows 1, 2 and 30. R3 holds start()'s last isSystem read, the row-1 short-circuit (:2307 at base). check-system-context-census keys each read by file and enclosing symbol, so each row's security-plugin.ts#start becomes security-plugin-authz-middleware.ts#registerAuthzMiddleware. The row text and every other anchor stay.
    • Reverted: [file-without-a-row] for the new module and [anchor-is-not-a-read-site] for #start.
  • scripts/check-system-context-census.mjs, the NON_READ_ANCHORS row that declares collapsesOntoRead for row 2's owner_id guard. Its file, its symbol and the symbol name in its why change; collapsesOntoRead: true and its rowSeams stay. The ledger's docblock names the same pair as its worked example of collapsesOntoRead, so that one path changes too.
    • Reverted: [ledger-stale-collapse], because the census no longer finds a read in security-plugin.ts#start.
  • docs/adr/0055-master-detail-controlled-by-parent.md:24, in its own commit (1b2378c299; 7e3f354475 in the re-derivation). The rlsFilter local is R3's (:4064 at base), so the anchor security-plugin.ts#rlsFilter becomes security-plugin-authz-middleware.ts#rlsFilter. One path changes; the sentence, the symbol and every other anchor in the record stay.
    • Reverted: check-adr-symbol-anchors reports [unresolved-symbol] for that anchor.
  • Not readers here, measured: the plugin-security: delegation probes and diagnostics (canReadObject / canWriteObject / canExport, field mask, explain) answer for an on-behalf-of context the operation prologue now refuses (PR #25101) #25107 pin (R3 resolves no delegator: 0 asks, 0 resolutions in the module), the core canReadObject caller pin (R3 has no call), and the position-name fold census (its method stays). feat(security): the external organization role is the EXTERNAL principal — owner-only org-wide default, explicit grants only #25541's lines add none of those.

No new anchor entry, and why

scripts/adr-anchors/ is not touched. The security-plugin.ts entry is unchanged and stays true. Paragraph by paragraph:

  • ADR-0055 §1 is the relation reader (security-plugin-cbp-relation.ts).
  • ADR-0106 D7 is the readable-field asymmetry, in cut 9's field-readers module.
  • ADR-0138 D1 names the seed claim, in cut 11's module. R3's guest-owner branch cites ADR-0138 D1, but the paragraph is about the claim's re-owning arm.
  • ADR-0123 D2 names "step 3.7's no-active-organization refusal" and its order ahead of the forge guard. The refusal, its message and its pre-test live in security-plugin-write-tenant-wall.ts, which carries that paragraph in its own entry since cut 5.
    • Disclosed: R3's step 3.7 asks that module, takes wall.preTest before wall.judgePayload(…), and runs only for insert and update. That call-site order now sits in a module with no entry. An ADR-0123 entry for it would need an ADR-0123 citation the moved code does not carry, and this cut plants none. A follow-up can add one if the seat wants it.

The published .d.ts does not change

Measured under the series bar (ruling 6348891160 item 1), on base 45f996231e and on the re-derivation. Each was a full turbo run build in its own worktree (both exit 0, 77 tasks).

  • Raw sha256 of dist/index.d.ts and dist/index.d.mts is the same for all four files, base and head: 7e6a99f7d59debbac137ff8509e7d92fd5d000c655b412ab0f042a27ad35b380 (14,722 lines). The raw diff is 0 lines, so no union order moved either.
  • Normalised sha256 is the same for all four: e22a714da70e9efc2849122d87f64bc2f364aa9a5a3fe73965118b5fd272f5a3.
  • The normaliser, run from packages/plugins/plugin-security, sorts the members of each string-literal union and changes nothing else:
node -e 'const s=require("fs").readFileSync(process.argv[1],"utf8");process.stdout.write(s.replace(/"[^"\n]*"(?:\s*\|\s*"[^"\n]*")+/g,r=>r.split(/\s*\|\s*/).sort().join(" | ")))' dist/index.d.ts | sha256sum
  • Positive control, a real package build rather than an edit of the emitted file. With private removed from the epoch field in the head source, pnpm build in the package emits epoch as a public member, and the .d.ts pulls in AuthzInvalidationReason. That gives raw 96fda037ff639ae8acc5dd136334fe517dbace0789a4a0abae075cc73b014b94 and normalised 89c71232ae367d47666acc8f443ac22528b32a5d272ea91574eb2f4677e294c9, both different from the real hashes. So the comparison does see a visibility change. With the source restored, a rebuild gives 7e6a99f7… again.
  • security-plugin.ts gains no export. registerAuthzMiddleware, AuthzMiddlewareHost and WriteImageCheckSeam are in no published file, because nothing re-exports the module.
  • Export tables: the ESM and CJS bundles each export the same 66 names at base and at head. The lists were loaded at runtime, with require and import() of the built bundles, not parsed. The sorted name list hashes to 83576430f83cf60cecbcc5c0e80214f16c626a8e66f700294fc6a44361423116 for all four, as at cuts 6 to 11.
  • The bundle JS bytes differ, because code moved within them: index.js goes from e29c0e06 to 77bed321, and index.mjs from d9401b7c to 29e9cece. The set of external specifiers each bundle loads is the same at both ends: 17 distinct require(…) specifiers in index.js and 24 in index.mjs.
  • Bundle order, disclosed. security-plugin.ts no longer imports twelve modules ahead of its first chunk, so they are now emitted later in index.mjs: flow-system-identity-write-refusal.ts, explain-engine.ts, lifecycle-verb-rls-operation.ts, rls-compiler.ts, object-default-row-scope.ts, object-membership-keys.ts, tenant-layer.ts, declared-comparison-columns.ts, rls-check-stored-form.ts, external-baseline.ts, objects/default-permission-sets.ts and predicate-guard.ts.
    • None of the twelve imports security-plugin.ts.
    • Their top level only declares: constants, sets, Maps, WeakMaps, a WeakSet, symbols, and objects/default-permission-sets.ts's default sets, which it builds from its own constants. The imported values their top level reads all come from @objectstack/spec and @objectstack/platform-objects, outside the bundle.
    • Both built bundles load at head (import() and require), so no top-level read meets an uninitialised binding.

No changeset: skip-changeset, per ruling 6348891160 item 4.

Proof of move-only

Measured on the re-derivation against 45f996231e. The head tree differs from it only by main's later commits, none of which touches these files.

  • The statement, by AST token. registerAuthzMiddleware's body is one statement, with the same TypeScript token sequence as the base statement: 5,471 tokens. Each of its 1,957 lines is the base line with two leading spaces removed. Multi-line JSDoc text tokens inside it are re-indented one level, like every other line, so the comparison dedents those tokens one level, and only those.

    • The base statement is the only one in start() whose handler names chainNext; the other three registerMiddleware calls stay, and none is left at head that matches it.
  • start() outside the statement. The head text of start() equals the base text exactly, outside the one statement, which is now a 44-line stub block ending in the .call. Its signature, docblock and every other statement are byte-identical: R1, cut 11's stub, the comment above, the log line after, and R4. start() goes from 2,789 lines to 876.

  • The stub's host literal. It has 32 members, exactly the Pick, in the same order. Each is in the form its own declaration calls for: 25 methods as this.m(...a) closures, engineOperationScope by value (declared readonly), and six getters, each over a read… local that reads this.m.

  • The host interface. 32 members, each typed SecurityPlugin['m'], fields readonly, and its set equals the Pick.

  • The two declarations. WriteImageCheckSeam's text in the module, docblock included, is the base text with export added, and nothing else. userFacingDenialMessage's is the base text. Neither is left in security-plugin.ts.

  • Class side. Every other class member is byte-identical to base: 125 named members plus the constructor. The class has 127 members at base and at head.

  • Top level of security-plugin.ts. Of the 112 top-level statements at base other than the class, 93 are byte-identical at head. The other 19 are:

    • eight imports that each lose the names listed above (one of them also loses the [#8220] comment paragraph above it);
    • nine imports that are gone, two of them with the comment lines above them ([#7414], [#19989]);
    • the two declarations, which are gone from the file.

    The one new statement is the module's import, with its comment. The check asserts the exact list of 23 dropped names and 3 added names (registerAuthzMiddleware, the type AuthzMiddlewareHost, the type WriteImageCheckSeam), and that no dropped name is still referenced in the file.

  • The module's imports. 31 names, each imported from the specifier security-plugin.ts imports it from, with no type import turned into a value import. ./security-plugin.js gives only the type SecurityPlugin and the two staying exports. The seven comment lines that left security-plugin.ts above imports are exactly the module's import comments, in order.

  • Negative controls. Eleven mutations of one copy each, and every one fails the check it targets:

    • in the moved statement, one denial code changed (record_change_not_allowed to record_access_denied);
    • in the moved statement, inside feat(security): the external organization role is the EXTERNAL principal — owner-only org-wide default, explicit grants only #25541's grant stamp, the read stamp's 'read' changed to 'write';
    • in start(), the log line after the stub changed;
    • in the stub, the epoch getter turned into a by-value member;
    • in the stub, collectRLSPolicies left out;
    • in the host interface, readonly dropped from epoch;
    • in userFacingDenialMessage, one space added to its signature;
    • in security-plugin.ts, recordNotFoundError left in the core import;
    • in the module, assertReadableQueryFields imported from ./field-masker.js;
    • in the module, the [#19989] import comment edited;
    • in security-plugin.ts, one space added to init()'s signature.
  • git's own view. This is git diff --color-moved=plain --color-moved-ws=allow-indentation-change 45f996231e over the two source files.

    • Of the 2,077 deleted lines in security-plugin.ts, git sees 2,068 as moved. The other 9 are the four one-line imports that lose names (core, spec/security, spec/data, rls-compiler), four specifier lines of multi-line imports, and the interface WriteImageCheckSeam { line, which gains export.
    • Of the 2,218 lines of the new module, 2,092 show as moved. The other 126 are among the module's own lines: its header, the regrouped imports, the RlsFilterOptions alias, the host interface, the function's docline and signature, and the export interface WriteImageCheckSeam { line.
  • Size. 4,369 changed lines (+2,284 / −2,085) over 5 files: security-plugin.ts (+58 / −2,077, from 8,183 to 6,164 lines), the new module (+2,218), the page (+3 / −3), the census script (+4 / −4) and ADR-0055 (+1 / −1).

ADR anchors (ruling 6353073966 item 2)

  • node scripts/check-adr-symbol-anchors.mjs exits 0 at base 45f996231e, on the re-derivation, on today's main 9c43e19f65 and on this head: 2,384 anchors over 142 records, 466 symbol anchors, at each.
  • pnpm check:adr-anchors exits 0 at each. Anchored files stay at 84.
  • Decision letters on the anchored surface. On the merge-base of the measured re-derivation, 45f996231e: 1946. On the re-derivation: 1895. On today's main 9c43e19f65: 1947. On this head fa9e98b036: 1896. The same 51 leave either way. That gate counts ADR-NNNN Dk citations in each anchored file and in each entry's own invariant text. All −51 are R3's letters leaving security-plugin.ts, which holds 236 at base and 185 at head:
    • ADR-0090 D10 ×20, ADR-0138 D1 ×6, ADR-0096 D5 ×4, ADR-0066 D2 ×3, ADR-0058 D4 ×2, ADR-0066 D3 ×2, ADR-0090 D12 ×2, ADR-0105 D5 ×2;
    • ×1 each: ADR-0029 D8, ADR-0056 D2, ADR-0057 D1, ADR-0058 D6, ADR-0095 D1, ADR-0111 D3, ADR-0131 D8, ADR-0139 D1, ADR-0139 D5.1, ADR-0139 D6.
    • Three letters leave security-plugin.ts entirely: ADR-0058 D6, ADR-0105 D5 and ADR-0131 D8.
    • feat(security): the external organization role is the EXTERNAL principal — owner-only org-wide default, explicit grants only #25541's lines cite a ruling of record, not a decision letter, so they move no letter. The stub and the new import comment cite none. No letter is planted and none is invented.
  • Every moved citation is still in the tree, in the module its code moved to. Citations that resolve tree-wide go from 52,280 across 6,688 files to 52,282 across 6,689 (on 45f996231e), and from 52,285 across 6,691 to 52,287 across 6,692 (on 9c43e19f65). The new file is the module. The two added citations are in its header, which names ADR-0056 and ADR-0090 D12 once each, describing two of the handler's steps.
  • The anchor map covers every ADR symbol anchor that names security-plugin.ts or the new module, at both ends: 27 citing lines over 18 symbols, measured with extractAnchors and symbolSegmentResolution from scripts/symbol-anchors.mjs (measurement, not a new gate). One anchor changes file: #rlsFilter, by the re-anchor above. No anchor changes class.
    • Several anchored members are named inside the stub's host literal and its Pick (#computeControlledByParentFilter, #assertControlledByParentWrite, #computeRlsFilter, #composeWriteTenantWall, and now #collectRLSPolicies). Each still resolves as declaration on its method.
ADR line anchor base head
ADR-0049:49 #SecurityPlugin security-plugin.ts · declaration · :1312 (class) security-plugin.ts · declaration · :1206 (class)
ADR-0055:123 #computeControlledByParentFilter security-plugin.ts · declaration · :7086 (computeControlledByParentFilter) security-plugin.ts · declaration · :5067 (computeControlledByParentFilter)
ADR-0055:125 #assertControlledByParentWrite security-plugin.ts · declaration · :7440 (assertControlledByParentWrite) security-plugin.ts · declaration · :5421 (assertControlledByParentWrite)
ADR-0055:125 #assertMasterRowEditable security-plugin.ts · declaration · :7566 (assertMasterRowEditable) security-plugin.ts · declaration · :5547 (assertMasterRowEditable)
ADR-0055:135 #CBP_MAX_CHAIN_DEPTH security-plugin.ts · declaration · :751 (top-level) security-plugin.ts · declaration · :700 (top-level)
ADR-0055:24 #computeRlsFilter security-plugin.ts · declaration · :6460 (computeRlsFilter) security-plugin.ts · declaration · :4441 (computeRlsFilter)
ADR-0055:24 #rlsFilter security-plugin.ts · declaration · :4064 (start) authz-middleware.ts · declaration · :2074 (registerAuthzMiddleware)
ADR-0055:5 #computeControlledByParentFilter security-plugin.ts · declaration · :7086 (computeControlledByParentFilter) security-plugin.ts · declaration · :5067 (computeControlledByParentFilter)
ADR-0055:58 #assertControlledByParentWrite security-plugin.ts · declaration · :7440 (assertControlledByParentWrite) security-plugin.ts · declaration · :5421 (assertControlledByParentWrite)
ADR-0066:112 #writeCheckPolicies security-plugin.ts · declaration · :1248 (writeCheckPolicies) security-plugin.ts · declaration · :1142 (writeCheckPolicies)
ADR-0096:181 #getReadFilter security-plugin.ts · declaration · :5195 (getReadFilter) security-plugin.ts · declaration · :3176 (getReadFilter)
ADR-0096:182 #getReadFilter security-plugin.ts · declaration · :5195 (getReadFilter) security-plugin.ts · declaration · :3176 (getReadFilter)
ADR-0096:183 #getReadFilter security-plugin.ts · declaration · :5195 (getReadFilter) security-plugin.ts · declaration · :3176 (getReadFilter)
ADR-0096:199 #getReadFilter security-plugin.ts · declaration · :5195 (getReadFilter) security-plugin.ts · declaration · :3176 (getReadFilter)
ADR-0099:317 #hasPlatformAdminPosture security-plugin.ts · declaration · :6487 (hasPlatformAdminPosture) security-plugin.ts · declaration · :4468 (hasPlatformAdminPosture)
ADR-0099:68 #manage_metadata security-plugin.ts · literal · :377 (top-level) security-plugin.ts · literal · :363 (top-level)
ADR-0105:93 #collectRLSPolicies security-plugin.ts · declaration · :7620 (collectRLSPolicies) security-plugin.ts · declaration · :5601 (collectRLSPolicies)
ADR-0123:4 #composeWriteTenantWall security-plugin.ts · declaration · :6778 (composeWriteTenantWall) security-plugin.ts · declaration · :4759 (composeWriteTenantWall)
ADR-0126:406 #nav_permission_sets security-plugin.ts · literal · :1740 (init) security-plugin.ts · literal · :1634 (init)
ADR-0138:152 #isPrincipalLessContext security-plugin.ts · declaration · :624 (isPrincipalLessContext) security-plugin.ts · declaration · :573 (isPrincipalLessContext)
ADR-0138:153 #SecurityPlugin.getMetadataReadableFields security-plugin.ts · declaration · :5330 (getMetadataReadableFields) security-plugin.ts · declaration · :3311 (getMetadataReadableFields)
ADR-0138:248 #SecurityPlugin security-plugin.ts · declaration · :1312 (class) security-plugin.ts · declaration · :1206 (class)
ADR-0138:265 #SecurityPlugin security-plugin.ts · declaration · :1312 (class) security-plugin.ts · declaration · :1206 (class)
ADR-0138:427 #SecurityPlugin.resolvePermissionSetsForContextUnmemoized security-plugin.ts · declaration · :5924 (resolvePermissionSetsForContextUnmemoized) security-plugin.ts · declaration · :3905 (resolvePermissionSetsForContextUnmemoized)
ADR-0139:222 #getReadFilter security-plugin.ts · declaration · :5195 (getReadFilter) security-plugin.ts · declaration · :3176 (getReadFilter)
ADR-0139:223 #computeDelegatedReadFilter security-plugin.ts · declaration · :5249 (computeDelegatedReadFilter) security-plugin.ts · declaration · :3230 (computeDelegatedReadFilter)
ADR-0139:246 #computeControlledByParentFilter security-plugin.ts · declaration · :7086 (computeControlledByParentFilter) security-plugin.ts · declaration · :5067 (computeControlledByParentFilter)

Censuses, and the collapsesOntoRead reading

Each census was run at base 45f996231e and on the re-derivation, and compared site by site, with the module path mapped back to security-plugin.ts and registerAuthzMiddleware mapped back to start.

  • isystem-census: 149 sites at both ends, 0 differences. The one moved read is the row-1 short-circuit, if (opCtx.context?.isSystem): security-plugin.ts:2307 (start) at base, security-plugin-authz-middleware.ts:317 (registerAuthzMiddleware) at head. Scanned files go from 406 to 407, the module.
  • check-system-context-census exits 0 at both ends with the same summary: 149 sites in 22 packages across 77 files, 129 symbols, 142 cited against 142 required, 164 anchors, 21 declared non-read, 20 row references. It exits 0 on today's main and on this head too.
  • collapsesOntoRead, at both ends. The gate holds the ledger row's declaration to the census in both directions, and both ends agree:
    • base: the row names security-plugin.ts#start and declares the overlap; the census finds exactly 1 elevation read in security-plugin.ts#start;
    • head: the row names security-plugin-authz-middleware.ts#registerAuthzMiddleware and declares the overlap; the census finds exactly 1 read there, and 0 in security-plugin.ts#start.
    • Without the ledger edit, the head reports [ledger-stale-collapse] (above).
  • tenant-audit-census: 238 sites at both ends, 0 differences, 176 non-engine calls, 87 engine types, 117 declared objects. The moved handler holds no site, feat(security): the external organization role is the EXTERNAL principal — owner-only org-wide default, explicit grants only #25541's lines included.
  • pnpm check:query-options-erasure exits 0 at both ends; its baseline key set is unchanged ("no files added").

Gates

All gates ran against base 45f996231e and on the re-derivation (7e3f354475), each in its own worktree after a full turbo run build there (both exit 0, 77 tasks).

  • pnpm --filter @objectstack/plugin-security test (vitest run): Test Files 222 passed (222); Tests 4628 passed, 45 skipped (4673). Base 45f996231e gives the same counts.
  • pnpm --filter @objectstack/plugin-security typecheck: exit 0 at head and at base. It runs tsc --noEmit, the scripts tsconfig, and check:test-typecheck.
  • pnpm --filter @objectstack/plugin-security build: exit 0. It produced the .d.ts and the bundles measured above.
  • row-serving-door-exposure.pin.test.ts and operation-private-keys.pin.test.ts (@objectstack/core): 8 of 8 pass at head and at base.
  • dispatch-gates --commands: 110 commands, the same set as on the first head. The --ran reconciliation shows 110 derived, 110 run and 0 NOT-MEASURED. The zero is derived: all 110 recorded an exit code, and none is 3.
  • Hand-run, because the series reads them every cut: check-adr-symbol-anchors, pnpm check:adr-anchors, check-platform-checklist (script and pnpm check:platform-checklist), check-scripts-symbol-anchors and its --self-test, pnpm check:query-options-erasure, the two census --json reads, check-system-context-census and its --self-test, and pnpm --filter @objectstack/spec check:liveness.
  • Artifact rosters: ten families keep their roster in a directory this change touches, so dispatch-gates marks their silence as no evidence. All ten were run: check-published-list-mirrors and its --self-test, check:authz-resolver, check:console-injection, check:engine-double-contract, check:error-code-casing, check:filter-alias-parity, check:i18n-stale-fill, check:published-readme-exports and check-dts-references --self-test. The package typecheck was run with them.
  • Base against head, all 128 commands. All 128 exit 0 at head and at base. 102 give identical output at both, once the worktree path and timings are masked. The other 26 are the same 26 families as on the first head, each read line by line:
    • These move as described above: check:adr-anchors (1946 to 1895 letters) and the two census --json reads (the module path and symbol of the one moved read).
    • The plugin-security suite's console output is not stable between runs: log lines from parallel workers, generated ids and timestamps. Its counts are the same at both ends.
    • check-issue-citations judges a change set, so it has nothing to read at base. At head it judged 134 citations across 2 files: 129 resolve, 4 resolve as pull requests and 1 is cross-repo.
    • check:merge-driver differs only by the name of the temporary repository it builds.
    • The other 20 move by exactly the one new file, and by nothing else: check-closing-keyword-parity, check:gitlink-declared and check:nul-bytes (tracked files), check-comment-mask-corpus, check-plugin-teardown-shape, check-position-name-fold-loaders (the same 8 references), check:duration-unit-keys, check:closed-ceremony-text, check:error-status-conformance (the same 511 producer sites), check:logger-receiver-detach, check:org-identifier, check:page-declaration-shape, check:pm-dispatch-gates, check:refd-timer-probe, check:vendor-version-stamps, check:engine-double-contract and check:error-code-casing, plus three that also count what the files hold:
      • check-undeclared-dep-imports: 3,341 to 3,342 files, and 2,914 to 2,919 @objectstack/* specifiers (the module imports from seven package specifiers; security-plugin.ts drops two);
      • check:doc-authoring: 1,456 to 1,457 parsed sources, and 105,684 to 105,793 strings read. The 109 new strings are the module's import specifiers net of the imports security-plugin.ts drops (+11), the new import's specifier (+1), the host's indexed-access keys (+32), the alias's (+1), and the Pick key lists in the function signature and the stub (+32 each);
      • check:skill-identifier-liveness: 140,292 to 140,294 implementation word tokens (registerAuthzMiddleware, AuthzMiddlewareHost), with the same 457 citations checked.
  • On this head's tree, which adds main's seven later commits to the re-derivation: check-adr-symbol-anchors, check:adr-anchors, check-system-context-census exit 0 (above). CI runs the rest on this head.

Acceptance notes


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa

…ecurity-plugin.ts

The one statement of start() that registers the authorization
middleware on the ObjectQL engine moves unedited to
security-plugin-authz-middleware.ts as registerAuthzMiddleware. It closed
over start()'s ctx, ql and metadata and nothing else, so it takes them as
parameters; start() keeps the statement's position, the comment above it
and the log line after it, and calls it from one block that hands it a
host literal of exactly the members it reads. The two module-level
declarations only the handler reads move with it: WriteImageCheckSeam,
which gains export so that security-plugin.ts can import it back
type-only for a docblock link, and userFacingDenialMessage. Import names
only the moved code reads leave security-plugin.ts, each whole import
with the comment lines that describe it.

The two readers that key on the moved read are re-pointed: the
system-context page's rows 1, 2 and 30 anchor the new function, and the
census ledger row that declares collapsesOntoRead for the owner_id guard
names it too. Move-only: no verdict changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa
…middleware

ADR-0055's context item 1 anchors the read-path RLS injection by the
rlsFilter local that start()'s middleware declared. That statement now
lives in security-plugin-authz-middleware.ts as registerAuthzMiddleware,
so the anchor names that file. One path changes; the sentence, the
symbol and every other anchor stay.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 23, 2026
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation labels Oct 23, 2026
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 73 documentable anchor(s).

51 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 9c43e19f659c40bb065601bda50c7e05250169f2.

⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 5 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 — 17 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 9c43e19f659c40bb065601bda50c7e05250169f2 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 9c43e19f659c40bb065601bda50c7e05250169f2

⚠️ 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 9c43e19f659c40bb065601bda50c7e05250169f2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

main gained the EXTERNAL rung's grant stamp inside start()'s
authorization middleware (step 2.6), the statement this branch moves,
and it edits the @objectstack/core import line this branch also edits.
The conflict in security-plugin.ts is resolved by carrying those lines
with the statement into security-plugin-authz-middleware.ts: the host
gains collectRLSPolicies, which they read, and the three names only they
import (isExternalAudienceContext, grantRowPolicySpeaks,
stampExternalGrant) move with them. The resulting tree is the one the
scripted move gives when it is re-run on the new main. Move-only: no
verdict changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa
@objectstack-fleet
objectstack-fleet Bot disabled auto-merge October 24, 2026 01:24
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · 2026-10-24T01:33Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25223 cut 12 (R3, the authorization middleware; claim 6375757728; os-dev-report 6378177397, superseding 6376821532) · reviewed head fa9e98b036 · merge base 9c43e19f65

  • Tier H, and how it lands: check-governed-merges --pr 25545 reports GOVERNED (1 of 5 paths hit the register), with 4,369 changed lines. os-zhuang (GOVERNED_APPROVERS) APPROVED it at 2026-10-24T00:20Z, on 1b2378c299. Under ruling C of 2026-09-13 (「approve 后不管后续改动都由席位落地」, check-governed-queue-guard.mjs), the owning seat lands it. The seat disarmed that approver's auto-merge at 2026-10-24T01:24Z, after the re-derived head was pushed, so that no head would queue unmeasured. The seat re-arms it with this ACCEPT.
  • Shape:
    • The ql.registerMiddleware(…) statement in start() moves unedited into security-plugin-authz-middleware.ts as registerAuthzMiddleware(this: Pick<AuthzMiddlewareHost,…>, ctx, ql, metadata).
    • It has one move commit (b7b408e502) and the ADR-0055:24 re-anchor alone in 1b2378c299. That is followed by a merge of origin/main, never a force-push once there is an approval (AGENTS §3 ④). The merge's tree equals the scripted re-derivation on 45f996231e (tree 8704c395d0, per the report).
  • The feat(security): the external organization role is the EXTERNAL principal — owner-only org-wide default, explicit grants only #25541 hunk: the step-2.6 EXTERNAL grant stamp, from "[Ruling of record 6355206139, letter B]" through the two stampExternalGrant calls, is the 16 lines feat(security): the external organization role is the EXTERNAL principal — owner-only org-wide default, explicit grants only #25541 added. The seat diffed it, with indentation stripped, between security-plugin.ts at the merge base and the moved module at head, and it is identical. The host gains collectRLSPolicies. isExternalAudienceContext, grantRowPolicySpeaks and stampExternalGrant move with the statement's imports.
  • .d.ts, ruling 6348891160 item 1, re-run by the seat in its own worktree:
    • turbo run build at 9c43e19f65 and at this head.
    • Raw 7e6a99f7d59debba… / normalised e22a714da70e9efc… on both index.d.ts and index.d.mts at both ends. The raw diff is 0 lines.
    • The seat's positive control makes line 94, private permissionEvaluator;, public. That moves both hashes, to raw c2d8413e71c1b931… and normalised aed83afe2a517670….
  • Gates re-run by the seat, all exit 0 at base and at head:
    • check-adr-symbol-anchors: 2,384 anchors at both ends.
    • check-tenant-audit-census: byte-identical output, 238 sites.
    • check-system-context-census: identical output, 149 sites over 77 files, with 142 of 142 symbols cited.
    • check-adr-anchors
    • check-platform-checklist
  • Two negative controls, run by the seat on this head:
    • Restoring docs/adr/0055-…md to the merge base makes check-adr-symbol-anchors exit 1 with [unresolved-symbol] …0055…md:24 security-plugin.ts#rlsFilter. So the re-anchor is owed, and it is the only ADR edit.
    • Restoring scripts/check-system-context-census.mjs to the merge base makes the census exit 1 with ledger-stale-collapse on the NON_READ_ANCHORS row. So the collapsesOntoRead repoint is owed. The report shows the read: at base, 1 read in security-plugin.ts#start; at head, 1 read in …authz-middleware.ts#registerAuthzMiddleware and 0 in #start.
  • Readers: system-context.mdx rows 1, 2 and 30 are repointed to #registerAuthzMiddleware. They are path-only edits, and row 1b is untouched.
  • Decision-letter count, read by the seat: 1947 at the merge base and 1896 at head, with 84 anchored files at both ends. The −51 are all R3's own letters leaving security-plugin.ts, itemised in the PR. No anchor paragraph describes the statement, apart from the ADR-0055 sentence re-anchored above.
  • Report: 128 gates exit 0 at both ends. The suite gives 222 files and 4,628 / 45 at both ends. Exports: 66.
  • CI on this head: 33 success and 10 expected skips, none failed so far. 5 runs are still in progress, and auto-merge waits on them.
  • Mergeable: git merge-tree onto today's main (ce18e73483) is clean.
  • Landing: ready (already set) + auto-merge re-armed through the relay. The card stays open and dispatched. R4 (claimTargetAdminUserId as a get/set pair, citing refactor(plugin-auth): move AuthManager's request routing into auth-manager-request-routing.ts (#25224 cut 10) #25508) is claimed afresh as cut 13 after this merges.

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 40052496958 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (2/6) — 失败步骤: Run this shard's tests

    @objectstack/plugin-security:test:  FAIL  src/delegated-organization-membership.test.ts > [#25107 · ADR-0090 D10] parity: each probe agrees with the engine operation it describes > `group`: refused by
      ↳ 失败原因: @objectstack/plugin-security:test: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/delegated-organization-membership.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 0 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 24, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Queue triage · domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · 2026-10-24T02:37Z

Queue build 40052496958 went red on one test: delegated-organization-membership.test.ts › [#25107 · ADR-0090 D10] parity › group. The reason given was Test timed out in 5000ms, not an assertion. The same shard's objectql run shows import 2260.49s, so the runner was heavily loaded.

Seat re-run of the file, on head fa9e98b036 and on main's plugin-security/src, twice each:

tree group parity isolated parity file
PR head 682 ms, 726 ms 708 ms, 673 ms 66/66
main 673 ms, 686 ms 667 ms, 668 ms 66/66

The test's timing is unchanged by the move (about 0.7 s against a 5 s budget), and it passes at both ends. That is a load timeout, not a regression (triage case 2). git merge-tree against today's main is clean. The seat re-arms once, and the ACCEPT 6378223973 stands. If the queue goes red again on the same signature, the seat stops and opens a flake issue rather than re-queueing.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit df893f6 Oct 24, 2026
49 checks passed
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/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants