Skip to content

An empty scope ceiling means "no restriction" — flipping it to "nothing" requires the mint and Shield to change together #301

Description

@KunalJavelin

Sub-issue of highflame-ai/highflame-sdk#143 — the scope-mechanism review.

Three coupled changes. They cannot land separately — the sequencing section below explains why one of them alone makes the platform more permissive.

D3 — an empty ceiling means "no restriction"

intersectScopes returns the request unchanged when the allowed list is empty, per RFC 6749 §3.3:

func intersectScopes(requested, allowed []string) []string {
	if len(allowed) == 0 {
		return requested
	}
	...

So an identity registered with no allowed_scopes is not locked down. It is unbounded. This is the root cause of highflame-ai/highflame-authn#180 — the intersection already runs there; it is simply a no-op.

D4 — the mint drops unheld scopes silently and returns 200

apiKeyGrant hands the intersection straight to IssueCredential with no emptiness check. Ask for a b c while holding a b and you get a b, HTTP 200, no warning. Ask for only c and you get a token with no scopes claim, still HTTP 200.

D5 — Shield waves an empty scopes claim through

checkScopeCeiling in highflame-shield (internal/scheduler/nhi.go):

scopes := tenant.AgentIdentity.Scopes
if len(scopes) == 0 {
	// Token carries no `scopes` claim — treat as "scope check not
	// applicable" rather than "no permissions".
	return true, ""
}

Deliberate, and load-bearing: it is what keeps RS256 human tokens and pre-scope NHI tokens working. But it inverts the incentive. A token with some scopes and no tools:* is denied every action; a token with none is allowed everything.

Sequencing — why these are one change

Fixing D3 alone makes Shield more permissive. Make an empty ceiling grant nothing, and default-policy agents mint tokens with no scopes claim. D5 then reads that as "not applicable" and allows every action. Agents that hold nothing would be permitted everything.

D4 is the release valve: if the mint refuses to issue on an empty intersection, no such token exists for Shield to mishandle.

Fixing D3 also breaks the workaround highflame-ai/highflame-authn#181 documents. That issue tells people to request the union up front, which succeeds only because D3 grants unheld scopes today. The durable form does not depend on it: register the orchestrator with the union in its ceiling and request nothing.

Ask

Land as one change, behind a deprecation window:

  1. An empty ceiling means nothing, not "no restriction".
  2. The mint refuses an empty intersection with an explicit error rather than issuing a scope-less token.
  3. Shield stops treating an empty scopes claim as "not applicable" — needs its own ticket in highflame-shield, split out once the decision lands, and a plan for the RS256 and pre-scope paths that branch exists to protect.

Blocked on

The default-policy decision (sibling issue). Until the default policy carries a scope ceiling, flipping D3 strips every default-policy agent at once.

Activity

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

Metadata

Metadata

Assignees

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