Filed from Reclaim Your Week, Daybreak's first leaf. Two halves of one gap, found together.
1. Module capabilities do not inherit exposure enforcement
facetAllows is called by the framework's own fill_slot and get_state and by nothing else. BaseCapability does nothing with context.customConfig.
So a leaf that declares a capability on its own ModuleDefinition and grants it an ExposureConfig gets a config that silently enforces nothing. The grant looks identical to one that works.
We believed for a year that our write allowlist was enforced twice — once as data on the grant, once in code. It was enforced once, in code. Nothing was actually broken, because the code layer is the one that ran, but the invariant documenting it was wrong, and worse: our guard test asserted against a hand-written mirror of facetAllows rather than the function itself, so it passed while proving only that the data said what we meant.
The docs read as though the enforcement is automatic, which is how we got it wrong. CapabilityContext.customConfig is even documented as "a capability can read its own per-binding configuration inside execute()" — accurate, but it reads as a convenience rather than as the only thing that will ever apply it.
Suggested shape: a BaseModuleCapability, or a withExposure() wrapper, that applies the grant's facets before execute. Failing that, a sentence in BaseCapability saying the binding config is inert unless the capability applies it itself.
2. facetSchema cannot express a slug-level rule
facetSchema is .strict() on { groups, scopes }. A slot group that legitimately mixes machine-computed lanes with user self-reports cannot express its real permission as data.
Ours does exactly that: one group holds both per-bucket totals computed from an uploaded file (which a model must never write) and the answers to two questions the source tells the agent to ask before any file exists. We had to permit the whole group on the grant and keep the computed lanes shut in code — the two layers deliberately not identical, which is a thing that will read as a bug to the next person unless they find the comment.
Any facilitation module with a computed branch beside a discursive one hits this.
Suggested shape: slots?: string[] and/or denySlots?: string[] on the facet.
Delegate-when-it-lands
The leaf moves its slug allowlist onto the grant and deletes the code-side special case, and drops the parse-and-check block from its capability once BaseCapability applies facets.
Filed from Reclaim Your Week, Daybreak's first leaf. Two halves of one gap, found together.
1. Module capabilities do not inherit exposure enforcement
facetAllowsis called by the framework's ownfill_slotandget_stateand by nothing else.BaseCapabilitydoes nothing withcontext.customConfig.So a leaf that declares a capability on its own
ModuleDefinitionand grants it anExposureConfiggets a config that silently enforces nothing. The grant looks identical to one that works.We believed for a year that our write allowlist was enforced twice — once as data on the grant, once in code. It was enforced once, in code. Nothing was actually broken, because the code layer is the one that ran, but the invariant documenting it was wrong, and worse: our guard test asserted against a hand-written mirror of
facetAllowsrather than the function itself, so it passed while proving only that the data said what we meant.The docs read as though the enforcement is automatic, which is how we got it wrong.
CapabilityContext.customConfigis even documented as "a capability can read its own per-binding configuration insideexecute()" — accurate, but it reads as a convenience rather than as the only thing that will ever apply it.Suggested shape: a
BaseModuleCapability, or awithExposure()wrapper, that applies the grant's facets beforeexecute. Failing that, a sentence inBaseCapabilitysaying the binding config is inert unless the capability applies it itself.2.
facetSchemacannot express a slug-level rulefacetSchemais.strict()on{ groups, scopes }. A slot group that legitimately mixes machine-computed lanes with user self-reports cannot express its real permission as data.Ours does exactly that: one group holds both per-bucket totals computed from an uploaded file (which a model must never write) and the answers to two questions the source tells the agent to ask before any file exists. We had to permit the whole group on the grant and keep the computed lanes shut in code — the two layers deliberately not identical, which is a thing that will read as a bug to the next person unless they find the comment.
Any facilitation module with a computed branch beside a discursive one hits this.
Suggested shape:
slots?: string[]and/ordenySlots?: string[]on the facet.Delegate-when-it-lands
The leaf moves its slug allowlist onto the grant and deletes the code-side special case, and drops the parse-and-check block from its capability once
BaseCapabilityapplies facets.