← index2026-05-03 06:43 (Beirut)(backfill from DOCUMENTATION/)

06 — Capabilities Resolver

06 — Capabilities Resolver

What a capability is

A capability is a composed action: "Brian can publish a blog post" or "Brian can run the daily cross-post." It's not a single tool call; it's a declarative description of what's required, what happens, and at what risk.

{
  "id": "cap.publish.fb_page_post",
  "name": "Publish to Facebook page",
  "requires": {
    "resources": ["acc.brian.fb_page", "key.meta_page_token", "chan.agency_pipeline"]
  },
  "side_effects": ["writes-external", "publishes-public", "irreversible-without-deletion"],
  "risk_level": "high",
  "cost_class": "free",
  "idempotency": "non-idempotent",
  "approval_required": false,
  "freshness_budget_hours": 24
}

The resolver walks requires, checks each dep's last_probe state, applies boundaries, and returns a verdict.

The resolver algorithm

def resolve(cap_id):
    cap = load_capability(cap_id)
    blocking, warnings, required_actions = [], [], []

    # 1. Walk deps
    for dep_id in flatten(cap.requires):
        state = atom_state(dep_id)         # fresh / stale / red / unknown
        if state == 'red':
            blocking.append(f"{dep_id}: red")
        elif state == 'stale':
            warnings.append(f"{dep_id}: stale")
            required_actions.append(f"probe:{dep_id}")
        elif state == 'unknown':
            warnings.append(f"{dep_id}: unknown")
            required_actions.append(f"probe:{dep_id}")

    # 2. Apply policy
    policy_blocks, policy_approvals, policy_advisories = check_policies(cap)
    blocking.extend(policy_blocks)
    if policy_approvals:
        required_actions.append("approval:jonah")
        required_actions.extend(policy_approvals)
    warnings.extend(policy_advisories)

    # 3. Decide verdict
    if blocking:
        if any(b.startswith("policy:") for b in blocking):
            return "blocked-by-policy"
        return "no"
    if required_actions:
        if any(a.startswith("approval:") for a in required_actions):
            return "yes-after-approval"
        return "yes-after-probe"
    return "yes"

Policy matcher (post-2026-05-02 fix)

check_policies(cap) walks boundaries.json rows where severity: hard. For each rule:

fired = (
    (m_se     specified) ⇒ (m_se ∩ cap.side_effects ≠ ∅)
  ∧ (m_cost   specified) ⇒ (m_cost == cap.cost_class)
  ∧ (m_risk   specified) ⇒ (m_risk == cap.risk_level)
  ∧ (m_id_re  specified) ⇒ (re.fullmatch(m_id_re, cap.id))
)

AND across clauses within a rule, OR across rules. Different rules contribute independently; the most restrictive verdict wins.

If cap.id ∈ rule.exceptions, the rule is skipped entirely.

For decision: deny_unless_brian_account, the resolver additionally checks whether the cap's requires JSON contains the substring "brian" (case-insensitive). If yes → advisory. If no → block, "no brian account in deps."

State tags after the fix

Cap Verdict Why
cap.memory.bloom_recall yes green deps, low risk, no boundary fires
cap.publish.fb_page_post yes-after-probe (typically) needs probe of brian's page token
cap.publish.linkedin_post no LI auth red
cap.business.stripe_charge yes-after-approval money — boundary.no_real_money_outflow_without_ask
cap.publish.daily_blog yes-after-probe exception list (no account ambiguity for self-hosted blog)

Bugs fixed during 2026-05-02 smoke test

Three semantic bugs in the resolver were caught during full subcommand smoke testing and fixed same session:

1. Match clauses used OR instead of AND

boundary.no_paid_model_calls declared side_effects_any: [costs-money] AND cost_class: paid. Pre-fix matcher fired on either. cap.business.stripe_charge (cost_class: metered, side_effects: costs-money) wrongly resolved to blocked-by-policy under the LLM rule, when it should have hit boundary.no_real_money_outflow_without_ask for yes-after-approval.

Fixed by counting clauses_specified vs clauses_matched and requiring equality.

2. boundary.no_jonah_personal_gmail_via_browser regex too broad

Was ^cap\.mac\..* — caught cap.mac.see_screen, cap.mac.listen_mic, etc. Tightened to ^cap\.mac\.(drive_chrome|run_command)$ (only caps that can actually open a browser tab on Jonah's gmail), and decision changed from deny to require_approval (runtime hook still inspects tool args for the gmail address).

3. boundary.brian_only_publisher and boundary.meta_only_brian_page regexes too broad

brian_only_publisher matched any writes-external. Tightened to ^cap\.(publish|ads)\..* with cap.publish.daily_blog exception (blog is intrinsically Brian-authored on Jonah's domain — no account-routing question).

meta_only_brian_page matched the same broad pattern. Narrowed to ^cap\.(publish\.(fb_page_post|ig_post|daily_cross_post)|ads\.meta_campaign)$.

Invariants checker (Item 7, landed 2026-05-03)

_check_invariants() in arg validate runs four cross-row checks:

  1. critical-needs-probe — every row with critical: true should have a probe block.
  2. grant-parties-resolve — every grants.json row's from_party, to_party, scope_account must point to known IDs.
  3. money-cap-needs-money-policy — if any cap has cost_class: metered/paid or side_effects: costs-money, at least one money-relevant boundary must exist.
  4. deny_unless_brian_account scope sanity — every cap matched by such a rule must reference a brian-* dep OR be in the rule's exceptions. Catches orphan publish caps before they runtime-block.

Surfaced 9 real gaps on first run; all reconciled. Verified working with deliberate-orphan injection.