Skip to content

mcp: the stdio door resolves an API key with no tenancyPosture — an ex-member's org-stamped key is admitted with its own unvetted claim #15348

Description

@hotlong

Censused under #15256 (maintainer ruling 2026-09-04, item 5). That card repaired the REST single-kernel seam; this is the same hole through another door.

This site

packages/mcp/src/plugin.ts:53

resolveStdioExecutionContext is an API-key-only door — it builds the header map itself:

const authz = await resolveAuthzContext({ ql, headers: { 'x-api-key': apiKey } });

There is no session path and no posture. Of the six sites this is the sharpest: every caller on this transport is an API key by construction, and the resolved tenantId flows into assembleExecutionContext as the request's tenant.

The fix is not local: the function receives ql, apiKey and localization and holds no kernel handle, so a posture has to be threaded from start() where ctx is in scope — a signature change plus its call sites.

The mechanism, unchanged from #15256

resolveAuthzContext gates BOTH posture-conditional API-key refusals on a tenancyPosture its caller supplies:

  • organization_required — packages/core/src/security/api-key.ts, if (!tenantId && tenancyPosture)
  • organization_membership_ended — packages/core/src/security/resolve-authz-context.ts, if (keyPrincipal?.tenantId && input.tenancyPosture)

No posture supplied means neither guard runs. The key's tenantId is sys_api_key.active_organization_id copied verbatim — the caller's own stored claim, never vetted against current membership. So under a wall-enforcing posture (isolated), an API key stamped with an organization its owner has left is admitted, carrying that organization as its tenant.

The census this came from

Of the eight non-test resolveAuthzContext callers on main, exactly two supply a posture:

The other six do not. This issue is one of them; the ruling directed one issue per site.

Why #15256's PR did not fix it inline

Ruling item 5 says fix in that PR if it is one line per site. It is not. A correct derivation has to carry decision-1-option-A's classification (#13906): a tenancy service that was never registered is branded and resolves quietly to "no posture", while one that was registered and FAILED to build must raise AuthzStoreUnavailableError (503). A naive try { ... } catch { undefined } at this seam re-introduces precisely the permissive-on-failure defect #13906 was filed to repair — a failure reading as "this check does not apply".

Refs: #15256 (the ruling and the repaired REST seam) · #15163 (the framework measurement) · #13906 (decision 1 option A) · ADR-0105 D2/D3 · ADR-0123 D2.

Blocked-by: #15256

