A role cannot narrow the skills it receives from a mounted provider #297

Closed
opened 2026-08-17 17:20:53 +00:00 by coilyco-ops · 0 comments
Member

The gap

A skill selector can only be declared on a provider or repository definition, never on the binding that attaches it to a role. So every role mounting a given path receives an identical skill set, and there is no way to give one role a subset of what another role gets.

Evidence

Read at 9039e92.

The bindings carry no selector. internal/schema/schema.go:

// RepositoryUse is one reference to a document-local repository definition.
type RepositoryUse struct {
	Repository string
}

ProviderUse is the same shape plus Required bool. The parse sites confirm it: use-provider rejects children outright with use-provider accepts no children, and use-repository rejects anything past the id with use-repository accepts only one argument.

The selectors live on the definitions. RepositoryDefinition and ProviderDefinition both carry Skills []string, parsed by parseSkillSelectorChildren. A definition's selector therefore applies uniformly to every role that mounts it.

Declaring the same path twice is closed in both directions. paths is a single map shared across the repositories and providers blocks:

  • repository declaration %q duplicates path %q already named by %s
  • providers %q and %q name the same repository path %q

So the obvious workaround, two declarations on one path with different selectors, fails whichever pair of node types you try.

Why it matters

The consumer is a private context repository with 64 entries mounted by one role. A second role needs roughly a quarter of them, and the excluded three quarters include third-party identity and NDA-bound material that must not reach that role, because it is the role that drafts outward-facing text. Today the only options are all 64 or nothing, and "nothing" is the safe answer, which means the role works from a less accurate secondary source instead.

Consumer detail: coilyco-bridge/agentic-os-kai#881.

Proposed shape

Accept a skill selector on the binding:

role "creator" {
    use-repository lore {
        skill "lore-self-*"
        skill "lore-rule-*"
    }
}

Reusing parseSkillSelectorChildren keeps the child syntax identical to the definition form.

Semantics worth deciding explicitly

  1. Intersect, do not replace. A binding selector should narrow within the definition's selector rather than override it, so a binding can never widen a role's reach past what the definition allows. Widening would turn a policy boundary into a suggestion.
  2. Empty intersection is an error. Consistent with the existing fail-closed posture, where Validate rejects an explicitly empty selector with skills selector is empty; omit skills to admit the whole provider.
  3. Keep the unmatched-pattern error, scoped to the intersection. Select already errors when a pattern matches nothing. A binding pattern that matches nothing after intersecting is a real authoring mistake and should say so.
  4. Keep the overlap error. Select rejects patterns that both admit one skill, with overlap on skill %q. That should hold within a binding's own selector.
  5. Omitted binding selector keeps today's behavior, admitting whatever the definition admits, so nothing existing changes.

Suggested tests

  • Binding selector narrows, and a second role mounting the same definition is unaffected.
  • Binding selector attempting to widen past the definition fails.
  • Empty binding selector fails with the existing message shape.
  • Binding pattern matching nothing after intersection fails and names the pattern.
  • Overlapping binding patterns fail with the overlap message.
  • Omitted binding selector is byte-identical to current output for an existing bundle.

Note on the workaround already applied

The consumer renamed all 64 entries onto seven non-overlapping category segments so the eventual allowlist is two globs rather than a long exact-name list. That was needed regardless, because the unmatched-pattern and overlap rules make a long exact-name selector brittle against any rename. It does not substitute for this change, it only makes the selector expressible once the change lands.

## The gap A skill selector can only be declared on a provider or repository **definition**, never on the **binding** that attaches it to a role. So every role mounting a given path receives an identical skill set, and there is no way to give one role a subset of what another role gets. ## Evidence Read at `9039e92`. **The bindings carry no selector.** `internal/schema/schema.go`: ```go // RepositoryUse is one reference to a document-local repository definition. type RepositoryUse struct { Repository string } ``` `ProviderUse` is the same shape plus `Required bool`. The parse sites confirm it: `use-provider` rejects children outright with `use-provider accepts no children`, and `use-repository` rejects anything past the id with `use-repository accepts only one argument`. **The selectors live on the definitions.** `RepositoryDefinition` and `ProviderDefinition` both carry `Skills []string`, parsed by `parseSkillSelectorChildren`. A definition's selector therefore applies uniformly to every role that mounts it. **Declaring the same path twice is closed in both directions.** `paths` is a single map shared across the `repositories` and `providers` blocks: * `repository declaration %q duplicates path %q already named by %s` * `providers %q and %q name the same repository path %q` So the obvious workaround, two declarations on one path with different selectors, fails whichever pair of node types you try. ## Why it matters The consumer is a private context repository with 64 entries mounted by one role. A second role needs roughly a quarter of them, and the excluded three quarters include third-party identity and NDA-bound material that must not reach that role, because it is the role that drafts outward-facing text. Today the only options are all 64 or nothing, and "nothing" is the safe answer, which means the role works from a less accurate secondary source instead. Consumer detail: `coilyco-bridge/agentic-os-kai#881`. ## Proposed shape Accept a skill selector on the binding: ```kdl role "creator" { use-repository lore { skill "lore-self-*" skill "lore-rule-*" } } ``` Reusing `parseSkillSelectorChildren` keeps the child syntax identical to the definition form. ## Semantics worth deciding explicitly 1. **Intersect, do not replace.** A binding selector should narrow within the definition's selector rather than override it, so a binding can never widen a role's reach past what the definition allows. Widening would turn a policy boundary into a suggestion. 2. **Empty intersection is an error.** Consistent with the existing fail-closed posture, where `Validate` rejects an explicitly empty selector with `skills selector is empty; omit skills to admit the whole provider`. 3. **Keep the unmatched-pattern error, scoped to the intersection.** `Select` already errors when a pattern matches nothing. A binding pattern that matches nothing after intersecting is a real authoring mistake and should say so. 4. **Keep the overlap error.** `Select` rejects patterns that both admit one skill, with `overlap on skill %q`. That should hold within a binding's own selector. 5. **Omitted binding selector keeps today's behavior**, admitting whatever the definition admits, so nothing existing changes. ## Suggested tests * Binding selector narrows, and a second role mounting the same definition is unaffected. * Binding selector attempting to widen past the definition fails. * Empty binding selector fails with the existing message shape. * Binding pattern matching nothing after intersection fails and names the pattern. * Overlapping binding patterns fail with the overlap message. * Omitted binding selector is byte-identical to current output for an existing bundle. ## Note on the workaround already applied The consumer renamed all 64 entries onto seven non-overlapping category segments so the eventual allowlist is two globs rather than a long exact-name list. That was needed regardless, because the unmatched-pattern and overlap rules make a long exact-name selector brittle against any rename. It does not substitute for this change, it only makes the selector expressible once the change lands.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/agent-compose#297
No description provided.