Skip to content

feat(reborn): capability-policy control plane — REST users + admin grants (epic #5261) - #5355

Closed
zetyquickly wants to merge 38 commits into
mainfrom
feat/capability-policy-control-plane
Closed

zetyquickly wants to merge 38 commits into
mainfrom
feat/capability-policy-control-plane

Conversation

@zetyquickly

@zetyquickly zetyquickly commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Part of epic #5261 (capability policy). This is the top of the merge chain:

#5262 (feat/capability-policy) → engine (#5344) → availability (#5349) → control-plane (this PR)

Builds on the availability PR plus #5270 (UserRole / is_admin), which this branch merges in.

What this adds (control plane)

Correctness anchor

Over the capability-policy crates (ironclaw_capability_policy, ironclaw_product_workflow_storage, ironclaw_loop_support, ironclaw_host_runtime, ironclaw_reborn_composition, ironclaw_reborn_webui_ingress, ironclaw_reborn_cli, .claude/rules/rest-endpoints.md, docs/reborn/acme-capability-policy-walkthrough.md), this branch's tree is byte-equal to the fully-tested integration branch feat/capability-policy-milestone. The stack was carved from that integration tree, so the top of the chain reproduces it exactly.

Gates run (all green)

  • cargo fmt / cargo fmt --check
  • cargo clippy -p ironclaw_reborn_composition --features capability-policy,webui-v2-beta --tests -- -D warnings
  • cargo clippy -p ironclaw_reborn_webui_ingress --all-features --tests -- -D warnings
  • cargo build -p ironclaw_reborn_cli --features webui-v2-beta,capability-policy
  • cargo clippy -p ironclaw_reborn_composition --tests -- -D warnings (feature OFF)

🤖 Generated with Claude Code


Role-model expansion (#5261)

This PR now also carries the role hierarchy guards (self-delete / strict-rank / single-owner on user delete), admin-can't-change-a-non-subordinate's-caps on the grant route, approval-pref gating by availability + settings/tools PUT, and the executable xyzorg e2e (9 passed / 1 xfail gdrive-OAuth) that is the ground-truth driver.

serrrfirat and others added 28 commits June 8, 2026 13:54
…ifecycle-admin

# Conflicts:
#	Cargo.lock
#	FEATURE_PARITY.md
#	crates/ironclaw_product_workflow/src/lib.rs
#	crates/ironclaw_product_workflow_storage/Cargo.toml
#	crates/ironclaw_product_workflow_storage/src/lib.rs
#	crates/ironclaw_product_workflow_storage/tests/durable_ledger_contract.rs
Settled Reborn-stack design for admin-shared tools/skills + per-user auth: one account type (User = secrets + tools + memory), Owner/Admin/Member roles, the four-dimension capability policy (availability/configuration/identity/approval) resolving capability default -> tenant -> user, plus a prior-art/compose-with map anchored to #4628 (#4544, #1626, #3289/#4354, #4527) and verified live-code seams.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First independent slice of the #4628 continuation: the four-dimension policy vocabulary and the pure precedence cascade, with no dependency on #4544. Availability / IdentityMode (user-keyed vs admin-keyed) / config / approval (reusing the existing PermissionMode). resolve_effective_policy() is an order-independent, most-specific-wins fold with deep-merged config. PolicyResolver async port is the seam a #4544-backed adapter fills. Depends only on ironclaw_host_api; clippy -D warnings clean; 8 unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pability defaults

A CapabilityDefaultPolicySource trait + in-memory StaticCapabilityDefaultPolicySource supplying the per-capability default policy (architecture doc §7) keyed by CapabilityId, over a conservative global fallback (hidden + ask). Sources the default without adding a field to the 49-construction-site CapabilityDescriptor, keeping the slice #4544-independent. clippy -D warnings clean; 3 new tests (11 total).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebase #4544 onto current main (106 commits of drift). Resolved 2 conflicts: product_workflow/src/lib.rs export list (kept both LifecycleSearchExtensionSummary from main and lifecycle_package_kind_label from #4544); FEATURE_PARITY.md (kept #4544's scoped-lifecycle clause on Hosted MCP, main's newer #5256 rows for NEAR AI MCP and Tool policies).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…er (#5266)

Gives the Reborn WebChat-v2 stack a typed user role so the facade can gate admin operations — the prerequisite for admin-grants-permissions in epic #5261.

- ironclaw_host_api: new UserRole { Owner > Admin > Member } + UserStatus authority enums (alongside UserId/TenantId); wire-stable snake_case, as_str/parse with least-privilege fallback, is_admin/is_owner.

- ironclaw_reborn_identity: surface role/status on UserRecord + the persisted StoredUser (serde-default so pre-existing records rehydrate to member/active); re-export the enums so crate::UserRole resolves.

- WebuiAuthentication carries role (env-bearer operator authenticates as Owner; default Member; session/OIDC populate from the persisted record in a follow-up). WebUiAuthenticatedCaller carries role + is_admin() (role.is_admin() || operator_webui_config — operator flag kept as a compat shim). The admin gate USAGE lands with the grant/revoke methods in #5268.

clippy -D warnings clean across host_api/identity/product_workflow/composition; host_api role + identity tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`ScopedLifecyclePolicyCapabilitySurfaceResolver` derives the per-(tenant,
user) capability allow-set from #4544's scoped-lifecycle installations
(admin-shared -> all users in the tenant; user-private -> owner only;
disabled excluded), mapping each installed package to its model-visible
capability ids via a `PackageCapabilitySource` seeded from the first-party
extension catalog (`visible_capability_ids`, manifest `Visibility::Model`).

Fail-closed but graceful: the resolver never returns `Err` (which would
abort the turn at host construction). No resolvable user, a user with no
grants, or a store read failure (including a not-yet-created installation
set) all deny every capability while the turn still runs.

Wired into `build_reborn_runtime`'s local-dev branch behind a new
`capability-policy` feature (compiles the resolver + the
`ironclaw_product_workflow_storage` dep) and *activated* per-runtime by
`IRONCLAW_REBORN_CAPABILITY_POLICY` (default off, mirroring the
`HooksActivationConfig` master-flag-default-off pattern). With the feature
absent or the flag off, local-dev keeps the historical `AllowAll` surface,
so existing flows and all current tests are unchanged.

Tests: 8 module unit tests (admin-shared/user-private visibility, disabled
exclusion, no-user + store-failure graceful deny, package->cap mapping,
real first-party catalog seeding, principal precedence). Full composition
lib suite (852) green under `--features capability-policy`; clippy clean in
default and `--all-features` states.

Part of #5261. Depends on #4544.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…§17)

Design §1-§14 holds; §17 captures what's built (epic #5261 child issues) and the
corrections implementation forced: first-party integrations are WASM extensions
not MCP (§7/§12 fixed); per-user availability is a CapabilityPolicyDelta (#4544
can_be_mutated_by forbids an admin writing UserPrivate for others, so #4544 is
tenant-shared + per-user rides deltas); the resolver is feature+IRONCLAW_REBORN_CAPABILITY_POLICY
gated (default off); the scoped-lifecycle store roots under the /tenants durable
mount (the /engine default has no backend); #5272 reworked to REST-created users;
approval has no admin path yet; Reborn admin gate is host_api::UserRole not
src/ownership.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…5273) (#5288)

The storage + resolution foundation for the configuration / identity / approval
dimensions. Adds, in `ironclaw_capability_policy`:

- `CapabilityPolicyDeltaStore` — durable store of per-(tenant, scope, capability)
  `CapabilityPolicyDelta` rows (the admin grants). Keyed with an explicit tenant
  (PolicyScope carries none) so deltas never leak across tenants;
  upsert/delete/`deltas_for`/`list_subject_deltas`. The admin REST surface
  (#5268) writes here; the resolver reads. Mirrors the #4544 store shape.
- `InMemoryCapabilityPolicyDeltaStore` — in-memory backend for tests / local-dev
  (durable filesystem / libSQL backend is the follow-on).
- `StoreBackedPolicyResolver` — implements the #5262 `PolicyResolver` port by
  folding the capability default (#5263 `CapabilityDefaultPolicySource`) with the
  subject's stored deltas via `resolve_effective_policy` into an
  `EffectivePolicy`. Resolution is live (reads the store every call).

`deltas_for` pre-filters to the subject (tenant-wide row + the subject's own user
row; project scope is dormant in v1). Availability in the resulting
`EffectivePolicy` is the policy view (default + deltas); combining it with the
installation view (#4544 / #5267) — plus injecting config, applying the identity
ownership filter, and merging the approval admin layer — is the enforcement layer
(the remaining #5273 work).

Tests: 6 (upsert/read-back/delete, tenant-wide vs user-only visibility, cross-
tenant isolation, default->tenant->user fold incl. config deep-merge + approval
override, default-only when no deltas, subject-scoped listing). Crate green under
`--all-features` clippy `-D warnings`.

Part of #5261. Depends on #5262, #5263.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…r, identity/config/approval enforcement (epic #5261)

Slices the #4544-INDEPENDENT capability-policy engine out of the tested
integration branch onto #5262 (main + the ironclaw_capability_policy crate).

Contains:
- durable FilesystemCapabilityPolicyDeltaStore (libSQL/filesystem) + its
  contract test, behind the storage crate's new ironclaw_capability_policy dep;
- StoreBackedPolicyResolver wired via capability_policy_engine.rs
  (local_dev_capability_policy_delta_store + build_capability_policy_resolver);
- the identity/config/approval dispatch seams (PolicyResolverConfigSource,
  PolicyResolverAdminApprovalSource) wired through factory.rs + local_dev.rs.

Builds on #5262. Independent of #4544: no scoped-lifecycle store,
capability_surface_policy, or control-plane route modules are present, and the
crate compiles + tests pass without any #4544 file. The acting-principal
derivation the config seam needs is inlined into the engine so it carries no
dependency on the availability surface resolver.

Enforces three of the four capability-policy dimensions (identity, config,
approval); availability stays AllowAll on this branch and lands in a separate
availability PR. Gated by the `capability-policy` feature + the
IRONCLAW_REBORN_CAPABILITY_POLICY env toggle (off by default).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…min' into feat/capability-policy-availability

# Conflicts:
#	Cargo.lock
#	crates/ironclaw_product_workflow_storage/Cargo.toml
#	crates/ironclaw_reborn_composition/Cargo.toml
#	crates/ironclaw_reborn_composition/src/lib.rs
…ycle store + dispatch resolver combine (epic #5261)

Builds on the capability-policy engine (#5344) + #4544's scoped-lifecycle
installation store. Adds the availability dimension of the four-dimension
capability model:

- #4544's scoped-lifecycle install store (merged in): the durable
  installed-package surface, disjoint from the engine's delta store.
- The dispatch-seam surface resolver (capability_surface_policy.rs,
  ScopedLifecyclePolicyCapabilitySurfaceResolver): availability = installed
  AND policy-available — it intersects the installed set from #4544's store
  with the engine's shared EffectivePolicy.available, behind the SINGLE
  shared policy resolver constructed once in factory.rs. runtime.rs wires it
  in, falling back to AllowAll when capability_policy is not activated.
- The availability admin REST surface (#5268, capability_admin_routes.rs):
  admin-gated tenant-wide extension install/list/uninstall writing into the
  same scoped-lifecycle store the resolver reads.

This is the only slice that depends on #4544. Availability-only: it does not
contain the control-plane files (local_user_directory, capability_user_policy_routes)
and does not modify serve.rs or the ingress crate — those are the
control-plane branch's job.

Note: the role-based admin gate (WebUiAuthenticatedCaller::is_admin /
UserRole) lands with the user-role work (#5270), which is not in this slice's
base. ensure_admin gates on the operator-config admin bit until #5270 merges
(see TODO(#5261) in capability_admin_routes.rs). The CLI capability-policy
feature forwards only to the composition crate; the ingress-crate forward is
added by the control-plane branch (#5272).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…le home (#5261)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…min REST moves to control plane (#5261)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ant surfaces (epic #5261)

Builds on the availability branch + #5270 (UserRole/is_admin). Adds the
REST-created local users directory (#5272), the availability admin REST
surface (#5268), and the per-user 4-dimension capability-policy grant REST.
This is the top of the merge chain (#5262 -> engine -> availability ->
control-plane); over the capability-policy crates its tree equals the
tested integration branch (origin/feat/capability-policy-milestone).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 26, 2026 16:53
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@github-actions github-actions Bot added scope: docs Documentation scope: dependencies Dependency updates size: XL 500+ changed lines risk: medium Business logic, config, or moderate-risk modules contributor: core 20+ merged PRs labels Jun 26, 2026
@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added capability-policy enforcement at dispatch (availability, identity/config overlays, and approval).
    • Added role-aware local user directory admin APIs and a durable user-directory-backed WebUI authenticator.
    • Added tenant-scoped extension administration plus per-user capability-policy controls.
    • Added admin-configurable policy-backed scoped lifecycle installations (durable).
    • Added WebChat v2 support for PUT to set tool permission (canonical).
  • Bug Fixes
    • Improved replay behavior to avoid re-applying policy overlays on resume.
  • Documentation
    • Added capability-policy architecture, parity audit, and end-to-end walkthrough docs.

Walkthrough

Adds a new ironclaw_capability_policy crate, scoped lifecycle storage, admin REST mounts, local user auth, and feature-gated runtime wiring so capability availability, config, identity, and approval are enforced at dispatch and credential resolution.

Changes

Capability Policy End-to-End

Layer / File(s) Summary
Policy and identity contracts
crates/ironclaw_host_api/src/role.rs, crates/ironclaw_host_api/src/lib.rs, crates/ironclaw_reborn_identity/src/lib.rs, crates/ironclaw_reborn_identity/src/filesystem_store/record.rs, crates/ironclaw_capability_policy/src/lib.rs, crates/ironclaw_product_workflow/src/webui_inbound.rs, crates/ironclaw_turns/src/scope.rs, crates/ironclaw_product_workflow/src/lifecycle.rs, crates/ironclaw_product_workflow/src/lib.rs, Cargo.toml, crates/ironclaw_capability_policy/Cargo.toml
Adds shared role/status enums, typed user record role/status fields, the capability policy types and defaults, and caller role propagation.
Policy resolution and delta storage
crates/ironclaw_capability_policy/src/store.rs, crates/ironclaw_product_workflow_storage/src/capability_policy_delta.rs, crates/ironclaw_product_workflow_storage/tests/durable_capability_policy_delta_contract.rs
Adds the policy delta store, resolver, filesystem-backed persistence, and durable contract coverage.
Scoped lifecycle model and storage
crates/ironclaw_product_workflow/src/scoped_lifecycle.rs, crates/ironclaw_product_workflow_storage/src/scoped_lifecycle/*, crates/ironclaw_product_workflow_storage/tests/durable_ledger_contract.rs, crates/ironclaw_product_workflow_storage/src/lib.rs, crates/ironclaw_product_workflow_storage/AGENTS.md
Adds the scoped lifecycle ownership model, effective-resolution rules, filesystem store implementation, backend wrappers, and contract tests.
Dispatch and approval enforcement
crates/ironclaw_loop_support/src/capability_port.rs, crates/ironclaw_host_runtime/src/obligations.rs, crates/ironclaw_host_runtime/src/wasm_credentials.rs, crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials.rs, crates/ironclaw_reborn_composition/src/profile_approval_authorization.rs, crates/ironclaw_reborn_composition/src/local_dev_authorization.rs, crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials/tests.rs, crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials/tests/duplicate_selection.rs
Threads capability ids through credential resolution, adds policy config deep-merge at dispatch, and inserts admin approval precedence in the approval gate.
Admin REST surfaces
crates/ironclaw_reborn_composition/src/local_user_directory.rs, crates/ironclaw_reborn_composition/src/capability_admin_routes.rs, crates/ironclaw_reborn_composition/src/capability_user_policy_routes.rs, .claude/rules/rest-endpoints.md
Adds the local user directory, extension install/uninstall/list routes, per-user capability policy routes, and the endpoint catalog notes for those surfaces.
WebUI v2 route surface
crates/ironclaw_webui_v2/src/descriptors.rs, crates/ironclaw_webui_v2/src/router.rs, crates/ironclaw_webui_v2/src/lib.rs, crates/ironclaw_webui_v2/tests/webui_v2_descriptors_contract.rs, crates/ironclaw_webui_v2/tests/webui_v2_handlers_contract.rs
Adds the canonical PUT route descriptor, router wiring, and contract tests for the per-tool permission settings endpoint.
Composition and feature wiring
crates/ironclaw_reborn_composition/src/capability_policy_engine.rs, crates/ironclaw_reborn_composition/src/capability_surface_policy.rs, crates/ironclaw_reborn_composition/src/factory.rs, crates/ironclaw_reborn_composition/src/runtime.rs, crates/ironclaw_reborn_composition/src/runtime/local_dev/*, crates/ironclaw_reborn_composition/src/webui.rs, crates/ironclaw_reborn_composition/src/product_live_adapters.rs, crates/ironclaw_reborn_composition/src/lib.rs, crates/ironclaw_reborn_cli/src/commands/serve.rs, crates/ironclaw_reborn_webui_ingress/src/lib.rs, crates/ironclaw_reborn_webui_ingress/CLAUDE.md, crates/ironclaw_reborn_webui_ingress/Cargo.toml
Builds the policy adapters, scoped lifecycle surface resolver, local-dev handle threading, layered authenticators, and role propagation through the WebUI path.
Documentation
docs/plans/2026-06-24-capability-policy-architecture.md, docs/reborn/acme-capability-policy-walkthrough.md, V1_REBORN_PARITY_AUDIT.md, FEATURE_PARITY.md
Updates the architecture plan, walkthrough, parity audit, and feature-parity note.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

Possibly related PRs

  • nearai/ironclaw#4588: Both touch crates/ironclaw_loop_support/src/capability_port.rs at the same dispatch-input point.
  • nearai/ironclaw#5063: Both rewrite profile_approval_authorization.rs precedence and hard-floor behavior.
  • nearai/ironclaw#4939: Both modify the runtime credential selection path where capability_id is now threaded.

Suggested reviewers

  • think-in-universe
  • serrrfirat
  • henrypark133

Poem

🦀 A policy winds through every gate,
A scoped install learns shared vs private fate.
Tokens hash and roles now speak,
The right path bends, but only where rules peak.
One feature flag lights the whole parade.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR body is off-template and omits several required sections, including Change Type, Security Impact, Database Impact, Blast Radius, Rollback Plan, and Review Follow-Through. Rewrite the PR body to match the repository template and fill every required section, including the linked issue, validation, security, database impact, blast radius, rollback plan, and review notes.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title uses Conventional Commits style and accurately summarizes the capability-policy control-plane changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/capability-policy-control-plane

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR completes the Reborn “capability-policy” control plane by wiring in multi-user local-dev auth (REST-created users + roles), admin-gated REST surfaces for tenant-wide installs and per-user 4D policy deltas, and the runtime seams that enforce availability/config/identity/approval using a single shared policy resolver + durable stores.

Changes:

  • Add durable scoped-lifecycle installation storage (admin-shared vs user-private) and intersect it with policy-derived availability at the dispatch seam.
  • Introduce role-carrying WebUI authentication + a layered authenticator that resolves REST-minted user tokens via a durable local user directory (operator env-bearer remains the bootstrap fallback).
  • Implement durable capability-policy delta storage + resolver adapters for config deep-merge, identity enforcement at credential resolution, and admin approval precedence in the dispatch approval chain.

Reviewed changes

Copilot reviewed 67 out of 68 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
FEATURE_PARITY.md Updates parity notes to reflect scoped lifecycle foundation for admin/user package resolution.
docs/reborn/acme-capability-policy-walkthrough.md Adds the hand-driven Acme walkthrough for creating users and granting policy via REST.
crates/ironclaw_reborn_webui_ingress/src/lib.rs Adds local user-directory authenticator + layered authenticator and tests.
crates/ironclaw_reborn_webui_ingress/CLAUDE.md Documents new authenticators and their intended layering.
crates/ironclaw_reborn_webui_ingress/Cargo.toml Adds capability-policy feature forwarding to composition.
crates/ironclaw_reborn_identity/src/lib.rs Re-exports UserRole/UserStatus and adds them to UserRecord.
crates/ironclaw_reborn_identity/src/filesystem_store/record.rs Persists role/status with serde defaults for backward compatibility.
crates/ironclaw_reborn_identity/src/filesystem_store.rs Initializes stored users with default role/status on create.
crates/ironclaw_reborn_composition/tests/product_live_adapters.rs Updates adapter config to include optional policy config source under feature.
crates/ironclaw_reborn_composition/src/webui_serve.rs Adds role propagation through WebUI authentication into the caller extension.
crates/ironclaw_reborn_composition/src/slack_personal_binding_pairing_serve.rs Updates test caller construction to include role.
crates/ironclaw_reborn_composition/src/slack_host_beta.rs Updates test caller construction to include role.
crates/ironclaw_reborn_composition/src/slack_channel_routes/allowed/tests.rs Updates test caller construction to include role.
crates/ironclaw_reborn_composition/src/slack_channel_routes.rs Updates test caller construction to include role.
crates/ironclaw_reborn_composition/src/runtime/local_dev/tests.rs Threads optional policy config source through local-dev runtime tests.
crates/ironclaw_reborn_composition/src/runtime/local_dev/shell_tests.rs Threads optional policy config source through shell tests.
crates/ironclaw_reborn_composition/src/runtime/local_dev/refreshing_capability_port.rs Plumbs optional policy config source into the refreshing local-dev port.
crates/ironclaw_reborn_composition/src/runtime/local_dev.rs Creates PolicyResolverConfigSource from the shared policy resolver and wires it into port creation.
crates/ironclaw_reborn_composition/src/runtime.rs Wires scoped-lifecycle-backed capability surface resolver when policy is activated; otherwise falls back to AllowAll.
crates/ironclaw_reborn_composition/src/profile_approval_authorization.rs Adds admin approval precedence (deny/allow) ahead of the existing user/profile chain while preserving the hard floor.
crates/ironclaw_reborn_composition/src/product_live_adapters.rs Threads optional policy config source into product-live capability port factory under feature.
crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials/tests/duplicate_selection.rs Updates credential resolver tests for the new capability_id field in requests.
crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials.rs Enforces identity mode via policy resolver at credential selection/refresh and asserts ownership class.
crates/ironclaw_reborn_composition/src/product_auth_durable/tests.rs Updates tests for new capability_id in runtime credential resolution requests.
crates/ironclaw_reborn_composition/src/product_auth_durable/interactions.rs Notes TODO for tagging credential ownership based on identity policy once flows carry capability/resolver.
crates/ironclaw_reborn_composition/src/product_auth_durable/flows.rs Notes TODO for tagging credential ownership based on identity policy once flows carry capability/resolver.
crates/ironclaw_reborn_composition/src/local_dev_authorization.rs Threads optional admin approval source into local-dev authorizer construction.
crates/ironclaw_reborn_composition/src/lib.rs Registers capability-policy modules and re-exports policy/local-user-directory types behind feature gates.
crates/ironclaw_reborn_composition/src/factory.rs Builds and stores the single shared policy delta store + resolver handles; wires admin approval and credential identity enforcement.
crates/ironclaw_reborn_composition/src/capability_policy_engine.rs New: constructs durable delta store + policy resolver; provides config/approval adapters and activation gate.
crates/ironclaw_reborn_composition/Cargo.toml Adds capability-policy feature enabling policy engine + storage deps.
crates/ironclaw_reborn_cli/src/commands/serve.rs Layers local user directory auth over operator auth; mounts new admin REST surfaces when available.
crates/ironclaw_reborn_cli/Cargo.toml Adds CLI feature forwarding for capability-policy (composition + optional ingress).
crates/ironclaw_product_workflow/src/webui_inbound.rs Adds role to authenticated caller and implements is_admin() gate.
crates/ironclaw_product_workflow/src/scoped_lifecycle.rs New: defines scoped lifecycle ownership model + effective resolution logic.
crates/ironclaw_product_workflow/src/lifecycle.rs Adds stable string labels for lifecycle package kinds.
crates/ironclaw_product_workflow/src/lib.rs Exposes scoped lifecycle types and kind label helper from the workflow crate.
crates/ironclaw_product_workflow_storage/tests/durable_ledger_contract.rs Extends durable contract tests to cover scoped lifecycle stores across libSQL/Postgres.
crates/ironclaw_product_workflow_storage/tests/durable_capability_policy_delta_contract.rs New: durable contract test for capability policy delta store + resolver fold.
crates/ironclaw_product_workflow_storage/src/scoped_lifecycle/store/tests.rs New: unit tests for filesystem scoped lifecycle installation store behavior and CAS semantics.
crates/ironclaw_product_workflow_storage/src/scoped_lifecycle/paths.rs New: path layout for scoped lifecycle durable records.
crates/ironclaw_product_workflow_storage/src/scoped_lifecycle/entries.rs New: serialization and indexing for scoped lifecycle records + tombstones.
crates/ironclaw_product_workflow_storage/src/scoped_lifecycle.rs New: scoped lifecycle storage module wiring and shared helpers/errors.
crates/ironclaw_product_workflow_storage/src/lib.rs Exports new scoped lifecycle and capability-policy delta storage adapters.
crates/ironclaw_product_workflow_storage/Cargo.toml Adds deps needed for scoped lifecycle + capability policy delta storage.
crates/ironclaw_product_workflow_storage/AGENTS.md Updates crate purpose/boundaries to include new workflow ports beyond the ledger.
crates/ironclaw_loop_support/src/lib.rs Re-exports the new LoopCapabilityConfigSource trait.
crates/ironclaw_loop_support/src/capability_port.rs Adds dispatch-time policy config deep-merge (non-replay only) and tests; plumbs config source through port factory.
crates/ironclaw_loop_support/Cargo.toml Adds ironclaw_capability_policy dependency for merge utility usage.
crates/ironclaw_host_runtime/src/wasm_credentials.rs Threads capability_id into runtime credential account requests.
crates/ironclaw_host_runtime/src/obligations.rs Adds capability_id to RuntimeCredentialAccountRequest and threads it through built-in obligation handling.
crates/ironclaw_host_api/src/role.rs New: defines wire-stable UserRole/UserStatus enums with parsing helpers.
crates/ironclaw_host_api/src/lib.rs Exposes the new role module.
crates/ironclaw_capability_policy/src/store.rs New: defines delta store trait, in-memory implementation, and store-backed resolver.
crates/ironclaw_capability_policy/Cargo.toml New crate manifest for the capability-policy model/store/resolver.
Cargo.toml Adds ironclaw_capability_policy to workspace members.
Cargo.lock Adds lock entries for new crate/deps.
.claude/rules/rest-endpoints.md New: catalogs WebChat v2 and admin REST endpoints including capability-policy surfaces.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +227 to +231
pub(crate) fn capability_policy_activated() -> bool {
std::env::var("IRONCLAW_REBORN_CAPABILITY_POLICY")
.map(|value| matches!(value.trim(), "1" | "true" | "yes" | "on"))
.unwrap_or(false)
}
Comment on lines +68 to +72
/// - `None` — no credential needed.
/// - `UserKeyed` — the user supplies their own key ("introduce yourself"); a
/// missing key triggers an auth gate.
/// - `AdminKeyed` — an admin supplies a shared key; the user uses it but cannot
/// set it. A missing key resolves to *unavailable*.
@railway-app

railway-app Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-5355 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jun 26, 2026 at 9:45 pm

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
crates/ironclaw_reborn_composition/src/profile_approval_authorization.rs (1)

280-307: 🎯 Functional Correctness | 🔴 Critical

Disabled per-tool override incorrectly yields gate (RequireApproval) instead of Deny for forced-effect tools.

The code order at lines 288-290 places the effects_force_approval check before the per-tool Disabled check at lines 303-307. This causes a reachable regression: when a user explicitly disables a forced-effect tool (e.g., Financial) with no admin opinion, the flow returns RequireApproval instead of Deny. This violates the stated contract at line 299 ("den outright (strongest user intent)") and contradicts the "Fail closed" guideline for approvals and trust.

The hard floor's purpose (#4776/#4959) is to prevent auto-approval of forced-effect tools. Deny is not auto-approval—it is an explicit rejection. Therefore, a user's Disabled override must take precedence over the hard floor requirement. The hard floor should only block admin Allow from auto-approving, not override user opt-outs.

Restore the Disabled check before the hard floor gate, or move the hard floor check below the Disabled override while keeping it above admin Allow. Add a regression test for Disabled + Financial + admin None → Deny.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_reborn_composition/src/profile_approval_authorization.rs`
around lines 280 - 307, The forced-effect hard floor in
profile_approval_authorization::authorization flow is taking precedence over an
explicit per-tool Disabled override, causing RequireApproval instead of Deny for
tools like Financial. Update the precedence in the decision chain so
settings.tool_override(...)->Disabled is checked before
gate_policy.effects_force_approval(&gate_effects), while still keeping the hard
floor above admin Allow. Verify the behavior around require_approval(),
Decision::Deny, and the admin_approval handling, and add a regression test for
Disabled + forced-effect + no admin opinion returning Deny.

Source: Coding guidelines

crates/ironclaw_reborn_composition/src/product_auth_durable/flows.rs (1)

457-468: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Admin-keyed provisioning still dead-ends on first account creation.

This TODO is documenting a real behavior gap: the branch below still creates new accounts as CredentialOwnership::UserReusable, while ProductAuthRuntimeCredentialResolver::resolve_access_secret rejects that ownership for IdentityMode::AdminKeyed and returns Backend. First-time OAuth/manual-token provisioning therefore leaves admin-keyed capabilities unavailable instead of provisioning a shared-admin account. Fix both creation paths together, not just this OAuth branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_reborn_composition/src/product_auth_durable/flows.rs` around
lines 457 - 468, Admin-keyed provisioning still falls back to UserReusable,
which `ProductAuthRuntimeCredentialResolver::resolve_access_secret` will reject
for `IdentityMode::AdminKeyed`. Update the new-account creation logic in both
provisioning paths, not just the OAuth callback branch, so
`NewCredentialAccount` uses the correct shared-admin ownership when admin-keyed
provisioning is intended. Use the existing
`credential_status_for_completed_flow`, `NewCredentialAccount`, and
`ProductAuthRuntimeCredentialResolver::resolve_access_secret` symbols to locate
the affected flow and align account creation with the resolver’s ownership
expectations.
crates/ironclaw_reborn_composition/src/product_auth_durable/tests.rs (1)

250-263: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

This test does not cover the new capability-aware enforcement.

With no policy resolver installed and only a UserReusable account in the store, resolve_access_secret() returns the same account regardless of capability_id. A regression that drops capability_id in BuiltinObligationHandler::inject_credential_accounts or RuntimeCredentialRestager::stage_for_request_async would still pass here. Add a caller-level regression where two capabilities resolve differently and drive the real staging path. As per path instructions, "Test through the caller" applies when a helper gates a side effect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_reborn_composition/src/product_auth_durable/tests.rs` around
lines 250 - 263, The current test only exercises
ProductAuthRuntimeCredentialResolver::resolve_access_secret directly, so it
misses capability-aware enforcement and could pass even if capability_id is
dropped earlier in the flow. Add a caller-level regression that goes through the
real staging path, using RuntimeCredentialRestager::stage_for_request_async and
the path that invokes BuiltinObligationHandler::inject_credential_accounts, with
two different CapabilityId values resolving to different outcomes. Keep the test
focused on the end-to-end behavior from the caller so it verifies the capability
is preserved through staging and account injection.

Source: Path instructions

crates/ironclaw_reborn_composition/src/slack_host_beta.rs (1)

1671-1678: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use an admin caller in this admin-route test.

This test still expects the channel-route write to succeed, but it now injects UserRole::Member. That no longer matches the runtime admin path and would let a member-access regression slip through unnoticed.

Suggested fix
                     .extension(WebUiAuthenticatedCaller {
                         tenant_id: TenantId::new(TENANT).expect("tenant"),
                         user_id: UserId::new(USER).expect("user"),
                         agent_id: Some(AgentId::new(AGENT).expect("agent")),
                         project_id: Some(ProjectId::new(PROJECT).expect("project")),
                         operator_webui_config: true,
-                        role: ironclaw_host_api::UserRole::Member,
+                        role: ironclaw_host_api::UserRole::Admin,
                     })

Based on learnings, "SSO sessions are user identity only; they must not inherit operator WebUI configuration privileges from the deployment."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_reborn_composition/src/slack_host_beta.rs` around lines 1671
- 1678, The admin-route test is using a WebUiAuthenticatedCaller with
UserRole::Member, which no longer exercises the admin execution path. Update the
test setup in the WebUiAuthenticatedCaller extension to use an admin caller/role
so the channel-route write is validated under the same privileges as production
admin handling, and keep the SSO session identity separate from operator WebUI
configuration privileges.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/ironclaw_capability_policy/src/store.rs`:
- Around line 175-177: The lock-error handling in the capability policy store is
dropping the original poisoning cause by using `map_err(|_|
PolicyError::Internal { ... })`, so update the affected `Store` methods that
call `self.deltas.write`, `self.deltas.read`, and the other matching lock
acquisitions to preserve and propagate the underlying error context instead of
discarding it. Adjust the `map_err` conversions so the resulting
`PolicyError::Internal` includes the original lock error details/cause, keeping
the failure loud and traceable across all four affected sites.
- Around line 116-143: The in-memory delta key is stringly typed and should be
replaced with typed domain values to match the project’s typed-internals rule.
Update the storage key in store.rs around DeltaKey, scope_key, and the related
read/write paths in the policy store to use specialized types or a structured
key enum instead of tuples of String. Preserve the existing
tenant/scope/capability uniqueness semantics while moving the key-shape logic
out of ad-hoc string formatting and into typed constructors/matching.

In `@crates/ironclaw_host_api/src/role.rs`:
- Around line 80-87: `UserStatus::parse()` in `role.rs` currently maps any
unrecognized persisted string to `Active`, which can incorrectly re-enable
accounts. Update the parsing logic so only truly missing/pre-migration values
keep the backward-compatible `Active` default, while unknown strings fail closed
by returning an error or mapping to a disabled status such as
`Suspended`/`Deactivated`. Use the `UserStatus::parse` symbol and keep the known
status matches (`suspended`, `deactivated`) unchanged.

In `@crates/ironclaw_product_workflow_storage/AGENTS.md`:
- Around line 13-22: The crate contract in AGENTS.md is stale because it only
describes idempotency and scoped-lifecycle storage, but this crate now also
persists capability-policy data. Update the crate-purpose/boundaries text to
mention capability-policy persistence alongside the existing storage
responsibilities, and keep the ownership boundaries clear so only storage
adapters are described. Use the existing AGENTS.md contract section as the place
to align this with the current durable-storage behavior.

In `@crates/ironclaw_product_workflow_storage/src/capability_policy_delta.rs`:
- Around line 103-117: Reject PolicyScope::Project at write time in
CapabilityPolicyDeltaStorage so unsupported project-scoped deltas cannot be
persisted and later silently disappear from reads. Update upsert_delta() and
delete_delta() to fail closed for Project, or otherwise add full project
identity support through subject_scopes(), scope_applies_to_subject(), and the
read paths before allowing it. Use the existing CapabilityPolicyDeltaStorage
methods and PolicyScope handling to keep the boundary consistent.

In `@crates/ironclaw_product_workflow_storage/src/scoped_lifecycle.rs`:
- Around line 78-89: The scoped lifecycle durable error path is only logging the
error type, so the real backend/path/migration cause is lost. Update
scoped_lifecycle_durable_error() to log the underlying error value (using
error.to_string() or equivalent) alongside the operation, and keep the
ProductWorkflowError context by mapping the failure with the actual reason
instead of only returning a generic transient message. Use the existing
scoped_lifecycle_durable_error() and scoped_lifecycle_transient() flow so the
durable-store failure remains traceable end to end.

In `@crates/ironclaw_product_workflow_storage/src/scoped_lifecycle/store.rs`:
- Around line 443-446: The listing path is silently skipping records whose
parsed tenant_id does not match the tenant-scoped query, which should fail
closed instead of returning a partial view. Update list_installations() in
scoped_lifecycle/store.rs to treat a tenant mismatch from
parse_versioned_scoped_lifecycle_installation as an error and propagate it with
? (using the existing thiserror/context style), rather than conditionally
pushing only matching installations. Keep the tenant invariant handling
consistent with load_installation() so corrupted or cross-tenant records are
surfaced immediately.

In `@crates/ironclaw_product_workflow/src/scoped_lifecycle.rs`:
- Around line 160-170: ScopedLifecycleInstallation currently has two competing
identities: the store APIs use installation_id while the durable path is derived
from tenant, ownership, and package_ref. Update
ScopedLifecycleInstallationStore::get_installation() and delete_installation()
to use the same canonical key as the persistence layer, or make installation_id
part of the persisted path in scoped_lifecycle paths so there is only one source
of truth. Ensure all read/write/delete paths in ScopedLifecycleInstallation and
the related storage helpers resolve the installation by a single identity tuple,
not mixed keys.
- Around line 21-66: ScopedLifecycleInstallationId should follow the canonical
validated-newtype pattern instead of a manual serde/newtype setup. Update the
type in scoped_lifecycle.rs to use a shared validate(&str) helper on
ScopedLifecycleInstallationId, make new() validate an owned String via that
helper, and add the standard TryFrom<String>/From<ScopedLifecycleInstallationId>
for String conversion path alongside as_str()/as_ref()/into_inner(). Replace the
custom Deserialize impl with serde’s try_from = "String" flow so
serialization/deserialization uses the same validation path as CredentialName
and ExtensionName.

In `@crates/ironclaw_product_workflow/src/webui_inbound.rs`:
- Around line 119-125: Keep is_admin() strictly role-based by removing the
operator_webui_config fallback from WebUiInbound::is_admin and relying only on
self.role.is_admin(). The operator bearer path in webui_serve already maps to
Owner, so preserve that there and keep the downstream admin REST surfaces gated
by the resolved user role rather than the deployment config bit.

In `@crates/ironclaw_reborn_composition/src/capability_admin_routes.rs`:
- Around line 184-194: The admin_shared_installation_id helper currently derives
the shared installation ID by lossy sanitizing package.id, which can collide for
distinct packages like foo.bar and foo-bar. Update admin_shared_installation_id
in capability_admin_routes to build the ScopedLifecycleInstallationId from a
collision-resistant stable encoding or hash that incorporates both package kind
and package id, rather than recomputing from a sanitized string; keep the
identifier strongly typed by using the existing LifecyclePackageRef fields
directly and only converting to the final ScopedLifecycleInstallationId at the
end.
- Around line 228-233: The install_extension_handler currently extracts
Json(request) before any admin authorization, allowing non-admin callers to
receive body-parse feedback on an admin-only route. Move the admin check into a
FromRequestParts-based extractor that runs before Json, and wire it into
install_extension_handler so ensure_admin() (or equivalent auth gating) happens
first and fails closed before request body parsing.

In `@crates/ironclaw_reborn_composition/src/capability_policy_engine.rs`:
- Around line 33-43: The policy dispatch is resolving different principals
across dimensions, so availability/config and approval/identity can evaluate
against inconsistent users. Update the capability policy flow in
capability_policy_engine to carry a single PolicySubject through dispatch, or
make PolicyResolverAdminApprovalSource and resolve_identity_mandate use the same
helper that availability/config already use. Keep tenant/user scope intact on
authority and state records so every policy dimension resolves the same subject
before merge.

In `@crates/ironclaw_reborn_composition/src/capability_surface_policy.rs`:
- Around line 15-17: The module-level contract comment in
capability_surface_policy is stale and still describes layered policy work as
follow-on `#5273`, but the file now implements policy-backed availability
intersection. Update the comment near the top of the file to reflect the current
split of responsibilities, keeping the description aligned with the behavior in
resolve_effective_policy and the availability/installation signal handling.

In `@crates/ironclaw_reborn_composition/src/local_user_directory.rs`:
- Around line 286-291: `set_role` in `LocalUserDirectory` currently does a stale
read-modify-write of `StoredUser`, which can overwrite a concurrent token
rotation and revive an old bearer. Update the `set_role` flow to preserve the
`VersionedEntry` version returned by `get`, then write through the existing
persistence path with `CasExpectation::Version` instead of `Any`, and handle
version mismatches by retrying or failing closed. Use the existing `get`,
`put_json`, and `StoredUser`/`VersionedEntry` symbols to keep the role update
consistent with concurrent `create_user` token updates.

In `@crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials.rs`:
- Around line 607-619: The `identity_mandate` check in
`resolve_runtime_credentials` is too strict for `IdentityMode::UserKeyed` and
now rejects valid `ExtensionOwned` runtime credentials by treating only
`UserReusable` as acceptable. Update the `match identity_mandate` guard so
`UserKeyed` only requires a user-owned/authorized runtime credential path
consistent with existing resolver behavior, and do not block `ExtensionOwned`
accounts that already satisfy the caller-level contract. Keep the `AdminKeyed`
branch unchanged and preserve the existing `AuthRequired` fallback only for
truly incompatible ownership cases.

In
`@crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials/tests.rs`:
- Around line 1313-1378: `FakePolicyResolver` only verifies the returned
`IdentityMode`, so the new tests don’t prove the resolver is called with the
correct `PolicySubject` and `CapabilityId`. Add a caller-level test around the
real request flow in `ProductAuthRuntimeCredentialResolver` that uses a
recording policy resolver to capture the observed `(subject, capability)` from
`resolve`, then assert they match the `RuntimeCredentialAccountRequest` values.
Keep the helper, but make the new test drive the actual call site rather than
only the fake resolver mapping.

In `@crates/ironclaw_reborn_webui_ingress/src/lib.rs`:
- Around line 343-360: The token resolution path in the authenticator currently
collapses store failures into `None`, which makes `resolve_token` errors look
like invalid bearer tokens. Update the auth boundary around `resolve_token` and
`WebuiAuthentication::user(...).with_role(...)` so it can return a distinct auth
error/result for backend outages, while keeping `Ok(None)` as the only
invalid-token case. Then have the middleware map store errors to a sanitized
unavailable response (503) and unknown tokens to 401, preserving the existing
warning log in the error branch.

In `@docs/plans/2026-06-24-capability-policy-architecture.md`:
- Around line 267-278: The logical data model and store contract for
CapabilityPolicyDelta are missing Tenant even though the resolution cascade and
PolicyScope already support it. Update the CapabilityPolicyDelta definition and
the CapabilityPolicyStore port spec to include Tenant alongside Project and
User, and make sure the scope_id behavior and related wording stay consistent
with the existing tenant_delta[capability] resolution path.

---

Outside diff comments:
In `@crates/ironclaw_reborn_composition/src/product_auth_durable/flows.rs`:
- Around line 457-468: Admin-keyed provisioning still falls back to
UserReusable, which
`ProductAuthRuntimeCredentialResolver::resolve_access_secret` will reject for
`IdentityMode::AdminKeyed`. Update the new-account creation logic in both
provisioning paths, not just the OAuth callback branch, so
`NewCredentialAccount` uses the correct shared-admin ownership when admin-keyed
provisioning is intended. Use the existing
`credential_status_for_completed_flow`, `NewCredentialAccount`, and
`ProductAuthRuntimeCredentialResolver::resolve_access_secret` symbols to locate
the affected flow and align account creation with the resolver’s ownership
expectations.

In `@crates/ironclaw_reborn_composition/src/product_auth_durable/tests.rs`:
- Around line 250-263: The current test only exercises
ProductAuthRuntimeCredentialResolver::resolve_access_secret directly, so it
misses capability-aware enforcement and could pass even if capability_id is
dropped earlier in the flow. Add a caller-level regression that goes through the
real staging path, using RuntimeCredentialRestager::stage_for_request_async and
the path that invokes BuiltinObligationHandler::inject_credential_accounts, with
two different CapabilityId values resolving to different outcomes. Keep the test
focused on the end-to-end behavior from the caller so it verifies the capability
is preserved through staging and account injection.

In `@crates/ironclaw_reborn_composition/src/profile_approval_authorization.rs`:
- Around line 280-307: The forced-effect hard floor in
profile_approval_authorization::authorization flow is taking precedence over an
explicit per-tool Disabled override, causing RequireApproval instead of Deny for
tools like Financial. Update the precedence in the decision chain so
settings.tool_override(...)->Disabled is checked before
gate_policy.effects_force_approval(&gate_effects), while still keeping the hard
floor above admin Allow. Verify the behavior around require_approval(),
Decision::Deny, and the admin_approval handling, and add a regression test for
Disabled + forced-effect + no admin opinion returning Deny.

In `@crates/ironclaw_reborn_composition/src/slack_host_beta.rs`:
- Around line 1671-1678: The admin-route test is using a
WebUiAuthenticatedCaller with UserRole::Member, which no longer exercises the
admin execution path. Update the test setup in the WebUiAuthenticatedCaller
extension to use an admin caller/role so the channel-route write is validated
under the same privileges as production admin handling, and keep the SSO session
identity separate from operator WebUI configuration privileges.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c145fedf-3866-41f6-9e27-a9d6c068a606

📥 Commits

Reviewing files that changed from the base of the PR and between 185ce88 and 61e8ebf.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (67)
  • .claude/rules/rest-endpoints.md
  • Cargo.toml
  • FEATURE_PARITY.md
  • V1_REBORN_PARITY_AUDIT.md
  • crates/ironclaw_capability_policy/Cargo.toml
  • crates/ironclaw_capability_policy/src/lib.rs
  • crates/ironclaw_capability_policy/src/store.rs
  • crates/ironclaw_host_api/src/lib.rs
  • crates/ironclaw_host_api/src/role.rs
  • crates/ironclaw_host_runtime/src/obligations.rs
  • crates/ironclaw_host_runtime/src/wasm_credentials.rs
  • crates/ironclaw_loop_support/Cargo.toml
  • crates/ironclaw_loop_support/src/capability_port.rs
  • crates/ironclaw_loop_support/src/lib.rs
  • crates/ironclaw_product_workflow/src/lib.rs
  • crates/ironclaw_product_workflow/src/lifecycle.rs
  • crates/ironclaw_product_workflow/src/scoped_lifecycle.rs
  • crates/ironclaw_product_workflow/src/webui_inbound.rs
  • crates/ironclaw_product_workflow_storage/AGENTS.md
  • crates/ironclaw_product_workflow_storage/Cargo.toml
  • crates/ironclaw_product_workflow_storage/src/capability_policy_delta.rs
  • crates/ironclaw_product_workflow_storage/src/lib.rs
  • crates/ironclaw_product_workflow_storage/src/scoped_lifecycle.rs
  • crates/ironclaw_product_workflow_storage/src/scoped_lifecycle/entries.rs
  • crates/ironclaw_product_workflow_storage/src/scoped_lifecycle/paths.rs
  • crates/ironclaw_product_workflow_storage/src/scoped_lifecycle/store.rs
  • crates/ironclaw_product_workflow_storage/src/scoped_lifecycle/store/tests.rs
  • crates/ironclaw_product_workflow_storage/tests/durable_capability_policy_delta_contract.rs
  • crates/ironclaw_product_workflow_storage/tests/durable_ledger_contract.rs
  • crates/ironclaw_reborn_cli/Cargo.toml
  • crates/ironclaw_reborn_cli/src/commands/serve.rs
  • crates/ironclaw_reborn_composition/Cargo.toml
  • crates/ironclaw_reborn_composition/src/capability_admin_routes.rs
  • crates/ironclaw_reborn_composition/src/capability_policy_engine.rs
  • crates/ironclaw_reborn_composition/src/capability_surface_policy.rs
  • crates/ironclaw_reborn_composition/src/capability_user_policy_routes.rs
  • crates/ironclaw_reborn_composition/src/factory.rs
  • crates/ironclaw_reborn_composition/src/lib.rs
  • crates/ironclaw_reborn_composition/src/local_dev_authorization.rs
  • crates/ironclaw_reborn_composition/src/local_user_directory.rs
  • crates/ironclaw_reborn_composition/src/product_auth_durable/flows.rs
  • crates/ironclaw_reborn_composition/src/product_auth_durable/interactions.rs
  • crates/ironclaw_reborn_composition/src/product_auth_durable/tests.rs
  • crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials.rs
  • crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials/tests.rs
  • crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials/tests/duplicate_selection.rs
  • crates/ironclaw_reborn_composition/src/product_live_adapters.rs
  • crates/ironclaw_reborn_composition/src/profile_approval_authorization.rs
  • crates/ironclaw_reborn_composition/src/runtime.rs
  • crates/ironclaw_reborn_composition/src/runtime/local_dev.rs
  • crates/ironclaw_reborn_composition/src/runtime/local_dev/refreshing_capability_port.rs
  • crates/ironclaw_reborn_composition/src/runtime/local_dev/shell_tests.rs
  • crates/ironclaw_reborn_composition/src/runtime/local_dev/tests.rs
  • crates/ironclaw_reborn_composition/src/slack_channel_routes.rs
  • crates/ironclaw_reborn_composition/src/slack_channel_routes/allowed/tests.rs
  • crates/ironclaw_reborn_composition/src/slack_host_beta.rs
  • crates/ironclaw_reborn_composition/src/slack_personal_binding_pairing_serve.rs
  • crates/ironclaw_reborn_composition/src/webui_serve.rs
  • crates/ironclaw_reborn_composition/tests/product_live_adapters.rs
  • crates/ironclaw_reborn_identity/src/filesystem_store.rs
  • crates/ironclaw_reborn_identity/src/filesystem_store/record.rs
  • crates/ironclaw_reborn_identity/src/lib.rs
  • crates/ironclaw_reborn_webui_ingress/CLAUDE.md
  • crates/ironclaw_reborn_webui_ingress/Cargo.toml
  • crates/ironclaw_reborn_webui_ingress/src/lib.rs
  • docs/plans/2026-06-24-capability-policy-architecture.md
  • docs/reborn/acme-capability-policy-walkthrough.md

Comment on lines +116 to +143
/// Stable per-row key component for a scope: `tenant` / `project:<id>` /
/// `user:<id>`. Matches the store's `(tenant, scope, capability)` keying so an
/// upsert replaces the delta at the same scope+capability.
fn scope_key(scope: &PolicyScope) -> String {
match scope {
PolicyScope::Tenant => "tenant".to_string(),
PolicyScope::Project { project_id } => format!("project:{}", project_id.as_str()),
PolicyScope::User { user_id } => format!("user:{}", user_id.as_str()),
}
}

/// `true` when a delta at `scope` applies to `subject`: the tenant-wide row, or
/// the subject's own user row.
fn scope_applies_to_subject(scope: &PolicyScope, subject: &PolicySubject) -> bool {
match scope {
PolicyScope::Tenant => true,
PolicyScope::User { user_id } => user_id == &subject.user_id,
// Project scope is dormant in v1 (default project == tenant) and the
// subject carries no project id to match against.
PolicyScope::Project { .. } => false,
}
}

/// Map key for a stored delta: `(tenant, scope_key, capability)`.
type DeltaKey = (String, String, String);
/// Stored value: the delta plus its owning tenant (carried so reads can filter
/// by tenant — `PolicyScope` itself has no tenant).
type StoredDelta = (TenantId, CapabilityPolicyDelta);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Keep the in-memory key typed.

DeltaKey = (String, String, String) plus scope_key() turns TenantId, CapabilityId, and PolicyScope into ad-hoc strings inside the system. That breaks the repo’s “Typed Internals — No Stringly-Typed Values Inside the System” invariant and makes key-shape drift a runtime bug instead of a compile-time one.

Refactor sketch
-type DeltaKey = (String, String, String);
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+enum ScopeKey {
+    Tenant,
+    Project(ProjectId),
+    User(UserId),
+}
+
+type DeltaKey = (TenantId, ScopeKey, CapabilityId);

As per coding guidelines, “Every domain value gets a specialized type” and “Use enums over stringly-typed control flow when the shape is known.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_capability_policy/src/store.rs` around lines 116 - 143, The
in-memory delta key is stringly typed and should be replaced with typed domain
values to match the project’s typed-internals rule. Update the storage key in
store.rs around DeltaKey, scope_key, and the related read/write paths in the
policy store to use specialized types or a structured key enum instead of tuples
of String. Preserve the existing tenant/scope/capability uniqueness semantics
while moving the key-shape logic out of ad-hoc string formatting and into typed
constructors/matching.

Sources: Coding guidelines, Path instructions

Comment on lines +175 to +177
let mut deltas = self.deltas.write().map_err(|_| PolicyError::Internal {
reason: "capability policy delta store lock poisoned".to_string(),
})?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Don’t drop the poisoning cause.

These map_err(|_| PolicyError::Internal { ... }) conversions discard the original error, which violates the repo’s fail-loud rule and leaves poisoned-lock failures without a causal trail.

Fix pattern
-        let mut deltas = self.deltas.write().map_err(|_| PolicyError::Internal {
-            reason: "capability policy delta store lock poisoned".to_string(),
-        })?;
+        let mut deltas = self.deltas.write().map_err(|e| PolicyError::Internal {
+            reason: format!("capability policy delta store lock poisoned: {e}"),
+        })?;

As per coding guidelines, .map_err(|_| OtherError) is banned because it drops the underlying cause; path instructions also require failures to propagate loudly with context.

Also applies to: 189-191, 201-203, 219-221

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_capability_policy/src/store.rs` around lines 175 - 177, The
lock-error handling in the capability policy store is dropping the original
poisoning cause by using `map_err(|_| PolicyError::Internal { ... })`, so update
the affected `Store` methods that call `self.deltas.write`, `self.deltas.read`,
and the other matching lock acquisitions to preserve and propagate the
underlying error context instead of discarding it. Adjust the `map_err`
conversions so the resulting `PolicyError::Internal` includes the original lock
error details/cause, keeping the failure loud and traceable across all four
affected sites.

Sources: Coding guidelines, Path instructions

Comment on lines +80 to +87
/// Parse a persisted status. Unknown or missing values fall back to
/// [`UserStatus::Active`] (the DB-level default).
pub fn parse(value: &str) -> Self {
match value.trim().to_ascii_lowercase().as_str() {
"suspended" => Self::Suspended,
"deactivated" => Self::Deactivated,
_ => Self::Active,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed on unknown UserStatus values.

UserStatus::parse() currently revives any unrecognized persisted value as Active. That turns typo/corrupt/future status strings into a usable account instead of keeping the user blocked. Keep the backward-compatible default only for truly missing pre-migration fields, and make unknown strings error or map to a disabled state.

As per coding guidelines, "Fail closed for auth, approvals, trust...".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_host_api/src/role.rs` around lines 80 - 87,
`UserStatus::parse()` in `role.rs` currently maps any unrecognized persisted
string to `Active`, which can incorrectly re-enable accounts. Update the parsing
logic so only truly missing/pre-migration values keep the backward-compatible
`Active` default, while unknown strings fail closed by returning an error or
mapping to a disabled status such as `Suspended`/`Deactivated`. Use the
`UserStatus::parse` symbol and keep the known status matches (`suspended`,
`deactivated`) unchanged.

Source: Coding guidelines

Comment on lines +13 to +22
- Provide libSQL and PostgreSQL-backed scoped lifecycle installation stores for
admin-shared and user-private package records.

## Boundaries

- This crate owns storage adapters only. Product workflow orchestration remains
in `ironclaw_product_workflow`.
- Keep durable records behind the existing `IdempotencyLedger` port; do not add
product workflow call paths around that trait.
- This crate owns storage adapters only. Product workflow orchestration,
ownership rules, and effective lifecycle resolution remain in
`ironclaw_product_workflow`.
- Keep durable records behind the existing product workflow ports; do not add
product workflow call paths around those traits.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the capability-policy store in the crate contract too.

This crate now participates in capability-policy persistence as well, but the updated purpose/boundaries text still reads like it only owns idempotency + scoped-lifecycle adapters. That leaves the crate-local entrypoint stale for the next contributor.

As per coding guidelines, crates/*/{AGENTS.md,CLAUDE.md,CONTRACT.md,README.md}: Update crate-local AGENTS.md ... when behavior changes. Based on review context, this PR’s durable storage layer also adds capability-policy persistence.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_product_workflow_storage/AGENTS.md` around lines 13 - 22, The
crate contract in AGENTS.md is stale because it only describes idempotency and
scoped-lifecycle storage, but this crate now also persists capability-policy
data. Update the crate-purpose/boundaries text to mention capability-policy
persistence alongside the existing storage responsibilities, and keep the
ownership boundaries clear so only storage adapters are described. Use the
existing AGENTS.md contract section as the place to align this with the current
durable-storage behavior.

Source: Coding guidelines

Comment on lines +103 to +117
async fn upsert_delta(
&self,
tenant_id: &TenantId,
delta: CapabilityPolicyDelta,
) -> Result<(), PolicyError> {
let path = delta_leaf_path(&self.root, tenant_id, &delta.scope, &delta.capability)?;
// Upsert == replace-at-key, so `CasExpectation::Any` matches the trait's
// "replacing any existing delta at that exact key" contract; no
// read-modify-write is needed.
self.filesystem
.put(&path, entry_for_delta(&delta)?, CasExpectation::Any)
.await
.map(|_| ())
.map_err(|error| filesystem_error("upsert capability policy delta", error))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject PolicyScope::Project until reads can resolve it.

upsert_delta() / delete_delta() accept every PolicyScope, but subject_scopes() only enumerates tenant + user and scope_applies_to_subject() hard-codes project rows to false. Today a project-scoped delta can be written successfully and then never returned by deltas_for() or list_subject_deltas(). Either fail closed on Project at write time or plumb project identity through the subject/read path before accepting it. As per path instructions, "Fail loud" applies here: boundary code should not accept unsupported state and silently drop it on read-back.

Also applies to: 176-210

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_product_workflow_storage/src/capability_policy_delta.rs`
around lines 103 - 117, Reject PolicyScope::Project at write time in
CapabilityPolicyDeltaStorage so unsupported project-scoped deltas cannot be
persisted and later silently disappear from reads. Update upsert_delta() and
delete_delta() to fail closed for Project, or otherwise add full project
identity support through subject_scopes(), scope_applies_to_subject(), and the
read paths before allowing it. Use the existing CapabilityPolicyDeltaStorage
methods and PolicyScope handling to keep the boundary consistent.

Source: Path instructions

Comment on lines +540 to +544
async fn create_user_handler(
State(config): State<LocalUserAdminRouteConfig>,
Extension(caller): Extension<WebUiAuthenticatedCaller>,
Json(request): Json<CreateUserRequest>,
) -> Result<Json<CreateUserResponse>, LocalUserAdminError> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Run the admin gate before JSON body extraction.

These handlers parse Json before ensure_admin() runs, so a non-admin authenticated caller can probe body/schema parse behavior instead of consistently receiving 403. Use a FromRequestParts admin extractor before Json, matching capability_user_policy_routes.rs. As per path instructions, fail closed for auth/admin boundaries.

Also applies to: 560-565

Source: Path instructions

Comment on lines +607 to +619
#[cfg(feature = "capability-policy")]
match identity_mandate {
Some(IdentityMode::UserKeyed)
if account.ownership != ownership_for_identity(IdentityMode::UserKeyed) =>
{
tracing::debug!(
capability = %request.capability_id.as_str(),
"user-keyed capability resolved a non-user-reusable account; requiring re-auth"
);
return Err(CredentialStageError::AuthRequired);
}
Some(IdentityMode::AdminKeyed)
if account.ownership != ownership_for_identity(IdentityMode::AdminKeyed) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

UserKeyed now rejects valid extension-owned runtime credentials.

This turns IdentityMode::UserKeyed into “must be UserReusable”, but this resolver already supports ExtensionOwned user credentials as a valid runtime path. Once a capability policy resolves UserKeyed, those existing accounts will now fail with AuthRequired even though the user already authenticated.

Suggested direction
-        match identity_mandate {
-            Some(IdentityMode::UserKeyed)
-                if account.ownership != ownership_for_identity(IdentityMode::UserKeyed) =>
+        match identity_mandate {
+            Some(IdentityMode::UserKeyed)
+                if !matches!(
+                    account.ownership,
+                    CredentialOwnership::UserReusable | CredentialOwnership::ExtensionOwned
+                ) =>
             {
                 tracing::debug!(
                     capability = %request.capability_id.as_str(),
                     "user-keyed capability resolved a non-user-reusable account; requiring re-auth"
                 );
                 return Err(CredentialStageError::AuthRequired);
             }

The existing caller-level resolver tests in crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials/tests.rs already lock in ExtensionOwned reuse as valid behavior, so this policy layer is narrowing an established contract.

Also applies to: 644-655

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials.rs`
around lines 607 - 619, The `identity_mandate` check in
`resolve_runtime_credentials` is too strict for `IdentityMode::UserKeyed` and
now rejects valid `ExtensionOwned` runtime credentials by treating only
`UserReusable` as acceptable. Update the `match identity_mandate` guard so
`UserKeyed` only requires a user-owned/authorized runtime credential path
consistent with existing resolver behavior, and do not block `ExtensionOwned`
accounts that already satisfy the caller-level contract. Keep the `AdminKeyed`
branch unchanged and preserve the existing `AuthRequired` fallback only for
truly incompatible ownership cases.

Comment on lines +1313 to +1378
/// A `PolicyResolver` test double that returns a fixed identity mode for
/// every `(subject, capability)`, or a fixed error.
struct FakePolicyResolver {
outcome: Result<IdentityMode, PolicyError>,
}

impl FakePolicyResolver {
fn identity(mode: IdentityMode) -> Arc<dyn PolicyResolver> {
Arc::new(Self { outcome: Ok(mode) })
}

fn failing() -> Arc<dyn PolicyResolver> {
Arc::new(Self {
outcome: Err(PolicyError::Unavailable {
reason: "test backend down".to_string(),
}),
})
}
}

#[async_trait::async_trait]
impl PolicyResolver for FakePolicyResolver {
async fn resolve(
&self,
_subject: &PolicySubject,
_capability: &CapabilityId,
) -> Result<EffectivePolicy, PolicyError> {
match &self.outcome {
Ok(identity) => Ok(EffectivePolicy {
available: true,
identity: *identity,
approval: PermissionMode::Ask,
config: serde_json::Value::Null,
}),
Err(PolicyError::Unavailable { reason }) => Err(PolicyError::Unavailable {
reason: reason.clone(),
}),
Err(PolicyError::Internal { reason }) => Err(PolicyError::Internal {
reason: reason.clone(),
}),
}
}
}

fn resolver_with_policy(
accounts: Arc<InMemoryAuthProductServices>,
policy: Arc<dyn PolicyResolver>,
) -> ProductAuthRuntimeCredentialResolver {
resolver_with_accounts(accounts).with_policy_resolver(policy)
}

fn manual_token_request<'a>(
capability: &'a CapabilityId,
scope: &'a ResourceScope,
provider: &'a RuntimeCredentialAccountProviderId,
requester: &'a ExtensionId,
) -> RuntimeCredentialAccountRequest<'a> {
RuntimeCredentialAccountRequest {
capability_id: capability,
scope,
provider,
setup: &RuntimeCredentialAccountSetup::ManualToken,
provider_scopes: &[],
requester_extension: requester,
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert the policy lookup key, not just the mapped mode.

FakePolicyResolver ignores both PolicySubject and CapabilityId, and every new case uses the same literal capability. That leaves the new wiring untested: resolving policy for the wrong subject or wrong capability would still pass this module. Add one caller-level test that records the observed (subject, capability) and asserts it matches the RuntimeCredentialAccountRequest.

As per path instructions, "Test through the caller: when a helper gates a side effect, require a test driving the real call site (handler/factory/manager), not only the helper."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials/tests.rs`
around lines 1313 - 1378, `FakePolicyResolver` only verifies the returned
`IdentityMode`, so the new tests don’t prove the resolver is called with the
correct `PolicySubject` and `CapabilityId`. Add a caller-level test around the
real request flow in `ProductAuthRuntimeCredentialResolver` that uses a
recording policy resolver to capture the observed `(subject, capability)` from
`resolve`, then assert they match the `RuntimeCredentialAccountRequest` values.
Keep the helper, but make the new test drive the actual call site rather than
only the fake resolver mapping.

Source: Path instructions

Comment on lines +343 to +360
match self.store.resolve_token(&self.tenant_id, &token_hash).await {
Ok(Some(record)) => Some(
ironclaw_reborn_composition::WebuiAuthentication::user(record.user_id)
.with_role(record.role),
),
Ok(None) => None,
Err(error) => {
// A backend read failure is NOT a bad bearer; surface the cause
// at the auth boundary rather than letting a transient store
// outage masquerade as an authentication rejection (the request
// still fails closed — we return `None`). Per
// `.claude/rules/error-handling.md`, do not silently drop it.
tracing::warn!(
target = "ironclaw::reborn::webui_ingress",
%error,
"local user directory token resolution failed; rejecting request",
);
None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not collapse user-directory outages into authentication misses.

Err(error) => None makes a backend read failure indistinguishable from a bad bearer, so the middleware returns 401 instead of a sanitized unavailable response. Change the authenticator boundary to carry an auth error/result so store outages fail closed as 503 while unknown tokens remain 401. As per path instructions, fail loud at auth boundaries and do not weaken bearer auth semantics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_reborn_webui_ingress/src/lib.rs` around lines 343 - 360, The
token resolution path in the authenticator currently collapses store failures
into `None`, which makes `resolve_token` errors look like invalid bearer tokens.
Update the auth boundary around `resolve_token` and
`WebuiAuthentication::user(...).with_role(...)` so it can return a distinct auth
error/result for backend outages, while keeping `Ok(None)` as the only
invalid-token case. Then have the middleware map store errors to a sanitized
unavailable response (503) and unknown tokens to 401, preserving the existing
warning log in the error branch.

Source: Path instructions

Comment on lines +267 to +278

```
Tenant id
Project id, tenant_id, is_default # default == tenant (v1)
User id, tenant_id, role(Owner|Admin|Member),
kind(person|shared), auth(SSO ref), # "who can be a shared account" = IdP
memory_ref # per-user memory
Capability id, kind, default_policy # from manifest
CapabilityPolicyDelta
scope(Project|User), scope_id, capability_id,
availability?, config_patch?, identity_mode?, approval? # sparse
CapabilityCredential

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

CapabilityPolicyDelta scope omits Tenant in §10–§11.

§8 resolution correctly includes tenant_delta[capability], and the code's PolicyScope has Tenant, but §10's logical data model and §11's CapabilityPolicyStore port only list Project|User. Add Tenant to both so the port spec matches the actual store contract and resolution cascade.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/plans/2026-06-24-capability-policy-architecture.md` around lines 267 -
278, The logical data model and store contract for CapabilityPolicyDelta are
missing Tenant even though the resolution cascade and PolicyScope already
support it. Update the CapabilityPolicyDelta definition and the
CapabilityPolicyStore port spec to include Tenant alongside Project and User,
and make sure the scope_id behavior and related wording stay consistent with the
existing tenant_delta[capability] resolution path.

zetyquickly and others added 10 commits June 26, 2026 14:32
…#5266 #5261)

Foundation for the capability-policy role model:
- UserRole::rank() (Owner=2 > Admin=1 > Member=0) + outranks() (strict) in
  ironclaw_host_api — no derived Ord (variant order would invert privilege).
- TurnActor gains role (#[serde(default)] → Member for legacy/channel actors)
  + with_role(); WebUiAuthenticatedCaller::actor() now carries the caller's
  role to dispatch (it was dropped before), so the availability resolver can
  be role-aware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…#5267 #5261)

- Role-aware dispatch surface: the availability resolver reads the acting role
  (now carried on TurnActor) and, for Owner/Admin, returns the full installed +
  builtin set BYPASSING the per-capability policy intersection (admins/owner are
  capped by neither per-user nor tenant-scope hides). Members keep installed AND
  EffectivePolicy.available, fail-closed. Read-time bypass (demotion re-applies
  stored hides next turn).
- Builtin governance: the resolver now seeds builtin capability-ids (builtin.shell,
  ...) from the registry snapshot (minus installable-extension caps) into the
  base allow-set, so builtins are available-by-default AND hideable per-user. They
  were previously excluded (only installed extensions seeded), so policy-on denied
  all builtins.
+5 unit tests (admin bypass of user/tenant hides; builtin default-available;
member builtin-hide drops it; member installed∩policy holds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ser deletion (#5272 #5261)

delete_user_handler now enforces (after ensure_admin), in order: (a) self-delete
forbidden (owner & admin); (b) missing target -> 404; (c) strict rank — caller
must outrank target (admin cannot delete an owner or a peer admin; owner deletes
admins/members); (d) last-owner protected. Adds resolve_user(by user_id) to the
LocalUserDirectoryStore trait + both impls (filesystem + the ingress in-memory
fake). +caller-level test driving the full step-12 matrix through the handler.
Flips the e2e step-12 guard test from xfail to a hard assert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…5268 #5261)

The per-user capability grant route now strict-outrank-gates the TARGET: it
resolves the target user's role (resolve_user) and requires caller.role.outranks
(target) on PUT/DELETE. So admin->owner caps = 403 (step 13), admin->peer-admin =
403, admin->member ok, owner->admin/member ok, owner->owner = 403 (nobody edits
the owner's caps via this route), unknown target = 404. The route config carries
the SAME LocalUserDirectoryStore Arc serve already shares with the authenticator
and /admin/users (no second store). +7 caller-level regressions. Flips the e2e
step-13 test from xfail to a hard assert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n settings/tools (#5268 #5261)

- settings/tools/{capability_id} now accepts PUT (alongside POST for back-compat),
  with the Put descriptor registered so body/rate-limit middleware covers it.
- RebornServices gains a CapabilityAvailabilityProbe port; set_operator_config_key
  rejects (403 ParticipantDenied) an approval-pref write for a capability NOT
  available to the caller; probe error fails closed (deny). Unknown cap stays 400.
- Composition wires the probe from the shared capability_policy_resolver
  (EffectivePolicy.available for PolicySubject{tenant,user}) only under
  capability_policy_activated() — off-feature attaches no probe (unchanged).
Flips the e2e step-16 unavailable-rejection test from xfail to a hard assert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A live-instance e2e outline (setup driven manually, out of band) for the
four-dimension capability policy on org "xyzorg":
- SECTION 1 org+accounts (owner/admin via role, members)
- SECTION 2 per-user capability allow-list (enumerate settings/tools -> hide the
  rest) + bob's user-keyed product-auth secret setup
- SECTION 3 role privileges (owner > admin > member): promote-to-admin drops a
  user's limits; admins/owner get the default set; deletion + self-delete +
  single-owner guards; admin may not touch the owner's caps
- SECTION 4 dispatch enforcement (per-user tool surface in traces; user-keyed
  graceful auth gate; user-scoped approval prefs gated by availability)

Several SECTION 3 assertions intentionally encode role-model behavior that is
NOT enforced yet (rank/self/owner guards, role-aware availability) — this test
is the spec that drives those features.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#5261)

Executable body (httpx, below the 16-step comment spec) + a module-local
policy-enabled serve fixture (builds --features webui-v2-beta,capability-policy,
runs serve with IRONCLAW_REBORN_CAPABILITY_POLICY=1, tenant xyzorg, operator
bootstrap, mock LLM). Steps that work today HARD-ASSERT (org+accounts, install +
per-user hide grants, owner->admin delete, approval pref on an available cap);
role-model steps not yet built are xfail(strict=False) with the gap id, so the
suite is green now and each xfail is removed as its feature lands:
- step 12 403 guards -> D5/G1; step 13 owner-caps -> D6/G4;
- step 14 dispatch surface -> D2/D3/D4; step 16 unavailable-reject -> D7/G5;
- step 9 manual-token -> shape TBD.
Run: 4 passed, 5 xfailed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ch surface (epic #5261)

- step 9: bob's user-keyed github secret via the real manual-token flow
  (setup -> secret-submit with the round-tripped invocation_id). gdrive's
  OAuth-keyed path stays xfail (needs a real browser consent — not headless).
- step 14: drive a per-user turn that attempts a tool; assert the model's offered
  surface enforces availability — alice is offered builtin.shell (+ runs), carl
  (deny-all) is not, a fresh admin gets the full surface (D2/D3 bypass). mock_llm
  gains one TOOL_CALL_PATTERNS entry to deterministically emit the probe call.
Result: 9 passed, 1 xfailed (gdrive OAuth). No product changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5355 June 26, 2026 21:39 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/ironclaw_product_workflow/src/reborn_services.rs`:
- Around line 168-186: The CapabilityAvailabilityProbe contract is missing the
role- and install-aware context needed to match dispatch behavior, so update the
availability check used by RebornServices::set_operator_config_key to go through
the same surface resolver as dispatch or extend the probe to accept the missing
UserRole/install inputs. Make sure Owner/Admin bypass behavior and
“policy-available but not installed” exclusion both match
capability_surface_policy::resolve_allow_set_as, and add regression tests
covering valid privileged writes and rejection of prefs for capabilities that
will not appear at dispatch.

In `@crates/ironclaw_reborn_composition/src/capability_user_policy_routes.rs`:
- Around line 260-268: The comment for the rank gate overstates a cross-layer
invariant by saying the caller’s role was authenticated from the same directory
as the target. Soften the wording in capability_user_policy_routes.rs near the
rank-gate block and keep it aligned with what the code actually guarantees: the
target is resolved from the shared directory, while caller authentication may
come from another layer for operator fallback. Update the surrounding comment to
describe the intended strict-outrank check in AdminCaller /
caller.role.outranks(target_role) without promising an unenforced
directory-sharing guarantee.

In `@tests/e2e/test_reborn_capability_policy_xyzorg.py`:
- Line 661: The test fixture is hardcoding a provider-shaped GitHub token, which
should be removed from the e2e data. Update the token setup in the affected test
to source credentials from an environment variable or switch to a non-credential
test DTO path, and ensure the test data no longer contains anything resembling a
real PAT. Use the existing test case content around the token field in the
reborn capability policy test to locate the change.
- Around line 1-4: Update the stale executable-spec comments in the xyzorg
reborn capability policy test so they match the current behavior in the test
module. In the test file header and the related 725-729 comment block, remove or
rewrite the notes that say setup is manual/out of band and that the 403
deletion-guard test is xfailed, since the suite now boots and seeds the xyzorg
server itself and asserts the 403 guard directly. Use the surrounding test
functions and comments in the xyzorg policy test to keep the wording aligned
with the actual executable flow.
- Around line 418-425: The mock request tracking in _offered_tool_names is
process-global, so the current sleep-based check can accidentally read a
previous request and make the dispatch-surface assertions flaky. Update the
probe flow to attach a unique nonce to the test request content, then poll
/__mock/last_chat_request until the returned body contains that nonce before
extracting tool names. Apply the same correlation approach anywhere the helper
is reused in the affected test block so the assertions always reflect the
current request.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e1060603-5b7d-4ea3-9591-1f7a8878e52c

📥 Commits

Reviewing files that changed from the base of the PR and between 61e8ebf and 8123af5.

📒 Files selected for processing (19)
  • crates/ironclaw_host_api/src/role.rs
  • crates/ironclaw_product_workflow/src/lib.rs
  • crates/ironclaw_product_workflow/src/reborn_services.rs
  • crates/ironclaw_product_workflow/src/webui_inbound.rs
  • crates/ironclaw_reborn_cli/src/commands/serve.rs
  • crates/ironclaw_reborn_composition/src/capability_surface_policy.rs
  • crates/ironclaw_reborn_composition/src/capability_user_policy_routes.rs
  • crates/ironclaw_reborn_composition/src/local_user_directory.rs
  • crates/ironclaw_reborn_composition/src/runtime.rs
  • crates/ironclaw_reborn_composition/src/webui.rs
  • crates/ironclaw_reborn_webui_ingress/src/lib.rs
  • crates/ironclaw_turns/src/scope.rs
  • crates/ironclaw_webui_v2/src/descriptors.rs
  • crates/ironclaw_webui_v2/src/lib.rs
  • crates/ironclaw_webui_v2/src/router.rs
  • crates/ironclaw_webui_v2/tests/webui_v2_descriptors_contract.rs
  • crates/ironclaw_webui_v2/tests/webui_v2_handlers_contract.rs
  • tests/e2e/mock_llm.py
  • tests/e2e/test_reborn_capability_policy_xyzorg.py

Comment on lines +168 to +186
/// Port that answers "is this capability available to this caller's
/// `(tenant, user)` surface?" so the per-tool approval-preference write
/// (`tool.<capability_id>` via [`RebornServices::set_operator_config_key`])
/// can refuse to persist a preference for a capability the caller cannot
/// actually see at dispatch (#5261 D7 / G5).
///
/// The trait is defined in `ironclaw_host_api` vocabulary only
/// ([`ResourceScope`], [`CapabilityId`], [`RebornServicesError`]) so it stays
/// inside this crate's dependency boundary — the policy-resolver-backed
/// implementation lives in `ironclaw_reborn_composition`, which owns the
/// `ironclaw_capability_policy` dependency.
#[async_trait]
pub trait CapabilityAvailabilityProbe: Send + Sync {
async fn is_available(
&self,
scope: &ResourceScope,
capability_id: &CapabilityId,
) -> Result<bool, RebornServicesError>;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Capability-availability port is missing dispatch-critical context.

This port only gets ResourceScope, but the real allow-set here is role-aware and install-aware. capability_surface_policy::resolve_allow_set_as(..., UserRole) lets Owner/Admin bypass hide deltas, and the dispatch surface also excludes policy-available capabilities that are not installed. With the current signature, set_operator_config_key can deny valid Owner/Admin writes and still accept prefs for capabilities the caller will never actually see at dispatch. Please route this through the same surface resolver as dispatch, or extend the contract to carry the missing role/install context, then add regression coverage for both cases. Repo invariant: cross-layer guarantees in comments must be enforced by code/tests or softened. As per coding guidelines, "Comments that promise guarantees across layers must either be enforced by code/tests or softened to describe intent."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_product_workflow/src/reborn_services.rs` around lines 168 -
186, The CapabilityAvailabilityProbe contract is missing the role- and
install-aware context needed to match dispatch behavior, so update the
availability check used by RebornServices::set_operator_config_key to go through
the same surface resolver as dispatch or extend the probe to accept the missing
UserRole/install inputs. Make sure Owner/Admin bypass behavior and
“policy-available but not installed” exclusion both match
capability_surface_policy::resolve_allow_set_as, and add regression tests
covering valid privileged writes and rejection of prefs for capabilities that
will not appear at dispatch.

Source: Coding guidelines

Comment on lines +260 to +268
/// Rank gate for the per-user capability WRITE surface (admin-rest-4): an admin
/// may edit a subordinate's per-user caps, but **not** an owner's nor a peer
/// admin's, and the owner himself may not edit the owner's caps through this
/// route. The caller's `is_admin()` gate already ran in [`AdminCaller`]; this is
/// the ADDITIONAL strict-outrank check layered on top.
///
/// Resolves the TARGET's current role from the SAME directory the caller's role
/// was authenticated from (so the comparison cannot desync), then requires
/// `caller.role.outranks(target_role)`:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Soften the cross-layer guarantee in this comment.

The rank gate resolves the target from the shared directory, but operator fallback callers are authenticated outside that directory, so “the caller’s role was authenticated from” overpromises the invariant.

As per coding guidelines, “Comments that promise guarantees across layers must either be enforced by code/tests or softened to describe intent.”

Suggested wording
-/// Resolves the TARGET's current role from the SAME directory the caller's role
-/// was authenticated from (so the comparison cannot desync), then requires
+/// Resolves the TARGET's current role from the shared local-user directory used
+/// by local-user authentication and `/admin/users`. The caller role is trusted
+/// from `WebUiAuthenticatedCaller` (which may also come from the operator
+/// authenticator), then requires
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Rank gate for the per-user capability WRITE surface (admin-rest-4): an admin
/// may edit a subordinate's per-user caps, but **not** an owner's nor a peer
/// admin's, and the owner himself may not edit the owner's caps through this
/// route. The caller's `is_admin()` gate already ran in [`AdminCaller`]; this is
/// the ADDITIONAL strict-outrank check layered on top.
///
/// Resolves the TARGET's current role from the SAME directory the caller's role
/// was authenticated from (so the comparison cannot desync), then requires
/// `caller.role.outranks(target_role)`:
/// Rank gate for the per-user capability WRITE surface (admin-rest-4): an admin
/// may edit a subordinate's per-user caps, but **not** an owner's nor a peer
/// admin's, and the owner himself may not edit the owner's caps through this
/// route. The caller's `is_admin()` gate already ran in [`AdminCaller`]; this is
/// the ADDITIONAL strict-outrank check layered on top.
///
/// Resolves the TARGET's current role from the shared local-user directory used
/// by local-user authentication and `/admin/users`. The caller role is trusted
/// from `WebUiAuthenticatedCaller` (which may also come from the operator
/// authenticator), then requires `caller.role.outranks(target_role)`:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_reborn_composition/src/capability_user_policy_routes.rs`
around lines 260 - 268, The comment for the rank gate overstates a cross-layer
invariant by saying the caller’s role was authenticated from the same directory
as the target. Soften the wording in capability_user_policy_routes.rs near the
rank-gate block and keep it aligned with what the code actually guarantees: the
target is resolved from the shared directory, while caller authentication may
come from another layer for operator fallback. Update the surrounding comment to
describe the intended strict-outrank check in AdminCaller /
caller.role.outranks(target_role) without promising an unenforced
directory-sharing guarantee.

Source: Coding guidelines

Comment on lines +1 to +4
# this is a test file to test the capability of live ironclaw, org "xyzorg"
#
# (setup is done manually, out of band — boot serve, set the xyzorg tenant,
# install the capabilities, bootstrap the operator/director. not part of this file.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update stale executable-spec comments.

The file now boots and seeds the xyzorg server itself, and the 403 deletion-guard test is hard-asserted, not xfailed. These comments contradict the executable behavior.

Also applies to: 725-729

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/test_reborn_capability_policy_xyzorg.py` around lines 1 - 4, Update
the stale executable-spec comments in the xyzorg reborn capability policy test
so they match the current behavior in the test module. In the test file header
and the related 725-729 comment block, remove or rewrite the notes that say
setup is manual/out of band and that the 403 deletion-guard test is xfailed,
since the suite now boots and seeds the xyzorg server itself and asserts the 403
guard directly. Use the surrounding test functions and comments in the xyzorg
policy test to keep the wording aligned with the actual executable flow.

Comment on lines +418 to +425
async def _offered_tool_names(client, mock_llm_server):
"""Provider tool names the mock LLM was last offered (the model's dispatch
surface). Reads /__mock/last_chat_request — the most recent
/v1/chat/completions body the mock saw — and projects out tool names."""
resp = await client.get(f"{mock_llm_server}/__mock/last_chat_request", timeout=15)
resp.raise_for_status()
tools = resp.json().get("tools", []) or []
return {t.get("function", {}).get("name") for t in tools}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Correlate the mock LLM request instead of sleeping.

/__mock/last_chat_request is process-global; the fixed sleep can read the previous user’s request and make the dispatch-surface assertions flaky. Add a unique nonce to the probe content and poll until the mock’s last request contains that nonce.

Suggested direction
-async def _offered_tool_names(client, mock_llm_server):
+async def _offered_tool_names(client, mock_llm_server, expected_nonce: str):
+    deadline = asyncio.get_running_loop().time() + 10
+    last_tools = set()
+    while asyncio.get_running_loop().time() < deadline:
+        resp = await client.get(f"{mock_llm_server}/__mock/last_chat_request", timeout=15)
+        resp.raise_for_status()
+        body = resp.json()
+        messages = body.get("messages", []) or []
+        tools = body.get("tools", []) or []
+        last_tools = {t.get("function", {}).get("name") for t in tools}
+        if any(expected_nonce in str(m.get("content", "")) for m in messages):
+            return last_tools
+        await asyncio.sleep(0.25)
+    raise AssertionError(f"mock LLM did not observe probe nonce {expected_nonce}; last_tools={last_tools}")

 async def _drive_shell_probe(client, base_url, mock_llm_server, token):
+    nonce = str(uuid.uuid4())
+    content = f"{SHELL_PROBE_PROMPT} [{nonce}]"
     thread_id = await _create_thread(client, base_url, token)
     send = await client.post(
         f"{base_url}/api/webchat/v2/threads/{thread_id}/messages",
         headers=_bearer(token),
-        json={"client_action_id": str(uuid.uuid4()), "content": SHELL_PROBE_PROMPT},
+        json={"client_action_id": str(uuid.uuid4()), "content": content},
         timeout=30,
     )
     assert send.status_code in (200, 202), f"send: {send.status_code} {send.text}"
-    await asyncio.sleep(1.5)
-    offered = await _offered_tool_names(client, mock_llm_server)
+    offered = await _offered_tool_names(client, mock_llm_server, nonce)

Also applies to: 852-856

🧰 Tools
🪛 Ruff (0.15.18)

[warning] 418-418: Missing return type annotation for private function _offered_tool_names

(ANN202)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/test_reborn_capability_policy_xyzorg.py` around lines 418 - 425,
The mock request tracking in _offered_tool_names is process-global, so the
current sleep-based check can accidentally read a previous request and make the
dispatch-surface assertions flaky. Update the probe flow to attach a unique
nonce to the test request content, then poll /__mock/last_chat_request until the
returned body contains that nonce before extracting tool names. Apply the same
correlation approach anywhere the helper is reused in the affected test block so
the assertions always reflect the current request.

json={
"interaction_id": interaction_id,
"invocation_id": invocation_id,
"token": "ghp_bob_personal_access_token_0123456789",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not commit provider-shaped tokens.

ghp_... trips secret scanners and violates the e2e rule that credentials come from env. Use an env-provided test token or a non-credential DTO path that does not resemble a real GitHub PAT. As per path instructions, tests/e2e/**: “credentials come from env, never hardcoded.”

🧰 Tools
🪛 OpenGrep (1.23.0)

[WARNING] 661-661: Hardcoded GitHub token detected. Use environment variables or a secrets manager instead.

(coderabbit.secrets.github-token)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/test_reborn_capability_policy_xyzorg.py` at line 661, The test
fixture is hardcoding a provider-shaped GitHub token, which should be removed
from the e2e data. Update the token setup in the affected test to source
credentials from an environment variable or switch to a non-credential test DTO
path, and ensure the test data no longer contains anything resembling a real
PAT. Use the existing test case content around the token field in the reborn
capability policy test to locate the change.

Sources: Path instructions, Linters/SAST tools

@rpelevin

Copy link
Copy Markdown

I would make the top control-plane PR prove one more thing: that the branch-level milestone claim and the runtime authority boundary are the same claim.

The PR says the top tree is byte-equal to the tested integration branch and adds REST-created users plus per-user grants for availability, identity, approval, and config. That is the right place to put the acceptance gate, but I would keep the proof centered on the authority path, not just on compilation.

A useful acceptance shape:

  1. resolve the merge conflict, then re-run the byte-equality check against the tested integration tree;
  2. create owner, admin, member, and shared account through the REST user surface;
  3. issue grants through the per-user capability policy route for all four dimensions;
  4. restart and prove the persisted delta store, scoped lifecycle store, and user directory produce the same effective policy;
  5. attempt the same dispatch under a non-subordinate admin and assert the grant is rejected before any resolver state is consumed;
  6. attempt a disabled capability with permissive user approval and assert availability remains a hard precondition;
  7. record the effective policy explanation that the dispatch path consumed, including which layer supplied each dimension.

That would keep the control plane from becoming a parallel authority source. REST grants are inputs; the executable authority is the effective policy consumed at dispatch for the same tenant, user, capability, session, and role.

I would also keep the template warning from being cosmetic. For this PR, security impact, database impact, blast radius, rollback, and review follow-through are the review contract, because this is where admin intent becomes durable execution policy.

Boundary: architecture and regression-test feedback only; no claim about using this project, running this branch, validating implementation behavior, implementation correctness, merge readiness, security review, production readiness, partnership, customer interest, official alignment, NearAI usage, Ironclaw usage, conformance certification, or Neura usage.

@zetyquickly

Copy link
Copy Markdown
Contributor Author

ai slop

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-5355 — 8123af54 Deployed Jun 26, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: medium Business logic, config, or moderate-risk modules scope: dependencies Dependency updates scope: docs Documentation size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants