Skip to content

[capability-policy] Availability resolver at the dispatch seam (ScopedLifecyclePolicyCapabilitySurfaceResolver) #5267

Description

@zetyquickly

Part of #5261 (epic) · continues #4628. The slice that makes a per-user availability grant actually change the model-visible tool surface.

What

ScopedLifecyclePolicyCapabilitySurfaceResolver, a CapabilitySurfaceProfileResolver (crates/ironclaw_loop_support/src/capability_allow_set.rs), that:

  1. extracts the (tenant_id, user_id) principal from LoopRunContextactor.user_id first, then scope.explicit_owner_user_id();
  2. builds ScopedLifecycleSubject::new(tenant, user) and calls feat(reborn): scoped-lifecycle admin install store (#3288) — availability foundation for #5261 #4544's list_effective_installations(subject) (its default impl already filters enabled + ownership-visibility: AdminShared → all users in tenant, UserPrivate → owner only);
  3. maps each installed LifecyclePackageRef → CapabilityId(s) via a PackageCapabilitySource;
  4. returns CapabilityAllowSet::Allowlist(...).

Availability is sourced from #4544 effective installations — this PR depends only on #4544, NOT on #5262's EffectivePolicy fold. Config / identity / approval layer on top later (#5273).

PackageCapabilitySource (grounded)

visible_capability_ids come from the extension manifest [capabilities] filtered to Visibility::Model (available_extensions.rs:visible_capability_ids()), deterministic at parse time. There is no cross-boundary facade (product_workflow cannot depend on composition-scoped lifecycle), so the resolver gets a PackageCapabilitySource seeded at composition from the AvailableExtensionCatalog. Covers WASM extensions cleanly; skills / MCP / WASM-kind don't expose cap-ids the same way yet → extension-tools-first.

Fail-closed (grounded — Err kills the turn)

resolve() is called once per turn at host build (loop_driver_host.rs:1367); a returned Err maps to HostFactoryError and fails the whole turn. Therefore:

  • no resolvable user / no grants → Ok(Allowlist(empty)) (deny all, turn still runs — a user with no grants can still chat);
  • genuine store / internal error → Err (fail-closed — never expose tools on uncertainty).

Wiring

Replaces EmptyCapabilitySurfaceResolver (production) and AllowAllCapabilitySurfaceResolver (local-dev) in crates/ironclaw_reborn_composition/src/runtime.rs (~2828 / ~2852). Must coexist with SubagentCapabilitySurfaceResolver (subagent intersection). The local-dev swap is what makes the effect observable for the manual test.

Tests

At the host-factory level (build_text_only_host_with_profiled_capabilities), modeled on crates/ironclaw_reborn/tests/runtime_policy_tool_visibility_integration.rs: admin-shared visible to any user; user-private only to its owner; disabled excluded; no-user → empty; mapping correctness.

Depends on: #4544. Co-land bundle.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestrebornIronClaw Reborn architecture and landing work

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions