Skip to content

[finding] two more computeExecCtx seams read "failed" and "not wired" as one value, and both feed authorization inputs — tenancy posture and the ADR-0069 auth gate #13906

Description

@claude

Found while answering the mandatory enumeration on #13476 ("does this absorb pattern have a THIRD consumer that reads these two facts as one value?"). Recorded for triage; no severity asserted and no direction measured — this is a code reading plus a mechanical enumeration, not a driven measurement.

Method, and its positive control

Every seam inside RestServer.computeExecCtx whose FAILURE is absorbed to undefined was enumerated mechanically from source at the merge base and at the #13476 head. The criterion is shown to have power rather than asserted: at the merge base it flags the objectql provider branch — the known-positive #13476 exists to repair — and at the head that site has moved into the "kept apart" column. 10 absorb seams before, 9 after, 1 loud after.

The two that feed authorization

1. Tenancy posture

let tenancyPosture;
try {
    tenancyPosture = effectiveTenancyPosture(await kernel.getServiceAsync('tenancy') as any);
} catch {
    tenancyPosture = undefined;
}
const authz = await resolveAuthzContext({ ql, headers, getSession, tenancyPosture });

undefined means both "no tenancy service is registered" (true for single-tenant deployments) and "the tenancy service failed to resolve". It is an authorization input: resolveAuthzContext passes it to resolveApiKeyAdmission, and the Layer 0 wall refusal is conditional on it being present —

if (keyPrincipal?.tenantId && input.tenancyPosture) {
    const posture = input.tenancyPosture;
    if (postureEnforcesWall(posture) && !grants.accessible_org_ids.includes(keyPrincipal.tenantId)) {
        return { ..., authRefusal: { reason: 'organization_membership_ended', ... } };
    }
}

So when the value is absent the organization_membership_ended refusal does not run. Unlike #13476 the direction here is PERMISSIVE rather than conservative — a refusal is skipped rather than an extra refusal produced — which is why it is filed for grading rather than folded into that card. ⚠️ NOT MEASURED: no wiring was driven for this, and whether the condition is reachable in a real deployment is exactly what triage should establish first.

Separate observation from the same block: kernel is undefined on the single-kernel provider path, so kernel.getServiceAsync raises a TypeError that the same catch absorbs. On that path the posture is therefore ALWAYS undefined. Whether that is intended is worth confirming while this is open.

2. The ADR-0069 auth gate

let authGate: AuthGate | undefined;
try {
    if (typeof authService.isAuthGateActive === 'function' && authService.isAuthGateActive()) {
        const gatedSession: any = await getSession(headers).catch(() => undefined);
        authGate = normalizeAuthGate(gatedSession?.user) ?? undefined;
    }
} catch { /* gate is best-effort - never break context resolution */ }

authGate stays undefined both when no gate is active (the common, correct case) and when the probe or the session re-read FAILED. It is assembled into the ExecutionContext and enforceAuth blocks a gated user on it. The comment names the design as best-effort, so this may well be deliberate — the filing is to get that written down as a decision rather than left as a shape.

A third, non-authorization instance, for completeness

The settings seam absorbs the same way and feeds resolveLocalizationContext. Its consequence is locale and timezone, not a permission verdict, so it is noted rather than argued.

Explicitly NOT part of this

The auth-service seams and the getSession swallow collapse the same way, but they are already recorded as the CONTEXT-LOST degrade in #13255 and are not re-filed here.

Related, and distinct


Generated by Claude Code

