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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions