Skip to content

runtime: two dispatcher domain gates read the tenancy posture through the collapsing resolveService probe, so a tenancy service that failed to build reads as "no wall" at /keys mint and at the activation-write refusal #15900

Description

@zhuangjianguo

Found while implementing #15366 (the same defect class, one seam over — measured on origin/main at 6acb37eb9; not addressed there, and out of that card's scope by site).

The two sites

Both derive the effective tenancy posture through deps.resolveService(context, 'tenancy', …) and a bare catch:

  • packages/runtime/src/domains/keys.tshandleKeysRequest's mint path: under a walled posture an org-less key is refused at mint time (walled && !activeOrganizationId). Its comment says an ABSENT service means "we cannot tell, and the honest answer at MINT time is to mint" — a deliberate decision for absent, which does not distinguish absent from failed.
  • packages/runtime/src/domains/activation-gate.tsrefuseUngrantedActivationWrite: under a wall-enforcing posture an organization administrator may not flip an install-wide activation row (ADR-0126 §5). With no posture the refusal is skipped (if (!posture || !postureEnforcesWall(posture)) return undefined;).

HttpDispatcher.resolveService is a capability probe: every step of its fallback chain absorbs every rejection and falls through to undefined, so a tenancy factory that threw and a name nothing registered both arrive at these gates as the same absent posture. The seam-level catch beneath it inspects nothing either.

Consequence, stated carefully (source reading, not an end-to-end run)

Under a wall-enforcing posture whose tenancy service is registered and fails to build:

  • POST /keys mints an organization-less key it would otherwise refuse;
  • an organization administrator's activation write is served instead of refused.

Both are the permissive direction #13906 decision 1 option A ruled against at the authorization-input seam: 「A posture that could not be READ is not a posture that is ABSENT.」 The fix for #15366 gives HttpDispatcher a classified lookup (resolveServiceOrLoud: branded "never registered" → quiet undefined; every other rejection re-raised) and uses it for the identity step only. These two sites were left as they are on purpose:

  • they are domain gates, not the authz-input seam — the loud answer there is a domain-level decision (the keys comment documents absent-posture-mint as deliberate, and re-reading it under the failed/absent split is a decision, not a mechanical edit);
  • DomainHandlerDeps.resolveService is the probe by contract, and rerouting 'tenancy' for every domain would change behaviour on every gate that reads it in one stroke.

Sizing note

In this tree tenancy is registered as an instance by plugin-auth (ctx.registerService('tenancy', …)), so the "registered and failed to build" class is reachable only through a factory registration by an embedder, or through the loader's other unbranded rejections (a scoped registration resolved without a scope id, a circular dependency). Same reachability as #15366, and the same reason it is not p0 on this reading.

Refs: #15366 (the identity-step fix) · #13906 (decision 1 option A) · #13905 (the registry brand) · ADR-0126 §5.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions