Skip to content

Scope Intersection

Scope intersection is the permission model at the heart of LumenFlow’s security. Before any tool executes, the kernel computes the intersection of four independent scope layers. A tool can only operate within the permissions that all four layers jointly permit.

LayerSourceWho controls itExample
Workspaceworkspace.yaml security.allowed_scopesWorkspace administratorsrc/** (read+write), no network
LaneLane configuration allowed_scopesLane ownersrc/core/** (write), ** (read)
TaskTask spec declared_scopesTask authorsrc/core/auth/** (write)
ToolPack manifest required_scopesPack author** (write) — tool needs write access

The intersection narrows progressively:

Workspace allows: src/** (write)
Lane allows:      src/core/** (write)
Task declares:    src/core/auth/** (write)
Tool requires:    ** (write)

Intersection: src/core/auth/** (write)
  ← narrowest pattern that all four agree on

The tool can only write to src/core/auth/**. Even though the tool itself requests ** (everything), the workspace, lane, and task scopes constrain it.

For each scope type (path-read, path-write, network), the kernel:

  1. Takes every possible combination across the four layers
  2. Checks that all four patterns overlap (using micromatch as the matching library)
  3. Selects the narrowest pattern by specificity score (literal characters minus wildcard penalties)
  4. Deduplicates results by access:pattern key

If any layer provides zero scopes for a given access type, the result for that access type is empty — which means denial.

Workspace allows: src/** (write)
Lane allows:      tests/** (write)       ← no overlap with workspace
Task declares:    src/core/** (write)
Tool requires:    ** (write)

Intersection: [] (empty)
  → SCOPE_DENIED

The workspace and lane have no overlapping write patterns, so the intersection is empty and the tool call is denied.

LumenFlow supports two scope types:

required_scopes:
  - type: path
    pattern: 'src/**'
    access: read # or 'write'

Path patterns use glob syntax (* for single segment, ** for any depth). The kernel uses micromatch for pattern matching and a synthetic-path heuristic for containment checking.

required_scopes:
  - type: network
    posture: full # 'off' | 'allowlist' | 'full'

Network scopes control whether a tool can access the network and, if so, which hosts it can reach. The posture field accepts three values:

PostureBehavior
offAll network access blocked
allowlistOnly traffic to explicitly listed hosts/CIDRs is permitted
fullUnrestricted network access

When posture is allowlist, the scope must include an allowlist_entries array listing permitted destinations:

required_scopes:
  - type: network
    posture: allowlist
    allowlist_entries:
      - 'registry.npmjs.org:443'
      - '10.0.0.0/24'

Each entry is either a host:port pair (e.g., registry.npmjs.org:443) or a CIDR block (e.g., 10.0.0.0/24). The allowlist_entries field is required when posture is allowlist and must contain at least one entry.

Network posture intersection follows deny-wins semantics across the four scope layers:

  1. off blocks everything. If any layer declares off, the tool cannot use the network regardless of what other layers say.
  2. full is compatible with allowlist. When one layer declares full and another declares allowlist, the effective posture downgrades to allowlist. The full layer imposes no restriction, so the allowlist layer’s entries apply.
  3. Allowlist entries are set-intersected. When multiple layers declare allowlist, only entries present in all layers survive the intersection. An entry must appear in every restricting layer to be permitted.
Workspace: posture=full
Lane:      posture=allowlist, entries=[registry.npmjs.org:443, api.github.com:443]
Task:      posture=allowlist, entries=[registry.npmjs.org:443]
Tool:      posture=allowlist, entries=[registry.npmjs.org:443, api.github.com:443]

Intersection: posture=allowlist, entries=[registry.npmjs.org:443]
  ← only entry present in all restricting layers

If the intersection produces an empty entry set, the result is denial — the tool cannot access the network.

Regardless of what scopes are configured, the kernel unconditionally denies writes to .lumenflow/**. This protects the kernel’s own state (event log, evidence store, task specs) from being modified by any tool, even if all four scope layers would otherwise allow it.

This check runs before scope intersection — it cannot be overridden by any configuration.

The 4-level model provides several guarantees:

  1. Principle of least privilege. Each layer narrows permissions. A broad tool scope is constrained by a narrow task scope.
  2. No privilege escalation. A lower layer cannot grant permissions that a higher layer denied. The intersection can only shrink, never grow.
  3. Fail-closed. Empty intersection = denied. Missing scopes at any layer = denied. The safe direction is always denial.
  4. Defense in depth. Even if one layer is misconfigured (e.g., ** for everything), the other three layers still constrain the effective scope.

Consider a workspace with two lanes and a tool that writes files:

# workspace.yaml
security:
  allowed_scopes:
    - type: path
      pattern: '**'
      access: write

# Lane: Framework Core
allowed_scopes:
  - type: path
    pattern: 'src/core/**'
    access: write

# Lane: Experience UI
allowed_scopes:
  - type: path
    pattern: 'src/components/**'
    access: write

An agent working in the “Framework Core” lane on a task scoped to src/core/auth/** can write to src/core/auth/** — but cannot touch src/components/ (lane boundary) or docs/ (task boundary), even though the workspace allows **.

  • Kernel Runtime — Where scope intersection happens in the execution pipeline
  • Policy Engine — The policy layer that runs alongside scope checks
  • Packs — How tools declare their required scopes
  • Lanes — How lanes define scope boundaries