What it is
CapabilityPolicyBinder (packages/capability-registry/src/CapabilityPolicyBinding.ts) is a
one-method class that enforces a single invariant: for a fixed set of production capabilities
(a connector action like hubspot:deal-update or paytm:refund), the policy the caller
declares must be the one canonical policy Parmana designates for that capability, never a
different, caller-chosen one.
It lives in its own package, @parmana/capability-registry, deliberately depending on nothing
except @parmana/shared, not on @parmana/policy, not on either connector package, so that
anything needing to ask “is this capability bound, and to what” can depend on just this map,
and @parmana/policy itself can depend on it without creating a dependency cycle back through a
connector package.
Why it was built
This is a distinct problem from Chapter 5’s signal-intent binding, easy to conflate with it because both involve the word “binding.” Signal-intent binding proves a policy’s declared signals match the real Intent once a policy has already been selected. Capability-policy binding is about which policy gets selected in the first place.CapabilityPolicyBinding.ts’s own doc comment (lines 74-96) states the gap directly: a
capability’s boundSignals/SignalStateVerifier protections are scoped to one specific policy.
Nothing upstream enforces that the request is actually evaluated against that policy ,
PolicyRouter/FilePolicyRepository load whatever policy.name/policy.version the caller
declares, PolicyEngine.evaluate takes no action parameter at all, and the connector-level
capability check only confirms the resolved connector declares the capability, never which
policy authorized it. A caller could therefore pair a real capability (hubspot:deal-update)
with an unrelated, unprotected policy (say, vendor-payment/2.0.0, which declares no
boundSignals for that capability at all), have that unrelated policy’s looser rules evaluated
against self-declared signals, and get the real hubspot:deal-update intent executed anyway ,
bypassing the capability’s intended protections entirely, not merely weakening them.
Found by an independent repository verification (Phase 2K, TD-22) and closed as its own
package extraction (G-30 architecture follow-up, see G-30-ARCHITECTURE-OPTIONS.md at the
repo root and docs/VERIFICATION-GAPS.md G-30 for the full history of why this specific
package boundary was chosen).
How it works
The canonical binding table
CANONICAL_CAPABILITY_POLICY_BINDINGS (CapabilityPolicyBinding.ts:43-71) is a ReadonlyMap
from capability string to PolicyReference ({ name, version, schemaVersion }). Current real
production bindings:
Every entry corresponds to a capability actually registered in production bootstrap
(
packages/api/src/bootstrap/createConnectorRegistry.ts) and the one policy file purpose-built
to authorize it. An action with no entry here, every test/tutorial/example fixture action, and
any future capability not yet given a canonical policy, is entirely unaffected by
CapabilityPolicyBinder. The table is additive, not a replacement for PolicyRouter/
PolicyEngine; it doesn’t change how a selected policy is loaded or evaluated, only whether
the caller was allowed to select a different one.
The request-time check
CapabilityPolicyBinder.findViolation(action, declared)
(CapabilityPolicyBinding.ts:104-123) looks up action in the canonical table. No entry means
nothing to enforce, returns undefined. An entry that matches the declared
(name, version) also returns undefined. Any mismatch returns a
CapabilityPolicyBindingViolation ({ action, expected, declared }), which the runtime
pipeline (Chapter 8) turns into a rejection before the (wrong) policy file is even evaluated.
The startup-time companion check
A second, separate mechanism enforces the other direction: that every capability a connector actually registers has either a canonical binding or an explicit, reasoned exemption.assertConnectorCapabilitiesBound
(packages/api/src/bootstrap/assertConnectorCapabilitiesBound.ts) runs once, at startup, from
createConnectorRegistry.ts, after every connector registration is built and before the
registry is handed back to the rest of bootstrap. For each registered capability, it checks
CANONICAL_CAPABILITY_POLICY_BINDINGS first, then falls back to
INTENTIONALLY_UNBOUND_CAPABILITIES (packages/api/src/bootstrap/intentionallyUnboundCapabilities.ts)
, a second allowlist for capabilities deliberately left unbound, each entry carrying a reason.
A capability found in neither map fails the process at startup, before it can bind a port. The
file’s own comment notes this assertion currently never fires outside a test-fixture exemption,
since every real production capability already has a binding, its purpose is making sure the
next connector added can’t silently ship the same gap CapabilityPolicyBinder was built to
close.
Together, the two checks form a closed loop: the startup check guarantees every real capability
is accounted for (bound or explicitly exempted), and the request-time check guarantees a caller
can’t route a bound capability through the wrong policy.
How it enables things, with a concrete example
- Tutorial 83 (
examples/tutorials/83-capability-policy-binding/run.ts) exercisesCapabilityPolicyBinderdirectly, including the exact TD-22 exploit shape (a real capability paired with an unrelated, unprotected policy) and confirms it’s now rejected. - Tutorial 31 (
examples/tutorials/31-authorization-binding/run.ts) covers a related but distinct binding concern at the execution-authorization layer, worth reading for contrast, not a substitute for Tutorial 83.
How to validate this yourself
packages/capability-registry/src/CapabilityPolicyBinding.ts(124 lines, read the whole file, every design decision is explained in its own doc comments).packages/capability-registry/tests/unit/CapabilityPolicyBinder.test.ts, its own header comment restates the exact exploit this class closes, and the test suite proves it.packages/api/src/bootstrap/assertConnectorCapabilitiesBound.tsandpackages/api/src/bootstrap/intentionallyUnboundCapabilities.tsfor the startup-time companion check.packages/api/src/bootstrap/createConnectorRegistry.tsto see exactly where and when the startup check runs relative to connector registration.
Integration requirements
None beyond registering a connector at all,CapabilityPolicyBinder needs no configuration.
Adding a new capability that should be protected this way means adding an entry to
CANONICAL_CAPABILITY_POLICY_BINDINGS; deliberately leaving one unprotected means adding a
reasoned entry to INTENTIONALLY_UNBOUND_CAPABILITIES instead, the startup assertion will
fail the process if a new capability is registered with neither.