You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] ResolvedAuthzContext.authRefusal has zero transport readers — both #8287 refusal reasons collapse to the generic anonymous 401 on every wire #14273
Found while measuring #13906 (PR #14272, the tenancy-posture seam legs). Recorded for triage; no severity asserted — the refusals themselves FIRE and fail closed, so this is dead declared observability, not a bypass.
The shape
resolveAuthzContext (packages/core/src/security/resolve-authz-context.ts) sets ctx.authRefusal = { reason, message } for the two posture-conditional refusals (organization_required at admission, organization_membership_ended after grants), and its doc block says a caller that can report WHY uses this surface. #8287's design comments say the point of refusing is to be "loud at call time instead of silently empty", and the organization_membership_ended message even explains "The key was not revoked — the membership that backed it ended."
Measured on today's tree (b93d4fd): resolveAuthzContext has eight non-test consumers across packages (rest-server, runtime resolve-execution-context, mcp plugin, plugin-sharing, service-datasource admin-routes, service-settings, service-storage, cloud-connection marketplace-install) and grep -c authRefusal is ZERO in every one of them. The only readers of the field anywhere are the resolver itself and its own unit test.
The consequence
A refused principal carries no userId, so every transport takes its anonymous path: on REST the refused ex-member key answers the same 401 UNAUTHENTICATED body as a request with no credential at all (driven in PR #14272's section 2 — the positive-control leg observes the reason only by calling the resolver directly). The half of #8287's loudness that distinguishes "your key's backing membership ended" from "you are not signed in" is authored, stored, and dropped before every wire.
#13906 is about failures being ABSORBED upstream of the refusals; this is about the refusal's answer being under-reported downstream even when everything works. Fixing it is a wire-contract judgement (which doors may disclose WHY admission was refused, and whether that leaks key-existence information to a caller probing with a stolen key) — a decision, not a mechanical repair.
Related, and distinct: #13906 (the absorb seams that can SKIP these refusals), #13255-family CONTEXT-LOST (a lost context also answers 401 — one more member of the crowd this reason disappears into).
Triage (R+89, triage seat, session session_019kDRpB7D2XzVzkaLp57T5D): graded → needs-user-decision · priority:p3 · security (topic marker) · domain:engine (packages/core/src/security/resolve-authz-context.ts) · type Task. finding removed. Reason for the inbox: the one option that gives the field a reader (disclosing on the wire why admission was refused) is an authorization-boundary judgement — the human floor — and the card itself says so. 决策分析(中文,按四维模板):
<!-- os-decision-facets --> 一句话问题:一把因「所属组织成员资格已终止」而被拒的 API key,今天在每条线路上得到的回答都和「没登录」一模一样;平台内部已经算出并写下了真正的原因,只是没有任何出口把它说出来。
四轴(业务立场;①权重 ≥50%):
① 项目长远合理性:零读者的声明面正是 ADR-0049 明令要移除的形状,A 缩小契约增生;若将来有拉动,应从 C(审计出口)而非线路出口开始加。⇒ 荐 A。
② 实际业务拉动:零 —— 无客户报「分不清 401 原因」;两条拒绝本身照常触发并 fail-closed。
③ 防 AI 犯错:B 会让 AI 写的客户端按「原因」分支,而这些原因是可被探测的;A / C 不给 AI 一个不该依赖的字段。
④ 创业阶段不扩散:A。
Maintainer ruling recorded — A: remove the zero-reader authRefusal surface
Director seat, summon #8, session session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-02.
Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-02, replying to decision batch #2 in which this card was item 3 with the recommendation A (fallback C). Verbatim reply: 「14361 你不处理;14261 现在就改,定位为 minor;其他同意」 — 「其他同意」 adopts the recommendation.
Ruled: A. Per ADR-0049 enforce-or-remove, ResolvedAuthzContext.authRefusal and the two reason texts it carries (organization_required, organization_membership_ended) are removed from packages/core/src/security/resolve-authz-context.ts and its unit test; the refusals themselves keep firing and keep failing closed exactly as today. Option B (disclose the reason on the wire) is not taken — it is a security-boundary question and stays on the human floor if it ever returns with pull. Option C (audit-only outlet) is the recorded fallback if a reader appears later; it starts from the audit layer, never from the wire.
Execution: domain:engine lane, p3, Clause-②: no (internal type, not a published contract); one pin that the field is gone with the reverse check. If #13906's phase 2 touches the same resolver, the engine seat serials behind it by symbol rather than by line.
State transition, same stroke: needs-user-decision → pm:queue. Ledger: director seat post #12708, summon #8.
Maintainer ruling recorded — A1: the removal stands, now shipped with the carriers a published-type narrowing is owed
Director seat, summon #11, session session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-03.
Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-03, replying to decision batch #17 in which this card was item 5 with the recommendation A1 (alternatives A2, HOLD), presented with the measurement that the earlier ruling's stated basis ("internal type, not a published contract", 5507414114) is falsified: ResolvedAuthzContext is public API of the published @objectstack/core 17.x and authRefusal is a public optional member in the emitted dist/index.d.ts. Verbatim reply: 「同意」 — adopts A1.
Ruled: A1. Option A of 5507414114 is re-affirmed on its merits (zero production readers anywhere; ADR-0049 enforce-or-remove), and executed with corrected carriers: Clause-②: yes (a member of a published type is removed); @objectstack/core changeset at minor with a **BREAKING** note and migration text per the repo's launch-window convention (precedent .changeset/action-doubled-redirect-refusal.md); needs:contract-review on the PR, reviewed in-seat before ready. File surface widened to the two files the earlier ruling did not name: packages/rest/src/execctx-authz-input-seam-reachability.test.ts (:408, :430 — replace the field assertions with the adjacent behavioural contrast) and the rationale block in packages/core/src/security/authz-store-unavailable.ts:47-56, rewritten so it cites the measurement that was acted on rather than a shape that no longer exists. ApiKeyRefusalReason stays (independently used by ApiKeyAdmission). Unchanged from the earlier ruling: the two refusals keep firing and keep failing closed; no transport change; option C (audit-only outlet) remains the recorded fallback if a reader appears. A2 (deprecate, remove at the next major) is rejected — 不渐进.
Execution: domain:engine lane, M, claude-fable-5 per clause ②, on the already-pushed empty branch claude/issue-14273-remove-authrefusal-surface (head 937ec142d) or a fresh one off origin/main; serial check against #13906 phase 2 is cleared as of 5512740244 and is re-run at dispatch.
State transition, same stroke: needs-user-decision → pm:queue. Ledger: director seat post #12708, summon #11.
Carrier hygiene — director seat, summon 13, session_01WXyGTWPbbreqXow7Z2pZCk, 2026-09-04 ~05:57Z. needs:contract-review removed from this card: no open PR references it (open-PR search, zero hits; positive controls #14754 → PR #14930 and #14180 → PR #15139 answered), so there is no reviewable increment to carry yet. Maintainer ruling 2026-08-28 (contract-review.md: 「⛔ 不预挂:可复审增量存在前永不挂标,提前挂已废止 —— 前瞻条款②事实住卡上裁决/分诊评论、Clause-② 申报与 --tier 输出;开载体恒 = 真实待审」). For the domain:engine seat that dispatched this card: the Clause-② expectation stays in the card's text and dispatch; hang the carrier on this card and the PR together the moment the diff exists. pm:dispatched, assignee and the rest of the label set untouched.
Cross-reference from the batch #33 ruling on #15256 (2026-09-04, maintainer 「同意」 to 1A · 2A · 3A) — director seat, session_01LsEjuNMPitCHwEfYftZ1um. A1 on this card stands unchanged: the zero-reader authRefusal field is still removed here, with the carriers a published-type narrowing owes. What changes is the premise the 09-02 grading rested on: once #15256's fix lands, the two posture-conditional refusals fire on every walled single-kernel deployment, so an operator needs to be able to see them. That exit is a server-side warn line at the decision point in resolveAuthzContext (key id, principal, organization, reason; the wire stays the generic 401) — owned by #15256's PR, not by this card. No overlap: this card deletes the field; that PR adds the log where the decision is made. If this card's dev lands first, the removal proceeds as ruled; if #15256 lands first, this card's diff rebases over one added line.
Measurement only — this card has read pm:dispatched for 3 days on a claim that implemented nothing. ⛔ No label touched.
domain:engine dispatching seat, 2026-09-06T13:0xZ. ⛔ Not a claim, ⛔ not a re-grade, ⛔ no label written — the standing claim belongs to another seat and I am not overriding it. Recording the reading because the card is in my lane and its state is currently invisible to dispatch.
question
reading
last substantive activity
os-musk, 2026-09-03T12:18:54Z — "⏸️ Dispatch failed on a tier rate limit — claim stands, waiting, nothing implemented"
any open PR referencing this card
none — all open PRs' titles and bodies intersected against 14273, zero hits
⇒ pm:dispatched is describing a round that never began. While it reads that way the card is excluded from every dispatch enumeration, so nothing will pick it up — on a card labelled security.
⭐ Why this is worth saying out loud rather than just fixing: the same leak was found on #15989 today (pm:dispatched with no round since 04:57Z) and corrected there, and the general form is that a dispatch state is only true while a round is actually running — a claim that fails to start leaves the card in a state that hides it. A rate-limit failure at claim time is exactly the case that produces it, because the failure happens after the flip.
⛔ I am not resolving it, because the claim is another seat's to release. Two clean routes, either of which takes one label write:
os-musk (or whoever holds that seat now) re-claims and dispatches it, or releases the claim.
Triage returns it to pm:queue if the claim is considered lapsed.
Until one of those happens this card is neither in flight nor dispatchable, and that combination is the only genuinely bad state in the machine.
Dispatching now — the 2026-09-03 claim is superseded. It implemented nothing, by its own account, and has been idle 4 days.
domain:engine dispatching seat, session session_01ARYe3yQTQCUFm5qPYNgKaJ, 2026-09-07T01:1xZ. ⛔ No grading, ⛔ no domain:* label produced, ⛔ no priority touched.
Why this seat is taking it rather than waiting
At 12:54Z on 09-06 this seat recorded the reading and deliberately did not write a label, because the standing claim belonged to another seat. Four days is long enough that deference has stopped being caution and started being the reason a security-labelled card is invisible to every dispatch enumeration. The claimant's own last word was 「⏸️ Dispatch failed on a tier rate limit — claim stands, waiting, nothing implemented」, and nothing has moved since.
pm:dispatched is left as it stands (it is now true again); zhuangjianguo is added to the assignees rather than replacing os-musk — the shared identity makes that field no proof either way, and this comment is the carrier that says who actually holds it.
⭐ The blocker the batch #33 cross-reference named is discharged, verified by content on origin/main3e7ef9c23
⛔ Not from the API's merged field. #15256's ruled item 2A — the server-side warn at the decision point — is in the tree:
probe
reading
firing control
warnApiKeyRefusal( call sites in packages/core/src/security/resolve-authz-context.ts
2 (:404 admission, :483 post-grant)
resolveAuthzContext in the same file = 3
warnSessionOrganizationClaimDropped (the session mirror, same ruling)
1 (:570)
—
⇒ the cross-reference's conditional 「if #15256 lands first, this card's diff rebases over one added line」 is the branch that applies. The operator exit exists; removing the dead field no longer removes the only record of the refusal.
⭐ And the source file now carries the instruction itself
packages/core/src/security/resolve-authz-context.ts:184, written by #15256's own PR:
ResolvedAuthzContext.authRefusal was that exit and never got a consumer (zero readers outside two test assertions); #14273's A1 ruling REMOVES the field in its own PR. ⛔ Not removed here — cross-referenced only. This log line is the operator exit that field never delivered.
The card's premise, re-measured on today's origin/main rather than carried from b93d4fd49f
authRefusal across packages/** (non-dist, non-node_modules):
⇒ the finding stands unchanged after #15256: still zero runtime readers, and the two comment references are cross-references to this card rather than consumers.
⚠️ The dispatched round re-derives all of the above from its own tree before editing — this table is the PM's reading and is not binding on it.
Clause ②
This seat's expectation, ⛔ not binding on the round: Clause-②: yes — removing a member from an exported interface narrows what @objectstack/core publishes. It is dispatched at, and will be reviewed at, the contract tier. The round re-derives it from its own diff and declares it in its own claim comment, in the bare spelling; ⛔ nobody fills that line in on its behalf.
Claim: domain:engine developer round on the A1 ruling (5522741447), dispatched by the PM seat in 5563509663 — session session_01ARYe3yQTQCUFm5qPYNgKaJ. Supersedes the idle 2026-09-03 claim by os-musk (its own last word: nothing implemented; branch claude/issue-14273-remove-authrefusal-surface stays an empty push and is not used).
Branch: claude/issue-14273-remove-dead-authrefusal-field
Clause-②: yes
Worktree ../objectstack-issue-14273 off origin/main at ac76425f0. The branch is pushed empty as the write-route probe (push exit 0).
Scope, per 5507414114 / 5522741447 / 5537709877. Remove ResolvedAuthzContext.authRefusal — the declaration and both writers in packages/core/src/security/resolve-authz-context.ts — repair the prose the removal makes false, relocate the refusal-reason pins onto the ruled operator exit (warnApiKeyRefusal, #15256 / 2A, already on origin/main at :404 and :483), and ship the carriers a published-type narrowing owes: an @objectstack/coreminor changeset with the BREAKING banner and an ADR-0087 disposition, needs:contract-review on this card and the PR together the moment the diff exists (not before, per 5536347794). ⛔ No transport change: both wires keep answering the generic anonymous 401 UNAUTHENTICATED. ⛔ The two warn lines and the session mirror are not touched.
File surface.packages/core/src/security/resolve-authz-context.ts, resolve-authz-context.test.ts, authz-store-unavailable.ts (rationale block, named by A1), packages/rest/src/execctx-authz-input-seam-reachability.test.ts (named by A1), the prose-only citations of the field that the removal falsifies (packages/plugins/plugin-auth/src/membership-ended-session.ts docblock; the parenthetical in .changeset/membership-ended-session-revoke.md), and one new .changeset/*.md. ⛔ Not touched: packages/core/package.json, packages/core/examples/**, packages/rest/src/rest-server.ts, rest-server-meta-read-org-scope.test.ts.
Derivation of the clause-② line.authRefusal?: { reason: ApiKeyRefusalReason; message: string } is a member of interface ResolvedAuthzContext, which packages/core/src/security/index.ts exports and packages/core/src/index.ts re-exports from the root entry of the published @objectstack/core (files: ["dist", ...], exports["."].types = ./dist/index.d.ts). Removing a member of a published interface narrows what the package declares — a compile break for any out-of-repo reader of the member. The declaration-surface diff (every published .d.ts / .d.cts, both legs built, with a firing control and rebuild mtimes) is re-measured before the PR opens and reported on the PR; the line above is the judgement, made from the diff this round will produce.
{
"issue": 14273,
"status": "done",
"branch": "claude/issue-14273-remove-dead-authrefusal-field",
"pr": "https://github.com/objectstack-ai/objectstack/pull/16442",
"head_sha": "5ecf881fc6ac9b1a31fec37fda75b81058f21fc0",
"premise_still_valid": true,
"clause2": "yes — derived from the built declaration surface, not recalled. Instrument: both legs built with tsup per package (no turbo) under the shared verify lock; base leg at ac76425f0, head leg at 5ecf881fc; every file under packages/core/dist diffed with sha256 and mtimes (index.d.ts mtime 1788743041 to 1788743504, so the rebuild is proven; logger.d.ts/.d.cts byte-identical). index.d.ts and index.d.cts each carry exactly two hunks: one JSDoc-only (the authz-store-unavailable rationale prose; not surface) and one that deletes the declaration text `authRefusal?: { reason: ApiKeyRefusalReason; message: string; }` from `interface ResolvedAuthzContext` (surface, a narrowing). Exported-name set 242 to 242 (blind to a member move by construction); member-level reading 17 to 13 lines with exactly those four removed; control `ResolveAuthzInput` block identical on both legs. Limb 2 does not move: the refusals fire identically on both legs (relocated reason pins green under the reverse mutation) and no wire answer changes. Sibling control: objectui at .objectui-sha a472b0716 has 0 files naming authRefusal against 6 naming resolveAuthzContext and 14 naming @objectstack/core. Declared bare as `Clause-②: yes` in claim comment 5563562951; scripts/pm/check-clause2-carriers.mjs --pair 16442 (checker blob 751b4a6e4 == origin/main c14ebfeb2's, verified after a fresh fetch) via redirect-then-read: REAL_EXIT=0, 'both carriers agree'.",
"zone2": {
"2.1_zero_runtime_readers": "held — git grep authRefusal over all tracked files at base: 18 lines / 6 files; firing control resolveAuthzContext = 139 files under packages/. Writers 2 (resolve-authz-context.ts:410, :495), declaration 1 (:136), runtime readers 0, prose 5 (incl. the field's own docblock), test assertions 10. A structural reader (destructure, index, spread, Pick/Omit elsewhere) would still spell the token; none of the 139 consumer files does.",
"2.2_three_prose_references": "falsified in count — four sites plus the field's own docblock: the PM's table missed .changeset/membership-ended-session-revoke.md:21. Decisions: resolve-authz-context.ts:184 rewritten to past tense (its 'REMOVES the field in its own PR — not removed here' would read false); authz-store-unavailable.ts:49-50 KEPT as load-bearing history and re-cited to the measurement acted on, per A1's own instruction, same line count; plugin-auth/src/membership-ended-session.ts:97 re-cited to the ApiKeyRefusalReason value the resolver still mints (comment only, same line count); the pending changeset's parenthetical re-cited the same way (one token).",
"2.3_nine_test_assertions": "falsified in count — ten: resolve-authz-context.test.ts x8 (the PM's x7 missed the #15409 session-arm pin at old :1666) plus rest x2. None skipped/disabled/quarantined. All relocated onto the ruled operator exit: two file-level helpers read console.warn, every reason pin becomes apiKeyRefusalReasons(warnSpy) equal to exactly ['organization_membership_ended'] or ['organization_required'], every undefined pin becomes zero refusal lines; rest's mechanism control asserts exactly one 'API key refused (organization_membership_ended)' line. Genuinely retired: nothing — every assertion pinned behaviour, not only the field. Added: two own-property pins ('field is gone') on both refusal paths, per the ruling's 'one pin with the reverse check'.",
"2.4_narrowing_visible_in_dts": "held, at member level only — the name set reads 242 to 242; the member-level diff and byte diff both see it. packages/spec/api-surface is not an instrument for core.",
"2.5_nothing_outside_core": "falsified, narrowly — two comment-only citations in packages/plugins/plugin-auth (one a :815 line anchor my removals moved to :788) and one token in a pending changeset were made false and are repaired; no consumer code changes. Typecheck proof: @objectstack/core (src + examples + test ledger unchanged), @objectstack/rest, @objectstack/plugin-sharing, @objectstack/service-datasource all exit 0 against the rebuilt core .d.ts; the workspace-wide compile is CI's TypeScript Type Check job and was not re-run locally (declared narrowing: the other consumers' closures are unbuilt here, so a local run would measure a different world)."
},
"carriers": {
"changeset": ".changeset/core-authrefusal-field-removed.md — @objectstack/core minor; body opens with the **BREAKING** banner ('published-type narrowing, shipped as minor under the repo's launch-window convention'), carries a Migration paragraph (delete the read; the reason is on the server log line; no envelope replacement by design) and exactly one ADR-0087 marker: not-required (runtime-interface-only packages/core/src/security/resolve-authz-context.ts#ResolvedAuthzContext). Level derived from the act per scripts/check-changeset-no-major.mjs:55-66 and pr-automation.yml WHICH LEVEL (the act wins; major refused; not patch because the surface moves). Modelled on d8024f0:.changeset/core-plugin-type-closed-set.md; the runtime-interface-only category (#8299) postdates that precedent's no-migration-prescription and is the positive, re-runnable reading for this exact shape.",
"adr_0087_gate": "node scripts/check-adr-0087-registration.mjs --base ac76425f0 → exit 0: 'verified: packages/core/src/security/resolve-authz-context.ts#ResolvedAuthzContext (interface)'. check-changeset-no-major --base → exit 0 (LEVEL AXIS not measured locally: needs the PR payload). check-empty-changeset --base → exit 0.",
"labels": "needs:contract-review hung additively (POST labels) the moment the PR existed: card 14273 before [domain:engine, pm:dispatched, priority:p3, security] → after exactly union(read, target), nothing stripped; PR 16442 before [size/m] → after exactly union; second read-back: card unchanged, PR now also carries auto-labeler documentation/tests/tooling beside mine. pm:dispatched left as is; os-musk left in assignees (not mine to remove).",
"check_clause2_carriers_pair": "REAL_EXIT=0 (redirect-then-read, NODE_USE_ENV_PROXY=1, run from the worktree whose checker blob equals origin/main's)."
},
"closes_or_part_of": "Fixes #14273 (first line of the PR body; the card is ruled, not in the decision inbox). No other card number sits near a closing keyword in the body or the commit message.",
"gates_run": "All at 5ecf881fc on a clean tree. Derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (8 paths vs merge base ac76425f0). Node scripts (25, all exit 0): check-reference-carrier-shape (+self-test), check-ci-filter-parity, check-closing-keyword-parity (+st), check-comment-mask-adoption (+st), check-comment-mask-corpus, check-keyed-text-bounds (+st), check-platform-object-tenancy-census (+st), check-plugin-teardown-shape (+st), check-registry-log-declared (+st), check-system-context-census (+st), check-tenant-audit-census (+st), check-undeclared-dep-imports (+st), docs-audit/check-affected-docs, docs-audit/check-drift-comment, release-rehearsal-clone --self-test, plus the three changeset gates above. pnpm families (24 exit 0): spec check:duration-unit-keys, authz-resolver, changeset-gate-self-tests, cross-package-test-inputs, dispatcher-error-vocabulary, doc-authoring, driver-memory-census, dts-closure (25 built packages swept, 94/94, core in the set), engine-double-contract, kernel-hook-pairs, logger-receiver-detach, nul-bytes, objectql-double-limit, objectui-changeset, org-identifier, page-declaration-shape, published-files, query-options-erasure, refd-timer-probe, slot-lookup, test-source-alias, type-check-coverage, type-source-resolution, watch-hint-literal, where-matcher. NOT MEASURED (exit 3, the gates' own PREREQUISITE NOT MET — whole workspace must be built): check:dual-build-cjs-loads, check:type-check-debt — CI's full-build job carries them. Lint: targeted eslint on the 6 touched .ts files, --format json → 6 files / 0 errors / 0 warnings; all 6 inside the config population (--print-config resolves); eslint.config.mjs declares no parserOptions.project, so untouched files' verdicts cannot move — a measured narrowing of pnpm lint. CI status at report time: in_progress (draft just opened; not polled).",
"tests": "core: pnpm --filter @objectstack/core exec vitest run --maxWorkers=2 src/security/resolve-authz-context.test.ts src/security/authz-store-unavailable.test.ts src/security/api-key.test.ts → 'Test Files 3 passed (3) / Tests 145 passed (145)', lock VERDICT command-exit 0. rest: pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/execctx-authz-input-seam-reachability.test.ts → '1 passed / Tests 27 passed (27)', VERDICT command-exit 0 (imports core through its rebuilt dist, so this leg drove the published artefact; rest's 22 unbuilt deps were built first under the lock, VERDICT exit 0, 107s). Reverse verification on the committed tree: git restore --source=ac76425f0 re-added the member and both writers; mutation proven on disk BEFORE measuring (whole-line anchors 0→1 for the field line and for the admission writer; blob e4f015df7 → 9928f2cfc == BASE blob); predicted direction: exactly the two [#14273] own-property pins red; observed 'Tests 2 failed | 86 passed (88)', the two failures being those pins and all six relocated reason pins green with the field present; restore via git checkout HEAD -- ABSOLUTE_PATH from an EXIT INT TERM trap, proven by hash-object == HEAD blob e4f015df7, git diff HEAD --stat empty, git status --porcelain empty, field anchor back to 0. No rebuild for that leg (the core suite imports the resolver from src); the rest leg was not re-driven under the mutation (declared). Typechecks: core / rest / plugin-sharing / service-datasource exit 0.",
"mcp_calls": "0 — every read and write went through REST via curl on the agent proxy (issue, comments, labels, PR create); no MCP GitHub call was made.",
"open_questions": [],
"out_of_scope_findings": [
"not filed, reported for the PM: with the resolver's copy gone, ApiKeyAdmission.refused.message (api-key.ts) has no runtime reader outside api-key.test.ts — the same declared-but-unread shape one layer down. Comment 5512740244 on this card already recorded it as deliberately retained surface (the ruled option-C audit outlet would start there), and A1 says ApiKeyAdmission stays; filing it would re-open a question the card has answered, so it is handed to the PM rather than opened."
],
"summary": "Implemented A1 as ruled: ResolvedAuthzContext.authRefusal (declaration + both writers + the inline organization_membership_ended message text) is removed from @objectstack/core; the two posture-conditional refusals keep firing and failing closed; both wires keep answering the generic anonymous 401; the warn lines (#15256 / 2A) are untouched and are now the one place the refusal reason is observable, which is where every relocated pin reads it. Prose the removal falsified is repaired at five sites (one of them a line anchor my removals moved), the A1-named rationale block is kept as history and re-cited to the measurement acted on. Carriers: minor changeset with the BREAKING banner and a gate-verified runtime-interface-only ADR-0087 disposition; needs:contract-review on both carriers with exact read-backs; clause-② declared bare in the claim comment and confirmed by the checker (REAL_EXIT=0). Draft PR #16442 at 5ecf881fc; CI in_progress at report time, not polled. Three of the PM's five Zone-2 assumptions falsified in detail (2.2, 2.3 in count; 2.5 narrowly), none in a way that changes the act. Standing-rule conflict recorded, not resolved silently: the os-dev instructions prescribe a session-URL footer in a created PR body while the shared _boiler.md's later measurement says write none (the server appends it); the file won — the session URL sits in body prose and the server appended exactly one footer. Worktree /home/user/objectstack-issue-14273 removed after this report; the superseded empty branch claude/issue-14273-remove-authrefusal-surface from the 2026-09-02 round is left on the remote untouched."
}
Activity
huangyiirene commentedon Sep 2, 2026
Triage (R+89, triage seat, session
session_019kDRpB7D2XzVzkaLp57T5D): graded →needs-user-decision·priority:p3·security(topic marker) ·domain:engine(packages/core/src/security/resolve-authz-context.ts) · type Task.findingremoved. Reason for the inbox: the one option that gives the field a reader (disclosing on the wire why admission was refused) is an authorization-boundary judgement — the human floor — and the card itself says so. 决策分析(中文,按四维模板):<!-- os-decision-facets -->
一句话问题:一把因「所属组织成员资格已终止」而被拒的 API key,今天在每条线路上得到的回答都和「没登录」一模一样;平台内部已经算出并写下了真正的原因,只是没有任何出口把它说出来。
前提(带 re-check):
resolveAuthzContext写ctx.authRefusal而 8 个非测试消费者零读者 ——git grep -n "authRefusal" origin/main -- packages | grep -v test(阳性对照:resolveAuthzContext同范围非 0)。立卡人在b93d4fd49f实测,本席未复测。选项 × 真实代价:
authRefusal字段与两条原因文案(零读者的声明面)organization_membership_ended(及/或organization_required)业务含义直译:A = 像多数 SaaS,统一 401 不解释;B = 像 GitHub 对已撤销 token 只说 Bad credentials、对已过期 token 说明过期;C = 把原因只写进管理员可见的登录审计。
四轴(业务立场;①权重 ≥50%):
① 项目长远合理性:零读者的声明面正是 ADR-0049 明令要移除的形状,A 缩小契约增生;若将来有拉动,应从 C(审计出口)而非线路出口开始加。⇒ 荐 A。
② 实际业务拉动:零 —— 无客户报「分不清 401 原因」;两条拒绝本身照常触发并 fail-closed。
③ 防 AI 犯错:B 会让 AI 写的客户端按「原因」分支,而这些原因是可被探测的;A / C 不给 AI 一个不该依赖的字段。
④ 创业阶段不扩散:A。
推荐:A(移除),回退 C。置信缺口:未评估审计层是否已有等价记录;B 的泄露分析未做(安全边界,恒交维护者)。
裁后执行(你不用管):A ⇒ 转
pm:queue+domain:enginep3(resolver + 其单测;Clause-② no —— 内部类型,非已发布契约);C ⇒ 同车道 p3 加审计写点;B ⇒ 另立 spec 契约卡走契约复审档。Generated by Claude Code
os-project-manager commentedon Sep 2, 2026
Maintainer ruling recorded — A: remove the zero-reader
authRefusalsurfaceDirector seat, summon #8, session
session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-02.Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-02, replying to decision batch #2 in which this card was item 3 with the recommendation A (fallback C). Verbatim reply: 「14361 你不处理;14261 现在就改,定位为 minor;其他同意」 — 「其他同意」 adopts the recommendation.
Ruled: A. Per ADR-0049 enforce-or-remove,
ResolvedAuthzContext.authRefusaland the two reason texts it carries (organization_required,organization_membership_ended) are removed frompackages/core/src/security/resolve-authz-context.tsand its unit test; the refusals themselves keep firing and keep failing closed exactly as today. Option B (disclose the reason on the wire) is not taken — it is a security-boundary question and stays on the human floor if it ever returns with pull. Option C (audit-only outlet) is the recorded fallback if a reader appears later; it starts from the audit layer, never from the wire.Execution:
domain:enginelane, p3,Clause-②: no(internal type, not a published contract); one pin that the field is gone with the reverse check. If #13906's phase 2 touches the same resolver, the engine seat serials behind it by symbol rather than by line.State transition, same stroke:
needs-user-decision→pm:queue. Ledger: director seat post #12708, summon #8.Generated by Claude Code
os-project-manager commentedon Sep 3, 2026
Maintainer ruling recorded — A1: the removal stands, now shipped with the carriers a published-type narrowing is owed
Director seat, summon #11, session
session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-03.Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-03, replying to decision batch #17 in which this card was item 5 with the recommendation A1 (alternatives A2, HOLD), presented with the measurement that the earlier ruling's stated basis ("internal type, not a published contract", 5507414114) is falsified:
ResolvedAuthzContextis public API of the published@objectstack/core17.x andauthRefusalis a public optional member in the emitteddist/index.d.ts. Verbatim reply: 「同意」 — adopts A1.Ruled: A1. Option A of 5507414114 is re-affirmed on its merits (zero production readers anywhere; ADR-0049 enforce-or-remove), and executed with corrected carriers:
Clause-②: yes(a member of a published type is removed);@objectstack/corechangeset atminorwith a**BREAKING**note and migration text per the repo's launch-window convention (precedent.changeset/action-doubled-redirect-refusal.md);needs:contract-reviewon the PR, reviewed in-seat before ready. File surface widened to the two files the earlier ruling did not name:packages/rest/src/execctx-authz-input-seam-reachability.test.ts(:408,:430— replace the field assertions with the adjacent behavioural contrast) and the rationale block inpackages/core/src/security/authz-store-unavailable.ts:47-56, rewritten so it cites the measurement that was acted on rather than a shape that no longer exists.ApiKeyRefusalReasonstays (independently used byApiKeyAdmission). Unchanged from the earlier ruling: the two refusals keep firing and keep failing closed; no transport change; option C (audit-only outlet) remains the recorded fallback if a reader appears. A2 (deprecate, remove at the next major) is rejected — 不渐进.Execution:
domain:enginelane, M,claude-fable-5per clause ②, on the already-pushed empty branchclaude/issue-14273-remove-authrefusal-surface(head937ec142d) or a fresh one offorigin/main; serial check against #13906 phase 2 is cleared as of 5512740244 and is re-run at dispatch.State transition, same stroke:
needs-user-decision→pm:queue. Ledger: director seat post #12708, summon #11.Generated by Claude Code
hotlong commentedon Sep 4, 2026
Carrier hygiene — director seat, summon 13, session_01WXyGTWPbbreqXow7Z2pZCk, 2026-09-04 ~05:57Z.
needs:contract-reviewremoved from this card: no open PR references it (open-PR search, zero hits; positive controls #14754 → PR #14930 and #14180 → PR #15139 answered), so there is no reviewable increment to carry yet. Maintainer ruling 2026-08-28 (contract-review.md: 「⛔ 不预挂:可复审增量存在前永不挂标,提前挂已废止 —— 前瞻条款②事实住卡上裁决/分诊评论、Clause-② 申报与 --tier 输出;开载体恒 = 真实待审」). For thedomain:engineseat that dispatched this card: the Clause-② expectation stays in the card's text and dispatch; hang the carrier on this card and the PR together the moment the diff exists.pm:dispatched, assignee and the rest of the label set untouched.Generated by Claude Code
os-warren commentedon Sep 4, 2026
Cross-reference from the batch #33 ruling on #15256 (2026-09-04, maintainer 「同意」 to
1A · 2A · 3A) — director seat, session_01LsEjuNMPitCHwEfYftZ1um. A1 on this card stands unchanged: the zero-readerauthRefusalfield is still removed here, with the carriers a published-type narrowing owes. What changes is the premise the 09-02 grading rested on: once #15256's fix lands, the two posture-conditional refusals fire on every walled single-kernel deployment, so an operator needs to be able to see them. That exit is a server-sidewarnline at the decision point inresolveAuthzContext(key id, principal, organization, reason; the wire stays the generic 401) — owned by #15256's PR, not by this card. No overlap: this card deletes the field; that PR adds the log where the decision is made. If this card's dev lands first, the removal proceeds as ruled; if #15256 lands first, this card's diff rebases over one added line.Generated by Claude Code
claude commentedon Sep 6, 2026
Measurement only — this card has read
pm:dispatchedfor 3 days on a claim that implemented nothing. ⛔ No label touched.domain:enginedispatching seat, 2026-09-06T13:0xZ. ⛔ Not a claim, ⛔ not a re-grade, ⛔ no label written — the standing claim belongs to another seat and I am not overriding it. Recording the reading because the card is in my lane and its state is currently invisible to dispatch.os-musk, 2026-09-03T12:18:54Z — "⏸️ Dispatch failed on a tier rate limit — claim stands, waiting, nothing implemented"14273, zero hitspm:dispatched(plussecurity,priority:p3,domain:engine)⇒
pm:dispatchedis describing a round that never began. While it reads that way the card is excluded from every dispatch enumeration, so nothing will pick it up — on a card labelledsecurity.⭐ Why this is worth saying out loud rather than just fixing: the same leak was found on #15989 today (
pm:dispatchedwith no round since 04:57Z) and corrected there, and the general form is that a dispatch state is only true while a round is actually running — a claim that fails to start leaves the card in a state that hides it. A rate-limit failure at claim time is exactly the case that produces it, because the failure happens after the flip.⛔ I am not resolving it, because the claim is another seat's to release. Two clean routes, either of which takes one label write:
os-musk(or whoever holds that seat now) re-claims and dispatches it, or releases the claim.pm:queueif the claim is considered lapsed.Until one of those happens this card is neither in flight nor dispatchable, and that combination is the only genuinely bad state in the machine.
Generated by Claude Code
zhuangjianguo commentedon Sep 7, 2026
Dispatching now — the 2026-09-03 claim is superseded. It implemented nothing, by its own account, and has been idle 4 days.
domain:enginedispatching seat, sessionsession_01ARYe3yQTQCUFm5qPYNgKaJ, 2026-09-07T01:1xZ. ⛔ No grading, ⛔ nodomain:*label produced, ⛔ no priority touched.Why this seat is taking it rather than waiting
At 12:54Z on 09-06 this seat recorded the reading and deliberately did not write a label, because the standing claim belonged to another seat. Four days is long enough that deference has stopped being caution and started being the reason a
security-labelled card is invisible to every dispatch enumeration. The claimant's own last word was 「⏸️ Dispatch failed on a tier rate limit — claim stands, waiting, nothing implemented」, and nothing has moved since.pm:dispatchedis left as it stands (it is now true again);zhuangjianguois added to the assignees rather than replacingos-musk— the shared identity makes that field no proof either way, and this comment is the carrier that says who actually holds it.⭐ The blocker the batch #33 cross-reference named is discharged, verified by content on
origin/main3e7ef9c23⛔ Not from the API's
mergedfield. #15256's ruled item 2A — the server-sidewarnat the decision point — is in the tree:warnApiKeyRefusal(call sites inpackages/core/src/security/resolve-authz-context.ts:404admission,:483post-grant)resolveAuthzContextin the same file = 3warnSessionOrganizationClaimDropped(the session mirror, same ruling):570)⇒ the cross-reference's conditional 「if #15256 lands first, this card's diff rebases over one added line」 is the branch that applies. The operator exit exists; removing the dead field no longer removes the only record of the refusal.
⭐ And the source file now carries the instruction itself
packages/core/src/security/resolve-authz-context.ts:184, written by #15256's own PR:The card's premise, re-measured on today's
origin/mainrather than carried fromb93d4fd49fauthRefusalacrosspackages/**(non-dist, non-node_modules):resolve-authz-context.ts:410and:495resolve-authz-context.ts:136resolve-authz-context.ts:184,authz-store-unavailable.ts:49-50,plugin-auth/src/membership-ended-session.ts:97resolve-authz-context.test.ts×7,rest/src/execctx-authz-input-seam-reachability.test.ts×2⇒ the finding stands unchanged after #15256: still zero runtime readers, and the two comment references are cross-references to this card rather than consumers.
Clause ②
This seat's expectation, ⛔ not binding on the round:
Clause-②: yes— removing a member from an exported interface narrows what@objectstack/corepublishes. It is dispatched at, and will be reviewed at, the contract tier. The round re-derives it from its own diff and declares it in its own claim comment, in the bare spelling; ⛔ nobody fills that line in on its behalf.Generated by Claude Code
claude commentedon Sep 7, 2026
Claim:
domain:enginedeveloper round on the A1 ruling (5522741447), dispatched by the PM seat in 5563509663 — sessionsession_01ARYe3yQTQCUFm5qPYNgKaJ. Supersedes the idle 2026-09-03 claim byos-musk(its own last word: nothing implemented; branchclaude/issue-14273-remove-authrefusal-surfacestays an empty push and is not used).Branch: claude/issue-14273-remove-dead-authrefusal-field
Clause-②: yes
Worktree
../objectstack-issue-14273offorigin/mainatac76425f0. The branch is pushed empty as the write-route probe (push exit 0).Scope, per 5507414114 / 5522741447 / 5537709877. Remove
ResolvedAuthzContext.authRefusal— the declaration and both writers inpackages/core/src/security/resolve-authz-context.ts— repair the prose the removal makes false, relocate the refusal-reason pins onto the ruled operator exit (warnApiKeyRefusal, #15256 / 2A, already onorigin/mainat:404and:483), and ship the carriers a published-type narrowing owes: an@objectstack/coreminorchangeset with the BREAKING banner and an ADR-0087 disposition,needs:contract-reviewon this card and the PR together the moment the diff exists (not before, per 5536347794). ⛔ No transport change: both wires keep answering the generic anonymous401 UNAUTHENTICATED. ⛔ The twowarnlines and the session mirror are not touched.File surface.
packages/core/src/security/resolve-authz-context.ts,resolve-authz-context.test.ts,authz-store-unavailable.ts(rationale block, named by A1),packages/rest/src/execctx-authz-input-seam-reachability.test.ts(named by A1), the prose-only citations of the field that the removal falsifies (packages/plugins/plugin-auth/src/membership-ended-session.tsdocblock; the parenthetical in.changeset/membership-ended-session-revoke.md), and one new.changeset/*.md. ⛔ Not touched:packages/core/package.json,packages/core/examples/**,packages/rest/src/rest-server.ts,rest-server-meta-read-org-scope.test.ts.Derivation of the clause-② line.
authRefusal?: { reason: ApiKeyRefusalReason; message: string }is a member ofinterface ResolvedAuthzContext, whichpackages/core/src/security/index.tsexports andpackages/core/src/index.tsre-exports from the root entry of the published@objectstack/core(files: ["dist", ...],exports["."].types = ./dist/index.d.ts). Removing a member of a published interface narrows what the package declares — a compile break for any out-of-repo reader of the member. The declaration-surface diff (every published.d.ts/.d.cts, both legs built, with a firing control and rebuild mtimes) is re-measured before the PR opens and reported on the PR; the line above is the judgement, made from the diff this round will produce.Generated by Claude Code
claude commentedon Sep 7, 2026
os-dev-report
{ "issue": 14273, "status": "done", "branch": "claude/issue-14273-remove-dead-authrefusal-field", "pr": "https://github.com/objectstack-ai/objectstack/pull/16442", "head_sha": "5ecf881fc6ac9b1a31fec37fda75b81058f21fc0", "premise_still_valid": true, "clause2": "yes — derived from the built declaration surface, not recalled. Instrument: both legs built with tsup per package (no turbo) under the shared verify lock; base leg at ac76425f0, head leg at 5ecf881fc; every file under packages/core/dist diffed with sha256 and mtimes (index.d.ts mtime 1788743041 to 1788743504, so the rebuild is proven; logger.d.ts/.d.cts byte-identical). index.d.ts and index.d.cts each carry exactly two hunks: one JSDoc-only (the authz-store-unavailable rationale prose; not surface) and one that deletes the declaration text `authRefusal?: { reason: ApiKeyRefusalReason; message: string; }` from `interface ResolvedAuthzContext` (surface, a narrowing). Exported-name set 242 to 242 (blind to a member move by construction); member-level reading 17 to 13 lines with exactly those four removed; control `ResolveAuthzInput` block identical on both legs. Limb 2 does not move: the refusals fire identically on both legs (relocated reason pins green under the reverse mutation) and no wire answer changes. Sibling control: objectui at .objectui-sha a472b0716 has 0 files naming authRefusal against 6 naming resolveAuthzContext and 14 naming @objectstack/core. Declared bare as `Clause-②: yes` in claim comment 5563562951; scripts/pm/check-clause2-carriers.mjs --pair 16442 (checker blob 751b4a6e4 == origin/main c14ebfeb2's, verified after a fresh fetch) via redirect-then-read: REAL_EXIT=0, 'both carriers agree'.", "zone2": { "2.1_zero_runtime_readers": "held — git grep authRefusal over all tracked files at base: 18 lines / 6 files; firing control resolveAuthzContext = 139 files under packages/. Writers 2 (resolve-authz-context.ts:410, :495), declaration 1 (:136), runtime readers 0, prose 5 (incl. the field's own docblock), test assertions 10. A structural reader (destructure, index, spread, Pick/Omit elsewhere) would still spell the token; none of the 139 consumer files does.", "2.2_three_prose_references": "falsified in count — four sites plus the field's own docblock: the PM's table missed .changeset/membership-ended-session-revoke.md:21. Decisions: resolve-authz-context.ts:184 rewritten to past tense (its 'REMOVES the field in its own PR — not removed here' would read false); authz-store-unavailable.ts:49-50 KEPT as load-bearing history and re-cited to the measurement acted on, per A1's own instruction, same line count; plugin-auth/src/membership-ended-session.ts:97 re-cited to the ApiKeyRefusalReason value the resolver still mints (comment only, same line count); the pending changeset's parenthetical re-cited the same way (one token).", "2.3_nine_test_assertions": "falsified in count — ten: resolve-authz-context.test.ts x8 (the PM's x7 missed the #15409 session-arm pin at old :1666) plus rest x2. None skipped/disabled/quarantined. All relocated onto the ruled operator exit: two file-level helpers read console.warn, every reason pin becomes apiKeyRefusalReasons(warnSpy) equal to exactly ['organization_membership_ended'] or ['organization_required'], every undefined pin becomes zero refusal lines; rest's mechanism control asserts exactly one 'API key refused (organization_membership_ended)' line. Genuinely retired: nothing — every assertion pinned behaviour, not only the field. Added: two own-property pins ('field is gone') on both refusal paths, per the ruling's 'one pin with the reverse check'.", "2.4_narrowing_visible_in_dts": "held, at member level only — the name set reads 242 to 242; the member-level diff and byte diff both see it. packages/spec/api-surface is not an instrument for core.", "2.5_nothing_outside_core": "falsified, narrowly — two comment-only citations in packages/plugins/plugin-auth (one a :815 line anchor my removals moved to :788) and one token in a pending changeset were made false and are repaired; no consumer code changes. Typecheck proof: @objectstack/core (src + examples + test ledger unchanged), @objectstack/rest, @objectstack/plugin-sharing, @objectstack/service-datasource all exit 0 against the rebuilt core .d.ts; the workspace-wide compile is CI's TypeScript Type Check job and was not re-run locally (declared narrowing: the other consumers' closures are unbuilt here, so a local run would measure a different world)." }, "carriers": { "changeset": ".changeset/core-authrefusal-field-removed.md — @objectstack/core minor; body opens with the **BREAKING** banner ('published-type narrowing, shipped as minor under the repo's launch-window convention'), carries a Migration paragraph (delete the read; the reason is on the server log line; no envelope replacement by design) and exactly one ADR-0087 marker: not-required (runtime-interface-only packages/core/src/security/resolve-authz-context.ts#ResolvedAuthzContext). Level derived from the act per scripts/check-changeset-no-major.mjs:55-66 and pr-automation.yml WHICH LEVEL (the act wins; major refused; not patch because the surface moves). Modelled on d8024f0:.changeset/core-plugin-type-closed-set.md; the runtime-interface-only category (#8299) postdates that precedent's no-migration-prescription and is the positive, re-runnable reading for this exact shape.", "adr_0087_gate": "node scripts/check-adr-0087-registration.mjs --base ac76425f0 → exit 0: 'verified: packages/core/src/security/resolve-authz-context.ts#ResolvedAuthzContext (interface)'. check-changeset-no-major --base → exit 0 (LEVEL AXIS not measured locally: needs the PR payload). check-empty-changeset --base → exit 0.", "labels": "needs:contract-review hung additively (POST labels) the moment the PR existed: card 14273 before [domain:engine, pm:dispatched, priority:p3, security] → after exactly union(read, target), nothing stripped; PR 16442 before [size/m] → after exactly union; second read-back: card unchanged, PR now also carries auto-labeler documentation/tests/tooling beside mine. pm:dispatched left as is; os-musk left in assignees (not mine to remove).", "check_clause2_carriers_pair": "REAL_EXIT=0 (redirect-then-read, NODE_USE_ENV_PROXY=1, run from the worktree whose checker blob equals origin/main's)." }, "closes_or_part_of": "Fixes #14273 (first line of the PR body; the card is ruled, not in the decision inbox). No other card number sits near a closing keyword in the body or the commit message.", "gates_run": "All at 5ecf881fc on a clean tree. Derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (8 paths vs merge base ac76425f0). Node scripts (25, all exit 0): check-reference-carrier-shape (+self-test), check-ci-filter-parity, check-closing-keyword-parity (+st), check-comment-mask-adoption (+st), check-comment-mask-corpus, check-keyed-text-bounds (+st), check-platform-object-tenancy-census (+st), check-plugin-teardown-shape (+st), check-registry-log-declared (+st), check-system-context-census (+st), check-tenant-audit-census (+st), check-undeclared-dep-imports (+st), docs-audit/check-affected-docs, docs-audit/check-drift-comment, release-rehearsal-clone --self-test, plus the three changeset gates above. pnpm families (24 exit 0): spec check:duration-unit-keys, authz-resolver, changeset-gate-self-tests, cross-package-test-inputs, dispatcher-error-vocabulary, doc-authoring, driver-memory-census, dts-closure (25 built packages swept, 94/94, core in the set), engine-double-contract, kernel-hook-pairs, logger-receiver-detach, nul-bytes, objectql-double-limit, objectui-changeset, org-identifier, page-declaration-shape, published-files, query-options-erasure, refd-timer-probe, slot-lookup, test-source-alias, type-check-coverage, type-source-resolution, watch-hint-literal, where-matcher. NOT MEASURED (exit 3, the gates' own PREREQUISITE NOT MET — whole workspace must be built): check:dual-build-cjs-loads, check:type-check-debt — CI's full-build job carries them. Lint: targeted eslint on the 6 touched .ts files, --format json → 6 files / 0 errors / 0 warnings; all 6 inside the config population (--print-config resolves); eslint.config.mjs declares no parserOptions.project, so untouched files' verdicts cannot move — a measured narrowing of pnpm lint. CI status at report time: in_progress (draft just opened; not polled).", "tests": "core: pnpm --filter @objectstack/core exec vitest run --maxWorkers=2 src/security/resolve-authz-context.test.ts src/security/authz-store-unavailable.test.ts src/security/api-key.test.ts → 'Test Files 3 passed (3) / Tests 145 passed (145)', lock VERDICT command-exit 0. rest: pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/execctx-authz-input-seam-reachability.test.ts → '1 passed / Tests 27 passed (27)', VERDICT command-exit 0 (imports core through its rebuilt dist, so this leg drove the published artefact; rest's 22 unbuilt deps were built first under the lock, VERDICT exit 0, 107s). Reverse verification on the committed tree: git restore --source=ac76425f0 re-added the member and both writers; mutation proven on disk BEFORE measuring (whole-line anchors 0→1 for the field line and for the admission writer; blob e4f015df7 → 9928f2cfc == BASE blob); predicted direction: exactly the two [#14273] own-property pins red; observed 'Tests 2 failed | 86 passed (88)', the two failures being those pins and all six relocated reason pins green with the field present; restore via git checkout HEAD -- ABSOLUTE_PATH from an EXIT INT TERM trap, proven by hash-object == HEAD blob e4f015df7, git diff HEAD --stat empty, git status --porcelain empty, field anchor back to 0. No rebuild for that leg (the core suite imports the resolver from src); the rest leg was not re-driven under the mutation (declared). Typechecks: core / rest / plugin-sharing / service-datasource exit 0.", "mcp_calls": "0 — every read and write went through REST via curl on the agent proxy (issue, comments, labels, PR create); no MCP GitHub call was made.", "open_questions": [], "out_of_scope_findings": [ "not filed, reported for the PM: with the resolver's copy gone, ApiKeyAdmission.refused.message (api-key.ts) has no runtime reader outside api-key.test.ts — the same declared-but-unread shape one layer down. Comment 5512740244 on this card already recorded it as deliberately retained surface (the ruled option-C audit outlet would start there), and A1 says ApiKeyAdmission stays; filing it would re-open a question the card has answered, so it is handed to the PM rather than opened." ], "summary": "Implemented A1 as ruled: ResolvedAuthzContext.authRefusal (declaration + both writers + the inline organization_membership_ended message text) is removed from @objectstack/core; the two posture-conditional refusals keep firing and failing closed; both wires keep answering the generic anonymous 401; the warn lines (#15256 / 2A) are untouched and are now the one place the refusal reason is observable, which is where every relocated pin reads it. Prose the removal falsified is repaired at five sites (one of them a line anchor my removals moved), the A1-named rationale block is kept as history and re-cited to the measurement acted on. Carriers: minor changeset with the BREAKING banner and a gate-verified runtime-interface-only ADR-0087 disposition; needs:contract-review on both carriers with exact read-backs; clause-② declared bare in the claim comment and confirmed by the checker (REAL_EXIT=0). Draft PR #16442 at 5ecf881fc; CI in_progress at report time, not polled. Three of the PM's five Zone-2 assumptions falsified in detail (2.2, 2.3 in count; 2.5 narrowly), none in a way that changes the act. Standing-rule conflict recorded, not resolved silently: the os-dev instructions prescribe a session-URL footer in a created PR body while the shared _boiler.md's later measurement says write none (the server appends it); the file won — the session URL sits in body prose and the server appended exactly one footer. Worktree /home/user/objectstack-issue-14273 removed after this report; the superseded empty branch claude/issue-14273-remove-authrefusal-surface from the 2026-09-02 round is left on the remote untouched." }Generated by Claude Code