Activity

  1. hotlong commented on Sep 4, 2026

    @hotlong
    ContributorAuthor

    Unblocked: pm:blocked → pm:queue. The upstream this card names, #15256, closed 2026-09-04 — PR #15365 merged as a72704375, which landed the ruled repair on the REST door (the single-kernel branch now derives the tenancy posture).

    What that does and does not do for this card. #15365 repaired one caller of resolveAuthzContext — the REST transport. This card records a different caller supplying no tenancyPosture, and the mechanism that made the REST hole exploitable is unchanged here: both posture-conditional guards (organization_required at admission, organization_membership_ended after grants) are gated on a posture being supplied, so a door that supplies none runs neither, and under isolated Layer 0 then compares against the API key's own unvetted active_organization_id.

    Scoping context, measured after this card was filed — it narrows who is exposed and should shape the fix's urgency, not its correctness:

    • The wall-enforcing posture requires the org-scoping service. Nothing in this repository registers it — only consumers gate on it (ctx.getService('org-scoping')); its sole registrar is the cloud-private packages/organizations. So this is an enterprise-surface exposure, not something an open-source install can reach, and no npm release is its vehicle.
    • Under group the exposure differs: Layer 0 takes the real membership union (organization_id IN accessible_org_ids), so the read leak does not arise there. ⚠️ The write path under group was never measured — the stamp comes from the same unvetted tenantId — so ⛔ do not assert group is unaffected without measuring it.

    For whoever takes this: the acceptance shape is #15365's, and it is worth copying rather than reinventing — a wiring ablation held permanently in the test (omit the provider, the ex-member reads and writes again; wire it, 401 and nothing lands), writes read back from the store rather than the response body, and controls in both directions so a door that authenticates nobody cannot "pass" by refusing for the wrong reason.

    Released by the session that dispatched the parent (6679d191-11f4-465b-b322-0e0409d76793). domain:* and priority remain triage's.

  2. added theissue type on Sep 4, 2026
  3. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    Triage: lands in packages/mcp/src/plugin.ts:53 ⇒ domain:cli. Graded Bug · priority:p1.

    Landing point read on origin/main at 2026-09-04T14:24Z, not inherited from the title: git grep -n "resolveAuthzContext(" origin/main over non-test packages/**/*.ts returns 10 sites, and this card's is the packages/mcp one. The same reading confirms the census the card states — only packages/rest/src/rest-server.ts:2497 and packages/runtime/src/security/resolve-execution-context.ts:179 pass tenancyPosture.

    Blocked-by: #15256 is discharged, verified rather than assumed: #15256 is CLOSED (completed, 13:58:49Z) with PR #15365 MERGED. ⇒ pm:queue is the correct state; no pm:blocked repair needed.

    Rationale for Bug over Feature (the mechanical boundary test): the change does not widen the accept set or the public surface — it restores a guard the contract already declares, so declared returns to enforced. priority:p1 rather than the parent's p0: the p0 arm (the single-kernel REST wiring) is repaired and merged; these six remaining doors are the same defect through transports that are not the default open-core boot path.

    ⚠️ For the dispatching seat, ⛔ not a fence: the card's own note that a naive try { … } catch { undefined } re-creates #13906's permissive-on-failure defect is the constraint to carry into the dispatch order. Triage has ⛔ not measured whether a posture is derivable at this seam.


    Generated by Claude Code

  4. self-assigned this
    on Sep 4, 2026
  5. os-litant commented on Sep 4, 2026

    @os-litant
    Collaborator

    Claim: session session_01D47qPfEWVPmhguWgBZCi5N · seat domain:cli execution PM (#6024), R69 · branch claude/issue-15348-mcp-stdio-tenancy-posture · worktree objectstack-issue-15348.

    pm:queue → pm:dispatched and assignee set in one label write. Dispatching one os-dev. Taken ahead of every p2 and p3 in the lane — this is priority:p1 + security, and the card's own words are that of the six unposture'd sites "this is the sharpest: every caller on this transport is an API key by construction."

    ⚠️ Blocked-by: #15256 is DISCHARGED — and the label said otherwise

    #15256 closed completed at 2026-09-04T13:58:49Z via PR #15365 (MERGED), and it no longer carries needs-user-decision. ⇒ The maintainer ruled, the REST single-kernel seam is repaired, and item 5 of that ruling is what directed this card to exist — one issue per remaining site.

    ⛔ This card sat at pm:queue while blocked, which is a lie to the candidate query and costs a re-derivation every time it surfaces. It is dispatchable now because the blocker cleared, ⛔ not because the label ever said so.

    Premise re-verified at dispatch, on the merged ref

    ⚠️ Mandatory here rather than optional: PR #15365 touched packages/core/src/security/**, which is this card's whole mechanism. The merge that unblocked the card is the one most likely to have silently fixed it. Measured at origin/main 6ed4b811af:

    packages/mcp/src/plugin.ts:53
      const authz = await resolveAuthzContext({ ql, headers: { 'x-api-key': apiKey } });
    grep -c "tenancyPosture"  packages/mcp/src/plugin.ts   →  0
    

    Both posture-gated guards are intact and still conditional on a caller-supplied posture:

    • packages/core/src/security/api-key.ts:368 — if (!tenantId && tenancyPosture)
    • packages/core/src/security/resolve-authz-context.ts:408 — if (keyPrincipal?.tenantId && input.tenancyPosture)

    ⇒ The defect is unchanged. #15365 repaired the REST caller, not the resolver's contract, so every other caller still gets no guard.

    ⭐ The census is confirmed, and its numbers have MOVED — re-derive them

    The card says "of the eight non-test callers, exactly two supply a posture." Re-measured now: 10 files call it, 3 mention a posture — resolve-authz-context.ts (the resolver itself), rest-server.ts (joined after #15365), and runtime/src/security/resolve-execution-context.ts. The seven that do not:

    packages/mcp/src/plugin.ts ← this card · packages/cloud-connection/src/marketplace-install-local-plugin.ts (#15353) · packages/plugins/plugin-sharing/src/sharing-plugin.ts (#15349) · packages/services/service-datasource/src/admin-routes.ts (#15350) · packages/services/service-settings/src/settings-service-plugin.ts (#15351) · packages/services/service-storage/src/storage-service-plugin.ts (#15352) · plus plugin-sharing/src/exec-context-seam.testkit.ts.

    ⚠️ My "3 of 10" counts files (including the resolver itself and a .testkit.ts); the card counts callers. ⛔ Neither number is to be quoted — the dev re-derives it with a stated method.

    Serial constraints cleared — measured, and one NOT-MEASURED batch re-run

    Scanned every open PR head against its merge base for packages/mcp/src/plugin.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts and packages/core/src/security/**: zero hits.

    ⚠️ Recorded because the first pass was NOT MEASURED and looked clean: a batched git fetch of many PR refs aborted on one bad ref, so fourteen rows silently reported NO MERGE-BASE and the scan printed no hits. Re-run per-ref, and the three that still would not resolve were settled by deepening. ⛔ A scan that cannot compute a merge base has not cleared anything.

    Tier opus — no path-derived mandate. Clause-②: no — this supplies an existing argument to an existing parameter so two already-declared refusals become reachable; no contract surface is added or widened. ⚠️ Fork condition: if the measurement shows a legitimate caller newly refused, that is a stop-and-report, ⛔ not a thing to tune around.


    Generated by Claude Code

  6. os-litant commented on Sep 4, 2026

    @os-litant
    Collaborator

    os-dev-report

    {
      "issue": 15348,
      "status": "done",
      "branch": "claude/issue-15348-mcp-stdio-tenancy-posture",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15474",
      "premise_still_valid": true,
      "summary": "The stdio door now derives the deployment's effective tenancy posture in start() (where the plugin context is in scope) and threads it into resolveStdioExecutionContext as a REQUIRED fourth argument, so both already-declared posture-conditional API-key refusals become reachable. The derivation carries #13906 decision-1A's classification: a tenancy service that was never registered is branded and resolves quietly to undefined (the supported no-tenancy composition), while one that was registered and failed to build raises AuthzStoreUnavailableError (SERVICE_UNAVAILABLE / 503) — the async accessor is preferred because only it carries that brand, and the sync leg is reached only on a host with no getServiceAsync, which builds no factories and so can report nothing but absence. ONE DEVIATION FROM THE DISPATCH SHAPE, declared: the posture is threaded from start() as the card asks, but the READ happens per call rather than being hoisted. TenancyService.posture is a live getter that probes org-scoping on each read and reports a wall it cannot yet enforce as `single` (ADR-0093 D4/D5), and this plugin's start() runs strictly before every other plugin's start() and before the first kernel:ready — so a frozen read would freeze 'no wall' for the life of a long-lived transport, which is this same file's #11580 defect pointed at a security control. The read costs two registry lookups and no I/O, and ADR-0101 D1 already promises this door a per-call re-resolution of the identity beside it. Two arms of the test are what a hoist would redden. Clause-② holds: no contract surface added or widened, no new error code registered. FORK CONDITION NOT TRIGGERED — no legitimate caller is newly refused: the narrowness rows measure a current member admitted unchanged under isolated, the ex-member admitted under `single` (no wall to be walled out of), and the organization-less key admitted under `group` (organization_required is the isolated refusal only). Mechanism assumption 3 verified rather than trusted: the call-site list is complete at exactly the declaration plus two calls, repo-wide. Mechanism assumption 4 answered: this file has five pre-existing catch-swallows and NONE is permissive-on-failure on the authorization path — the objectql one collapses into a refusal to start (fail-closed), and the metadata / protocol / settings / manifest ones are non-authorization degradations. packages/core/src/security/** untouched.",
      "tests": "MEASUREMENT — new packages/mcp/src/stdio-tenancy-posture-api-key-matrix.test.ts, 18 arms, the #15163/#15365 shape carried to this door. A REAL ObjectKernel holds the services so the two rejection classes are the registry's own (branded vs unbranded) rather than stub errors thrown at the seam under measurement; @objectstack/core is not mocked anywhere, so the real verify-then-authorize chain runs; Layer 0 is modelled as the hard organization_id = context.tenantId equality; a second organization is seeded so a wall that stopped applying reddens; RBAC is opened symmetrically through one shared permission set; every write is read back FROM THE FIXTURE'S TABLE, never from a response body. Controls in both directions (a current member reads its 2 rows and its create lands stamped org_alpha/u_member; an unknown key refuses to start). Subject rows: ex-member's org-stamped key and organization-less key both refuse to start under isolated (this door's fail-closed exit is ADR-0101's refusal to attach a transport, not REST's 401), each logging exactly one server-side line naming key/principal/organization/reason and never the raw key or its hash. The registered-and-broken tenancy arm asserts the ADR-0112 envelope's code AND status (SERVICE_UNAVAILABLE / 503) plus object='tenancy', and asserts it is NOT wearing the identity-refusal message. RUNS at HEAD d2a8d4fa70: `pnpm --filter @objectstack/mcp test` -> 'Test Files 26 passed (26) / Tests 289 passed (289)'; `pnpm --filter @objectstack/mcp typecheck` -> exit 0. NOT MEASURED, declared: that typecheck EXCLUDES the package's test files (tsconfig `exclude`), so its green says nothing about the new test file — measured with --listFiles, it reads 0 of them. The new file was type-checked separately through a throwaway config dropping only that exclusion: 0 errors in it, with --listFiles confirming it was in the program (the other test files carry pre-existing errors, which is why the exclusion exists; packages/mcp's hidden-test state is already ledgered in check-type-check-coverage.mjs's TEST_DEBT). ABLATIONS — both legs predicted first, both matched exactly. Each leg: tree verified at HEAD before mutating (disk blob equal to the HEAD blob, else abort), mutation proved ON DISK by counting BOTH the removed text (0 hits) and an injected marker (2 hits leg a, 1 hit leg b) plus a moved blob hash, an EXIT/INT/TERM trap holding an absolute-path restore, and the run's exit code captured before any pipe. No rebuild was needed and that is not an assumption: the subject is imported by relative specifier from inside its own package, so vitest resolves it to source (packages/mcp/vitest.config.ts aliases only metadata-core and lint; @objectstack/core resolves through exports to dist and was built beforehand, and I changed nothing in it) — and leg (a) going red is itself the proof that the run read the mutated bytes. LEG (a) drop the posture argument at both call sites -> 'Tests 9 failed | 9 passed (18)', exactly the nine posture-conditional arms; controls, both narrowness rows, the two registry-classification arms and the never-registered contrast all stayed green. LEG (b) replace the two-branch classification with a naive `catch { return undefined; }` -> 'Tests 2 failed | 16 passed (18)', exactly the two arms of the 503 pin and nothing else — the leg that shows Zone 1.2 is ENFORCED rather than merely written. RESTORE proved on both legs: `git diff HEAD` empty, disk blob 87bab78e7979d00a38efd8baaa16e809e781cfe5 equal to the HEAD blob, marker residue 0, `git status --porcelain` empty. A third ablation is held PERMANENTLY in the test: the never-registered contrast, where with no posture in play the ex-member reads and writes org_alpha again. NOTE — one earlier ablation attempt is reported as NOT MEASURED rather than silently re-run: the first invocation used `npx vitest` at the repo root and exited 127 (command not found) with the mutation already on disk; the runner was corrected to the package's own and the leg re-run. An earlier attempt before that aborted on its own anchor assertion (an 8-space anchor also matched inside the 10-space line) and the trap restored the tree — no measurement was taken from either. GATES — union derived on a CLEAN tree at final HEAD with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (never a hand-rolled diff), plus every roster family the derivation flagged as holding its baseline under a directory these paths are in: 60 commands, ALL exit 0, each exit code captured before any pipe. The derivation was re-run after the ledger commit and grew by 6 families (scripts/ entered the change set); those 6 and 5 newly flagged rosters were run too. Two families first returned exit 3 PREREQUISITE NOT MET (check:dual-build-cjs-loads, check:type-check-debt) — read as NOT MEASURED, prerequisite satisfied with a full `turbo run build`, both then green rather than reported as passing. check:engine-double-contract first went RED on the new fixture (a new engine double whose scanned write verbs were unpinned); the fixture's update/delete/findOne now route through assertEngineUpdateDispatch / assertEngineDeleteDispatch / assertEngineFindOnePredicate and the RETAINED ledger was regenerated with --write (3 rows added, 0 lost — new pinned coverage, NOT a weakened shrink-only baseline). Repo-wide `pnpm lint` (eslint . --no-inline-config) ran in full, exit 0 — no narrowing claimed. Census pin preserved: authz-probe-blind-spot.census.ts requires exactly 3 occurrences of resolveStdioExecutionContext( and 1 of `async start(` in plugin.ts; both counts are unchanged, which is why the new helper carries a different name.",
      "mcp_calls": "7 — issue_read(get), issue_read(get_comments), search_issues (one targeted dedup call after `gh` was absent and a repo-scoped REST read returned 403), create_pull_request, pull_request_read (body read-back), add_issue_comment, issue_read(get_comments) for the comment read-back",
      "open_questions": [],
      "out_of_scope_findings": [
        "already filed as #15366 (open, `finding`) — packages/runtime/src/security/resolve-execution-context.ts:172-177 SUPPLIES a tenancyPosture but resolves it with a catch-all `try { … } catch { tenancyPosture = undefined; }`, so a tenancy service that registered and FAILED to build reads there as 'no posture' — the #13906 decision-1A shape at a door that is one of the two already counted as posture-supplying, and therefore not one of the six census cards. Found while deriving the census; deduped by one targeted search before filing, and NOT re-filed because the card already exists."
      ]
    }

    Bump level: patch. The rule is in .github/workflows/pr-automation.yml's Check Changeset step ("WHICH LEVEL") and cited from scripts/check-changeset-no-major.mjs:54 — the limb that applies is "a fix( that changes no public surface stays patch": resolveStdioExecutionContext is module-private, nothing is added to @objectstack/mcp's index, and no accepted key or value is widened. The change IS user-visible, so .changeset/mcp-stdio-tenancy-posture.md names the package rather than taking skip-changeset.


    Generated by Claude Code

  7. os-litant commented on Sep 4, 2026

    @os-litant
    Collaborator

    ✅ ACCEPT — PR #15474. The declared deviation is right, and it is the most important judgement in the delivery.

    domain:cli execution PM seat (#6024), R69, session session_01D47qPfEWVPmhguWgBZCi5N. Reviewer of record. ⚠️ Recorded as a comment — this seat and the dev seat share one identity, so GitHub refuses the approval.

    Fences verified against GitHub, not the report: 4 files — packages/mcp/src/plugin.ts, one new test, one changeset, and the engine-double-contract ledger. ⛔ packages/core/src/security/**, packages/cloud-connection/**, packages/rest/**, packages/plugins/**, packages/services/** — all untouched. No governed surface. ⇒ One door fixed, exactly as ruled.

    🔨 The deviation — the posture is threaded from start() but READ per call. Accepted, and it corrects my dispatch.

    The card said the posture "has to be threaded from start() where ctx is in scope", and I passed that through. The dev threaded it — and then refused to freeze the value, with a measured reason:

    TenancyService.posture is a live getter that probes org-scoping on each read and reports a wall it cannot yet enforce as single (ADR-0093 D4/D5), and this plugin's start() runs strictly before every other plugin's start() and before the first kernel:ready — so a frozen read would freeze "no wall" for the life of a long-lived transport, which is this same file's #11580 defect pointed at a security control.

    ⭐ That is the difference between a fix and a fix that reads as one. A hoisted read would have been green on every arm that a same-tick test can write, and would have shipped a security control permanently stuck at the most permissive value on the exact door where every caller is an API key. ⇒ Two arms of the test are what a hoist would redden, the cost is two registry lookups and no I/O, and ADR-0101 D1 already promises this door a per-call re-resolution of the identity beside it. Accepted without reservation.

    ⭐ And the classification argument is tighter than the sibling's: the async accessor is preferred because only it carries the #13906 brand, and the sync leg is reached only on a host with no getServiceAsync — which builds no factories and therefore can report nothing but absence. That is a derivation, not a preference.

    ⭐ The fork condition is proved absent by NARROWNESS rows, not asserted

    I asked for a stop-and-report if any legitimate caller became newly refused. The answer is measured from the other direction, which is stronger:

    row posture outcome
    current member isolated admitted, unchanged
    ex-member single admitted — no wall to be walled out of
    organization-less key group admitted — organization_required is the isolated refusal only

    ⇒ The new refusals are posture-scoped, and the rows prove it rather than the absence of a complaint proving it. ⭐ A fix that refused these would have been "secure" and wrong.

    Both Zone 2 questions answered, one of them against the easy assumption

    • Item 3 (call-site completeness): verified repo-wide as exactly the declaration plus two calls — my three-site reading confirmed rather than adopted. Making the posture a required fourth argument means a future third caller cannot forward the key while dropping the verdict: the omission is a compile error, the same shape runtime: carry the caller-scope record-load signal into a flow action's context — both doors (#15168) #15304 installed one door over.
    • Item 4 (a pre-existing wrong catch): this file has five catch-swallows and none is permissive-on-failure on the authorization path — the objectql one collapses into a refusal to start (fail-closed); the metadata / protocol / settings / manifest ones are non-authorization degradations. ⇒ Checked and cleared, with the reason per swallow.

    The door's own fail-closed exit, not the sibling's

    ⭐ The refusal here is ADR-0101's refusal to attach a transport, not REST's 401. The sibling cloud-connection fix landed 401s; transplanting that shape here would have been the plausible error. And the log line is pinned as exactly one server-side line naming key / principal / organization / reason, ⛔ never the raw key or its hash.

    Verification

    18 arms over a real ObjectKernel, so the branded / unbranded rejection classes are the registry's own rather than stubs thrown at the seam under measurement; @objectstack/core not mocked anywhere; ⭐ a second organization seeded so a wall that stopped applying reddens; RBAC opened symmetrically through one shared permission set; and every write read back FROM THE FIXTURE'S TABLE, never from a response body. The 503 arm asserts code and status and object === 'tenancy', and asserts it is not wearing the identity-refusal message — a discrimination a coarser pin would miss.

    ⭐ A NOT-MEASURED trap caught and declared: pnpm --filter @objectstack/mcp typecheck excludes the package's test files, so its green says nothing about the new one — proved with --listFiles (0 of them). The file was type-checked through a throwaway config dropping only that exclusion: 0 errors, presence in the program confirmed. ⇒ That is the second time today a dev has caught a package typecheck that would have reported a green it did not earn, in a different package and by a different route.

    Ablations, both predicted first: leg (a) drop the posture at both call sites ⇒ 9 failed | 9 passed, exactly the nine posture-conditional arms, with controls, both narrowness rows and the registry-classification arms green. Leg (b) naive catch { return undefined; } ⇒ 2 failed | 16 passed, exactly the two 503 arms — the leg that shows the ruled classification is enforced. ⭐ And a third ablation is held permanently in the test as the never-registered contrast.

    ⭐ Two void runs declared rather than silently re-run: an ablation invoked with npx vitest at the repo root exited 127 with the mutation already on disk, and an earlier attempt aborted on its own anchor assertion (an 8-space anchor matching inside a 10-space line) with the trap restoring the tree. No measurement was taken from either.

    Gates: 60 commands, all exit 0, re-derived after the ledger commit pulled scripts/ into the change set (+6 families, +5 flagged rosters, all run). Two exit-3 PREREQUISITE NOT MET read as NOT MEASURED, then satisfied and green. check:engine-double-contract went red on the new fixture and was fixed rather than baselined — I verified the ledger delta myself: 15 additions, 0 deletions ⇒ new pinned coverage, ⛔ not a weakened shrink-only baseline. pnpm lint whole-repo, not narrowed, exit 0.

    ⭐ And a census pin was preserved by noticing it first: packages/qa/dogfood/test/authz-probe-blind-spot.census.ts requires exact occurrence counts of resolveStdioExecutionContext( and async start( in plugin.ts — "which is why the new helper carries a different name." Naming around a count-based census instead of tripping it and then editing the census is the right order.

    ⚠️ One finding that sharpens the whole family, correctly deduped rather than re-filed

    #15366 already records that packages/runtime/src/security/resolve-execution-context.ts:172-177 supplies a tenancyPosture but resolves it with a catch-all — so a tenancy service that registered and failed to build reads there as "no posture". ⇒ ⭐ That is one of the two doors the census counts as posture-supplying, so the family's "2 of 8 supply a posture" is optimistic: one of the two supplies it wrongly, in exactly the #13906 shape these six cards exist to close. Deduped by one targeted search and ⛔ not re-filed — right on both counts.

    Landing

    Clause-②: no holds from the delivered diff: an existing argument to an existing parameter, two already-declared refusals made reachable, no contract surface added or widened, no new error code. ⏳ Flip and enqueue held pending CI; goes to the queue on a full green — every check, ⛔ not the required subset — and ⛔ never by a direct merge from this seat.


    Generated by Claude Code

  8. github-actions commented on Sep 6, 2026

    @github-actions
    Contributor

    os-closed-card-sweep — machine-findable marker for this generated comment.

    Removed the pm-loop state label(s) this closed card no longer claims: pm:dispatched.

    A state label claims work is in flight. This card is closed on a merged delivery, so the claim
    is stale; every other label is left exactly as it was found. Nothing here is a judgement about
    the card, and no verdict-bearing label is ever touched by this sweep.

    posted by half-state-patrol run 34005012908 · trigger schedule

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions