feat(governance): wire Skills + MCP into roles + Cat-A A-017/A-018 (Phase 4.3) - #7
Merged
Merged
Conversation
… A-017/A-018 (Phase 4.3)
Connects PR 4.1 (acc/skills) and PR 4.2 (acc/mcp) to the cell:
RoleDefinitionConfig grows the capability whitelist, CognitiveCore
gains explicit invoke_skill / invoke_mcp_tool entry points, and a new
Python CapabilityGuard enforces Cat-A A-017 (skills) and A-018 (MCP
tools) at decision time.
acc/config.py
RoleDefinitionConfig:
+ allowed_skills: whitelist (fail-closed; empty = no skills)
+ default_skills: subset advertised in system prompt
+ max_skill_risk_level: ceiling LOW < MEDIUM < HIGH < CRITICAL
+ allowed_mcps: whitelist (fail-closed)
+ default_mcps: subset advertised in system prompt
+ max_mcp_risk_level: ceiling enforced by A-018
Inverse default vs allowed_actions: empty = deny-all on the new
surface (skills + MCPs are explicit opt-in), where empty
allowed_actions remains unconstrained for back-compat.
acc/governance_capabilities.py (new)
CapabilityGuard — stateless Python evaluator with the same
enforce/observe contract as acc.governance.CatAEvaluator. Rules
live here (not in OPA) because skill/MCP invocations are
decision-level events inside one agent's process_task; a
subprocess hop per invocation would dwarf the actual check.
check_skill_invocation (A-017): three checks composed —
1. skill_id ∈ role.allowed_skills (fail-closed)
2. manifest.requires_actions ⊆ allowed_actions
3. manifest.risk_level ≤ max_skill_risk_level
check_mcp_invocation (A-018): same three plus —
4. manifest.is_tool_allowed(tool_name)
CRITICAL hits set decision.needs_oversight=True so the caller can
enqueue an OVERSIGHT_SUBMIT alongside the invocation
(EU AI Act Art. 14).
acc/cognitive_core.py
CognitiveCore.__init__:
+ skill_registry: Optional[SkillRegistry]
+ mcp_registry: Optional[MCPRegistry]
+ self._capability_guard: CapabilityGuard(enforce=cat_a_enforce)
+ invoke_skill(skill_id, args, role) — guarded entry point
+ invoke_mcp_tool(server_id, tool, args, role) — guarded entry point
build_system_prompt(): when role.default_skills / default_mcps is
non-empty AND every entry is in the corresponding allowed_* list,
appends an "Available skills" / "Available MCP servers" block listing
id + manifest.purpose so the LLM knows the surface is callable.
Legacy roles with no capability wiring see exactly their previous
prompt (no extra block).
acc/agent.py
+ self._skill_registry = self._build_skill_registry() (always built)
+ self._mcp_registry = self._build_mcp_registry() (always built)
Both pass-through into CognitiveCore. Errors during discovery are
logged and the registry stays empty — one bad manifest must not stop
the agent from booting. Always-built so role hot-reload can toggle
allowed_skills [] → non-empty without restart.
regulatory_layer/category_a/constitutional_rhoai.rego
+ A-017 deny_skill_not_whitelisted (declarative documentation)
+ A-018 deny_mcp_not_whitelisted (declarative documentation)
Header note pins the Python guard as the source of truth for runtime
enforcement; Rego rules exist for audit visibility and future
Gatekeeper integration on RHOAI.
roles/_base/role.yaml
Adds the six new fields with documented defaults — fail-closed
whitelists ([]), MEDIUM risk ceilings.
roles/coding_agent/role.yaml
Demonstrator wiring: allowed_skills=[echo], allowed_mcps=[echo_server],
default_skills/default_mcps mirror the allowed lists, MEDIUM ceilings.
This is the role most likely to test the wiring end-to-end through
the existing coding_split compose profile.
Smoke tests (run locally — all green):
* RoleDefinitionConfig defaults validate, Literal types reject typos.
* Both registries load (1 skill, 1 MCP server).
* CapabilityGuard enforce-mode: empty whitelist denies, risk ceiling
denies, manifest tool gate denies, allowed path passes.
* CapabilityGuard observe-mode: every "would block" is tagged
reason='observed:...' and allowed=True.
* CognitiveCore.invoke_skill enforce-mode: A-017 raises
SkillForbiddenError, increments cat_a_trigger_count, fires
ALERT_ESCALATE.
* CognitiveCore.invoke_skill happy path: returns adapter dict.
* build_system_prompt: 'Available skills' + 'Available MCP servers'
blocks render only when default_* is non-empty AND aligned with
allowed_*. Legacy roles unchanged.
Existing test suite: 145 unit tests across config / role-store /
guardrails / compliance pass unchanged. TUI test failures (9) are
pre-existing on main with this branch's changes stashed — unrelated.
Unblocks PR 4.4 (TUI Ecosystem live tables + how-to docs); the LLM
output parser that maps [ACTION: skill_id ...] markers to invoke_skill
calls is also a 4.4 deliverable so this PR ships the enforcement
surface without prematurely binding the parser shape.
4 tasks
flg77
added a commit
that referenced
this pull request
Apr 30, 2026
…hedule infusion + bare excepts (PR-A) (#10) User report (notes 30-Apr-2026 + screenshots): on a freshly launched TUI the Ecosystem screen lists 30+ roles correctly, but every operator action below the list is dead. Audit traced four distinct bugs to one shared cause + one antipattern: 1. Skills / MCP SERVERS tables show "no skills loaded — see howto-..." guidance row even though skills/echo and mcps/echo_server exist on disk. 2. ROLE DETAIL panel never updates when a role row is highlighted or clicked — stays stuck on "Select a role row to view its full definition". 3. "Schedule infusion → Nucleus" button hint pinned at "Select a role first" forever; clicking the button does nothing. 4. Bare `except Exception: pass` blocks inside the row-select handler swallow every failure silently, so the symptoms above produce no log line operators could investigate from. Root causes: * `_roles_root() / _skills_root() / _mcps_root()` returned bare relative strings (`"roles"` etc.). When the TUI launches outside the repo root (pip install entry point, container with WORKDIR=/app), those resolve against os.getcwd(). Some manifest dirs hit and others miss depending on cwd at process start — exactly the partial-resolution mismatch the screenshot shows. * The row-select handler set `_selected_role` AFTER calling `_show_role_detail()`, so any failure inside the detail render silently dropped the selection state. Combined with bare excepts, the operator saw no message and the Schedule-infusion button stayed disabled. Fixes (acc/tui/path_resolution.py — NEW): resolve_manifest_root(env_var, default_dir_name) → absolute Path Resolution order: 1. ACC_*_ROOT env var (absolute or expanded against cwd) IF the path exists; warns + falls through if env var points to a missing dir (no silent use of bad config). 2. Repo-anchored: <repo>/<default_dir_name> computed from this module's filesystem location (Path(__file__).parent.parent.parent). Works in editable installs and container layouts. 3. CWD-relative literal as last-resort fallback (preserves test harness behaviour). Fixes (acc/tui/screens/ecosystem.py): * `_roles_root` / `_skills_root` / `_mcps_root` delegate to the new helper; return type widens str → Path (loaders accept both). * `on_data_table_row_highlighted` (NEW handler): cursor movement over a role row now populates ROLE DETAIL live, matching the UX every spreadsheet-style table provides. Pre-PR-A the operator had to know to press Enter; now scrolling does the right thing. * `on_data_table_row_selected` rewritten: sets `_selected_role` BEFORE calling `_show_role_detail()` so a render failure cannot leave the Schedule-infusion button disabled. Uses the new shared `_arm_infusion_button()` helper. * `_extract_role_name` static helper: pulls the role name string out of a Textual RowKey across the API surface variations different Textual versions present. * Every bare `except Exception: pass` replaced with `logger.exception(...)` so future regressions surface in the rotating TUI log file. * `on_button_pressed` calls `self.notify("Highlight or click a role row first", severity="warning")` when `_selected_role` is empty — the dead-click is now visible toast feedback rather than silent. Tests (tests/test_ecosystem_screen_pilot.py — NEW, 10 cases all green): Path-resolution (3): env-var wins / repo anchor falls back / missing env path warned-and-skipped (NOT silently used). Pilot-driven (7): skills_table_populated_when_manifests_exist — fixture sets ACC_SKILLS_ROOT to tmp dir with one echo manifest, table shows a real row not the empty-state guidance row. mcps_table_populated_when_manifests_exist — same shape for MCPs. row_selected_handler_directly — synthetic RowSelected event, detail panel update captured via monkeypatched Static.update. row_selection_arms_infusion_button — _selected_role + button state both correct after handler dispatch. row_highlighted_handler_directly — cursor-driven path covered too. button_press_with_selection_dispatches_role_preload — happy path → RolePreloadMessage reaches the App. button_press_without_selection_notifies — sad path → notify call captured, no RolePreloadMessage dispatched. Why synthetic events instead of pilot.press("enter"): Textual's DataTable RowSelected dispatch depends on the widget being the precise focus target at the moment of dispatch, which Pilot's harness can't reliably reproduce in test mode (we tried both pilot.press and action_select_cursor, both failed deterministically). Constructing DataTable.RowSelected / RowHighlighted directly and dispatching to the screen's handler exercises exactly the same code path Textual would invoke on a real keypress, with no flakiness. Existing suite: 139 unit tests across config / role-store / guardrails / compliance pass unchanged (TestEd25519Validation deselected for the lighthouse OpenSSL platform limit, as in PRs #7-#9). Out of scope (planned for follow-ups per the approved plan): * PR-A2 — in-TUI manifest upload (FilePicker modal). * PR-B — direct prompt pane + open PromptChannel Protocol. Notes file: C:\Users\micro\Documents\Notes\Notes\Development\AgenticCellCorpus\ACC TUI\TUI Fixes.md
flg77
added a commit
that referenced
this pull request
Apr 30, 2026
…ss operator input (#13) First non-TUI implementation of the PromptChannel Protocol from PR-B. Operators @-mention an ACC bot in Slack; the daemon dispatches the message as TASK_ASSIGN, awaits the agent's TASK_COMPLETE, and posts the reply back to the same Slack thread. acc/channels/slack.py (NEW) SlackPromptChannel — owns its own nats.aio.client connection (the TUI's NATSObserver is view-coupled and can't be reused from a daemon process). Wire format mirrors NATSObserver.publish so agents on the bus can't tell which channel a TASK_ASSIGN came from. .connect() — open NATS conn + subscribe to acc.{cid}.task .send(prompt, *, target_role, target_agent_id=None) -> task_id .receive(task_id, timeout) -> PromptResponse .supports_streaming() -> False .close() — cancel inflight + drain NATS Subscription callback fans only TASK_COMPLETE into the per-task_id Future registry; TASK_ASSIGN echoes + everything else is silently dropped. Single registry (_inflight) — receive() is the sole eviction point, so set_result + lookup race-free. SlackDaemon — Slack-side glue. Uses slack_bolt's AsyncApp + AsyncSocketModeHandler so the bot works behind firewalls (no public webhook). Imports slack_bolt LAZILY inside .run() so the rest of the package stays light when the operator doesn't run Slack. Bot @-mention → strip <@U...> tokens via _MENTION_RE, optional "role=<name>" prefix via _ROLE_PREFIX_RE, channel.send → dispatched-notice say(), channel.receive → final reply say() in the SAME Slack thread (uses event.thread_ts when present, falls back to event.ts so top-level @-mentions start a new thread). Empty prompts / role= without body produce⚠️ notices. Send / receive failures + timeouts produce ❌ /⚠️ notices that include task_id[:8] for cross-referencing audit logs. main() entry point — env-driven config (SLACK_BOT_TOKEN, SLACK_APP_TOKEN, ACC_NATS_URL, ACC_COLLECTIVE_ID, ACC_DEFAULT_TARGET_ROLE, ACC_SLACK_TIMEOUT_S). acc/channels/__init__.py + re-exports SlackPromptChannel + SlackDaemon so `from acc.channels import SlackPromptChannel` works without knowing the submodule path. pyproject.toml + [project.optional-dependencies] slack = [slack_bolt, aiohttp] (operator runs `pip install 'acc[slack]'` to enable the daemon) + [project.scripts] acc-channel-slack = "acc.channels.slack:main" docs/howto-slack-channel.md (NEW) 9-section operator guide: 1. Prerequisites 2. Slack app setup (app + bot scopes + Socket Mode + event subscriptions) 3. Install 4. Run 5. Use it (default routing + role=X prefix + threading) 6. Wire-protocol notes 7. Troubleshooting matrix 8. Out of scope (streaming, agent=X routing, slash commands) 9. See-also cross-links to acc/channels/base.py + acc/channels/tui.py Tests (tests/test_slack_channel.py — NEW, 20 cases all green): Channel-side (9): connect_subscribes_to_task_subject send_publishes_canonical_task_assign send_omits_target_agent_id_when_none receive_correlates_by_task_id receive_timeout_drops_listener close_cancels_inflight_and_drains on_message_ignores_non_task_complete supports_streaming_returns_false payload_to_response_handles_missing_fields Regex helpers (3): mention_regex_strips_user_id_tokens role_prefix_regex_captures_role role_prefix_regex_no_match_when_absent Daemon dispatch (8): app_mention_strips_bot_token_and_dispatches app_mention_role_prefix_overrides_default app_mention_empty_prompt_warns app_mention_role_prefix_without_body_warns app_mention_timeout_posts_warning app_mention_blocked_reply_renders_with_block_marker app_mention_uses_thread_ts_when_replying_in_existing_thread app_mention_send_failure_posts_error Mocking strategy: nats.aio.client is mocked via monkeypatch on the sys.modules entry so the channel's own `import nats` succeeds against a fake. slack_bolt is NOT imported by the daemon's _on_app_mention path — that's why the daemon tests run without slack_bolt installed (only `.run()` would need it, and we don't exercise that in unit tests; manual testing per docs/howto-slack-channel.md covers it). Combined regression footprint: 204 tests pass (20 new Slack + 184 existing PR-A/A2/B + PR #9 + core). TestEd25519Validation deselected for the lighthouse OpenSSL platform limit, as in PRs #7-#12. Out of scope (separate small follow-up PRs): * TASK_PROGRESS streaming into Slack (channel.supports_streaming() flips True once the protocol stabilises). * Per-agent routing via "agent=<id>" directive. * Slash commands (/acc <prompt>) — currently mention-only. * Container packaging (operator runs the daemon as a Python process for now; production image follow-up later). * TelegramPromptChannel + WhatsAppPromptChannel — same Protocol, different bot SDKs. Notes file: C:\Users\micro\Documents\Notes\Notes\Development\AgenticCellCorpus\ACC TUI\TUI Fixes.md
flg77
added a commit
that referenced
this pull request
May 31, 2026
…rue (v0.3.41) (#7) Continuation of followup #51. v0.3.40 enabled the reflection LOOP (`ACC_REFLECTION_INTERVAL_S` default 600 in baseline compose + INFO log on boot). But `_run_reflection_once` is ALSO gated on the per-role `memory_reflection` flag, and: - `RoleDefinitionConfig.memory_reflection: bool = False` (default) - Zero role.yaml files flip it on (`grep -rn memory_reflection roles/` returns nothing) So even with the loop firing every 600s, no role ever actually consolidated. Two-layer silent gate. Verified live on lighthouse 2026-05-31 post-v0.3.40: ACC_REFLECTION_INTERVAL_S=60, boot log shows "memory_reflection: enabled interval=60s role=assistant"; sent a smoke task → episodes rose from 5 to 6 → waited 70s for the cycle → memory_notes stayed 0. Reflection loop firing, but `_run_reflection_once` early-returns. Fix: flip the default from False to True. Roles that genuinely don't want reflection (arbiter is a plausible candidate — pure cluster control, doesn't reason on tasks) can opt out per role.yaml with `memory_reflection: false`. Cost: one extra LLM call per reflection_interval_s window per active role. With the 600s default that's ~6 LLM calls/hour per agent — well within the budget every existing role already consumes for tasks. Test update: `test_role_memory_reflection_defaults_false` → `test_role_memory_reflection_defaults_true` with opt-out assertion. Full sweep: 2417 passed, 38 skipped, 0 failed. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
flg77
added a commit
that referenced
this pull request
Jun 29, 2026
…cryptography (#141) Resolves the Python (pip) Dependabot alerts on flg77/acc-spearhead. Most were a stale uv.lock (last generated 2026-06-14) whose pins lagged the already-permissive pyproject constraints; one needed a constraint widening. Lockfile refresh (constraints already allowed the fix — `uv lock --upgrade-package`): aiohttp 3.14.0 → 3.14.1 (#15-22, 8 alerts) pyjwt 2.12.1 → 2.13.0 (#9-13, 5 alerts) starlette 1.0.1 → 1.3.1 (#24-27, 4 alerts; transitive via fastapi) msgpack 1.1.2 → 1.2.1 (#37, high) joserfc 1.6.5 → 1.7.2 (#39; transitive via authlib) Constraint widening (fix was outside the pin): cryptography 46.0.7 → 48.0.1 (#14/#23, two HIGH) — pyproject `<47`→`>=48.0.1,<49`. The Ed25519 arbiter sign/verify API is stable across 46→48; operator "pin and bump" one major at a time. The lock also catches up on the speech/turbovec optional-deps added since 06-14 (faster-whisper/piper-tts/ctranslate2/onnxruntime/av/turbovec) — no security content, just lock/pyproject reconciliation. No downgrades. Not fixed here (separate handling): transformers (#1) — vulnerable `Trainer` not in our execution path; ST<5.0 blocks the 5.0.0rc3 fix → dismissed on GitHub with that reason (intent already in pyproject). torch (#7, low) — no patched release exists → dismissed "no fix available". npm console-plugin (7 alerts) — need npm/node (absent here) → fleet hand-off. Verified with the bumped libs installed: signatures/spiffe 153 ✓, a2a/messenger/ slack/webgui/redis 168 ✓ (321 total, 0 failures). Co-authored-by: flg <flg@acc1.ic3net.internal> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
flg77
added a commit
that referenced
this pull request
Aug 23, 2026
…acks The ACC Assistant now knows the @acc/supply-chain-roles pack and the guardrails it must honor and surface when infusing/using it — so packaging work stays governed end-to-end. seed_context additions: - list @acc/supply-chain-roles (packaging_engineer — RPM/DEB hardened, signed, attested builds) among the canonical first-party packs - a general "rules of engagement — governance-sensitive packs" rule: before PROPOSE_INFUSE of such a pack, read its manifest rules of engagement (catalog_query / acc-pkg inspect) and state them back to the operator. The supply-chain pack is the reference case: builds in STAGING and only RECOMMENDS promotion (promotion to trusted/prod is CRITICAL, human-in-the-loop, never autonomous); signing keys referenced never placed in the workspace; hardening fail-closed; unjustified HIGH/CRITICAL CVE HOLDs the package. Propose the infuse + staging build, but route the promote decision to the operator. Pack is live in the ecosystem catalog (acc-ecosystem v1.4.0, signed keypair + keyless-OIDC); HowTo handed to acc-web (PR #55). Role source PR #7, tiers PR #8. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Connects PR 4.1 (`acc/skills`) and PR 4.2 (`acc/mcp`) to the cell. After this PR, a role can declare which skills and which MCP servers it may reach, and the membrane (`CapabilityGuard`) enforces those whitelists every time `CognitiveCore` dispatches.
Surface added
`RoleDefinitionConfig` — six new fields
Note the inverse default vs `allowed_actions`: empty = deny-all on the new surface (skills + MCPs are explicit opt-in), where empty `allowed_actions` remains unconstrained for back-compat.
`acc/governance_capabilities.py` — `CapabilityGuard`
Stateless Python evaluator mirroring `CatAEvaluator`'s enforce/observe contract. Lives in Python (not OPA) because skill / MCP invocations are decision-level events inside one agent's `process_task` — an OPA subprocess hop per invocation would dwarf the actual check.
`CognitiveCore` — registry kwargs + new entry points
`acc/agent.py` — registries plumbed
`_build_skill_registry()` + `_build_mcp_registry()` always run at agent startup (so role hot-reload can toggle whitelists from `[]` → non-empty without restart). Errors during discovery are logged; the registry stays usable but empty.
Roles
Rego declarative parity
`regulatory_layer/category_a/constitutional_rhoai.rego` gains `deny_skill_not_whitelisted` (A-017) and `deny_mcp_not_whitelisted` (A-018) for audit visibility and future RHOAI Gatekeeper use. The header note pins the Python guard as the source of truth for runtime enforcement.
Smoke tests (all green locally)
Existing test suite: 145 unit tests across `test_config` / `test_role_store` / `test_guardrails` / `test_compliance` pass unchanged. TUI test failures observed on this branch (9) are pre-existing on main with this branch's changes stashed — unrelated.
Test plan
Out of scope