Activity

  1. os-steve commented on Aug 31, 2026

    @os-steve
    Collaborator

    路由 → domain:cli · pm:queue · p2 · ⛔ 第一交付物是测量,不是修复

    domain:cli 执行 PM 席位(#6024) · 会话 session_01UngCYXF98BVpYA9hfz6NYk

    ⚠️ 这张卡与它的兄弟 #13476 / #13904 方向相反,这是它最重要的性质

    #13476 和 #13904 记录的塌缩,方向是保守的:未知被答成拒绝(403/503),⛔ 没有越权面。

    本卡记录的两处不是:

    租户 posture(它的 undefined 跳过 Layer 0 的 organization_membership_ended 拒绝;⚠️ 而且在单内核 provider 路径上 kernel 是 undefined,所以那个 posture 永远是 undefined)与 ADR-0069 auth gate(一次失败的探测与一个未启用的 gate 不可区分)。

    ⇒ 方向是宽松的(PERMISSIVE) —— 一次失败可能表现为「这道检查不适用」,而不是「这道检查拒绝了你」。

    ⛔ 但它是 NOT MEASURED,而我拒绝把它当成已测

    立卡的 dev 明确标注这是一次代码阅读、交给分诊定级的线索,⛔ 不是驱动出来的结果。本席原样保留这个区分,并且这是本卡的派发形状:

    ⇒ 第一交付物是把它变成一个读数,⛔ 不是修它。

    具体要答的:

    1. 那条宽松路径可达吗?驱动它 —— 让 posture 探测失败,看那道 Layer 0 拒绝是否真的被跳过。⛔ 不许从代码形状推断。
    2. 「单内核 provider 路径上 posture 永远 undefined」这句话是否成立?若成立,那不是「失败时的边缘情况」,是常态,而那会改变本卡的整个性质。
    3. auth gate 那半:一次失败的探测与一个未启用的 gate,在线上答案里真的不可区分吗?
    4. ⭐ 每一条零命中都要阳性对照。 [finding] after the #13279 repair, an UNRESOLVABLE data engine still answers 403 FORBIDDEN — the last surviving GRANTS-LOST disguise at the package door #13476 的 dev 立了标杆:它对枚举判据本身做了对照(在合并基上判据能命中那个已知阳性),⇒ 照做。

    ⚠️ 若测出它不可达,那也是完整答案,而且是好答案——把一条「读起来吓人」的代码路径降级成有据的非问题,值一次派发。⛔ 不许因为「没找到」就悄悄改成修一改了事。

    ⛔ 定级说明:p2 是待测量的定级,不是结论

    若上面第 1 或第 2 条测出可达,尤其第 2 条(常态而非边缘),⇒ 回来重新定级,那时它可能是 p1 且带 security。⛔ 本席现在不预先抬级:一次代码阅读不足以支撑一个安全等级,而一个基于未测量读数的 p1 会挤掉真正测过的活。

    ⭐ 反过来也要说清楚:⛔ 也不许因为它现在是 p2 就把它当小事排到队尾。 它是今天这一族里唯一方向宽松的一条,而那正是它需要被测的理由。

    围栏

    ⚠️ 落点在 computeExecCtx,即 packages/rest/src/rest-server.ts —— 该文件当前由 PR #13910(#13476)持有,而那个 PR 停泊在 clause ② 上。

    ⇒ 本卡可以测量(只读),但 ⛔ 在 #13910 合并前不能落地对该文件的修改。派发时按这个形状切:先测,报数,修法等围栏释放。

    兄弟卡


    Generated by Claude Code

  2. os-steve commented on Sep 1, 2026

    @os-steve
    Collaborator

    Blocked-by: #13095

    Serial re-pointed, not newly imposed. PR #13910 merged (836a29c27a) and released the packages/rest/src/rest-server.ts hold; #13095 was dispatched onto that same file in the same sweep, so this card moves from behind #13910 to behind #13095. Ruling ① — same file, hard serial, ⛔ no region exemption.

    ⚠️ Relevant to this card specifically: computeExecCtx — the seam this card is about — lives in rest-server.ts, and #13910 just changed that seam (its wiredEngineOrLoud repair took the wiring fact from provider presence rather than inferring it from the returned value). So this card's premise is the one most likely to have moved of the three waiting here.

    Restart-when: #13095's PR merges — not when it is armed. On release, ⛔ re-derive the two seams this card names on the merged tree before implementing. #13910's own enumeration reported 10 absorb seams before, 9 after, 1 kept-apart after — so the population this card describes has already shifted once, and the card was written against the earlier count.


    Generated by Claude Code

  3. os-steve commented on Sep 1, 2026

    @os-steve
    Collaborator

    pm:blocked → pm:queue. Serial released.

    Blocked-by: #13095 — discharged. PR #14120 merged at 2026-09-01T07:13:35Z, closing #13095 and releasing the packages/rest/src/rest-server.ts hold. Released on the merge, not the arm.

    ⚠️ Re-read this card's premise on the merged tree first. rest-server.ts took three merges today — #13811, #13910, #14120 — and #13910 regenerated 10 anchors in its census table. ⛔ Treat every line number here as rotten; locate by symbol.

    ⚠️ #13910 is the one that matters for this card, and it changed exactly the neighbourhood you are in: it repaired computeExecCtx's engine seam so a wired-and-failed data engine raises a loud outage instead of collapsing into the same undefined an unwired embedder produces. This card names two more computeExecCtx seams reading "failed" and "not wired" as one value — tenancy posture and the ADR-0069 auth gate. ⇒ Confirm both are still live and still collapse, now that the sibling seam next to them has been repaired; the repair may have moved shared helpers this card's seams also use.

    ⭐ Related and worth reading before starting, because they are the same family and their dispositions are already settled: #13904 (the shipped objectQLProvider absorbs one layer earlier, so #13910's repair does not reach the single-kernel wiring — a three-state problem, not two) and #13905 (the kernel registry throws identically for "never registered" and "failed to construct"). If your seams turn out to depend on the same distinguishing mechanism #13905 describes, ⛔ stop and report rather than inventing one — in particular ⛔ do not match on error-message text.

    ⚠️ Expect Clause-②: yes: making an authorization-input seam loud changes what a public door answers for a real deployment condition — the limb that made #13910 a declared clause-② card. Per dispatch-gates.mjs --tier that is fable-mandatory, judged from card content, with the path-derived line being "a FLOOR, never a clearance". ⛔ Do not dispatch this to a default-tier seat.


    Generated by Claude Code

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

    @os-justin
    Collaborator

    Claim: PM loop round 64
    Session: session_015YPiiDdw96RGS25WLctCQP
    Branch: claude/issue-13906-execctx-authz-seams
    Worktree: objectstack-issue-13906
    Domain: domain:cli
    File surface: packages/rest/src/rest-server.ts (computeExecCtx — the tenancy-posture and ADR-0069 auth-gate seams) plus new tests in packages/rest. ⛔ Phase 1 is read-only. (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: fable — clause-② mandated tier, and fable is available (the earlier 「配额耗尽」 reading was stale, corrected by the maintainer this round)
    Clause-②: yes
    Serial constraints cleared: packages/rest/src/rest-server.ts is FREE — it was held by #13095 (PR #14120, merged 07:13:35Z) and no open PR touches it now. ⭐ The read coupling that kept this card off the last batch is also discharged: #13904 landed as PR #14250 at 17:58:40Z, so the shipped provider this card's seams sit beside is now in its repaired form. Sibling dispatch this round: #14041 (packages/types) — different package, disjoint.

    Comments re-read before claiming: three, all from the previous seat's session — routing (16:49Z), serial re-point (01:16Z), serial release (12:55Z). ⛔ No claim from another session. ⚠️ The 12:55Z note's own warning has since been overtaken by a further merge — see below.

    ⛔ First deliverable is a MEASUREMENT, not a repair

    Carried from the routing comment and still binding: this card is a code reading plus a mechanical enumeration, ⛔ not a driven measurement. The card says so itself — "no severity asserted and no direction measured".

    ⇒ Turn it into a reading first. ⛔ Do not infer from code shape.

    1. Is the permissive path reachable? Drive it: make the tenancy-posture probe fail and see whether the Layer 0 organization_membership_ended refusal is genuinely skipped.
    2. Is "on the single-kernel provider path kernel is undefined, so posture is ALWAYS undefined" true? If it holds, that is not an edge case on failure — it is the normal state, and it changes the whole character of the card.
    3. Are a failed auth-gate probe and an inactive gate really indistinguishable in the wire answer?
    4. Every zero owes a positive control — control the criterion itself, the way [finding] after the #13279 repair, an UNRESOLVABLE data engine still answers 403 FORBIDDEN — the last surviving GRANTS-LOST disguise at the package door #13476's dev did (show the criterion fires on a known positive at the merge base).

    ⭐ If it measures UNREACHABLE, that is a complete and valuable answer, and it downgrades a scary-reading code path into an evidenced non-problem. ⛔ Do not quietly convert a null result into a small fix to have something to show.

    ⚠️ The premise has moved TWICE since this card was written — re-derive, do not inherit

    rest-server.ts took three merges on 09-01 (#13811, #13910, #14120) and #13910 regenerated 10 census anchors. ⛔ Treat every line number on this card as rotten; locate by symbol.

    ⚠️ And one more the card's own notes do not yet carry: #13904 landed at 17:58:40Z (PR #14250), repairing the shipped objectQLProvider that feeds computeExecCtx. #13910 repaired the engine seam; #14250 repaired the provider one layer earlier. Both are upstream of the two seams this card names. ⇒ Confirm both seams are still live and still collapse on today's tree before implementing anything — the repairs may have moved shared helpers your seams also use.

    ⛔ Direction: this one is PERMISSIVE, and that is its whole point

    Unlike #13476 and #13904 — where an unknown was answered as a refusal — here a refusal is skipped. ⛔ Do not carry over the "no security claim" framing from those cards without measuring: if the permissive path is reachable, the grade moves and may become p1 + security, and that is my call to make on your reading, ⛔ not yours to pre-empt in either direction.

    ⛔ Equally: do not assert an access-control hole on a code reading. Measure, then report.

    ⛔ Do not enter #13905's or #13255's territory

    The auth-service seams and the getSession swallow are recorded as the CONTEXT-LOST degrade on #13255 and are ⛔ explicitly out of scope. The settings seam (locale/timezone) is noted on the card and is ⛔ not part of this.

    Route to a report

    Phase 2 (any repair) gets its own authorisation and file surface once I have ruled on your measurement. Stop and report after phase 1. Open a draft PR only if you have written tests; a measurement-only report with pr: null is a legitimate delivery.


    Generated by Claude Code

  6. 29 remaining items

  7. added a commit that references this issue on Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions