Repository navigation
fix(control-plane): fail closed and honor inputs across kernel/mute-agent/governance - #3263
Conversation
…gent/governance Three agent_control_plane components silently failed to govern. Make each enforce what it advertises and fail closed on missing/empty/erroring state, and fix two metric/limit correctness bugs in the same files. kernel_space.py - Fail closed when no policy engine: route every syscall through _check_policy (removed the `if self._policy_engine` guard) so an unconfigured kernel denies instead of allowing. Add an explicit opt-in `permissive` flag (default False) to KernelSpace and create_kernel to restore legacy allow-all when intended. - A missing-engine denial returns a clean SyscallResult (no policy_violation signal, so it can't escalate to SIGKILL/AgentKernelPanic) and is not counted as an agent policy violation. - Collapse double-counting of both policy_checks and policy_violations to a single owner (_check_policy). - Mirror fail-closed in the SYS_CHECKPOLICY advisory; when an engine is present the query stays gated, when absent its handler reports allowed=False. - KernelMetrics.to_dict now serializes the agent_crashes count, not the builtin int type object. mute_agent.py - Normalize every request (object or dict) into a shape-independent view and run capability validators against it, so validation no longer depends on request shape. Coerce string action_type to ActionType; missing/unknown action_type fails closed in both strict and non-strict modes. Validators are invoked through a helper that fails closed on any exception. - Honor the strict_mode flag: when no capability matches, strict rejects and non-strict allows well-formed out-of-capability actions. - validate_action now runs the matching capability validators on its parameters instead of ignoring them. governance_layer.py - check_alignment fails closed when an alignment validator raises: it records a violation (type=validator_error) and reports aligned=False. - get_audit_log(0) returns zero entries (guarding the [-0:] whole-list trap); None returns the full log; a negative limit raises ValueError. Tests - New tests/test_control_plane_fail_closed.py covers all eight defects; each documented repro flips from broken to correct. - Update the permissive-mode kernel tests to opt in via permissive=True. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Liam Crumm <liamcrumm@gmail.com>
PR Review Summary
Verdict: AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims. |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
…dress review Follow-up to the deep review of the fail-closed change. - Kernel fail-closed is now scoped. A no-engine, non-permissive KernelSpace denies syscalls that reach outside the agent's own sandbox (SYS_EXEC, IPC, signal delivery, spawn) via an explicit _SELF_SCOPED_SYSCALLS allowlist, and keeps self-scoped syscalls (SYS_EXIT and own-VFS SYS_READ/WRITE/OPEN/CLOSE/ STAT) allowed. This fixes the SYS_EXIT agent leak and the user_space_execution docstring SIGKILL surfaced by the review, while keeping SYS_EXEC denied. The allowlist is deny-by-default, so unknown/future syscalls still fail closed. - Reverted the test_layer3_framework.py permissive edit: its no-engine KernelSpace() + ctx.write/read example works again under the scoped behavior. - Added a regression test asserting self-scoped syscalls are allowed and SYS_EXEC denied without an engine, and that SYS_EXIT removes the agent. - Recorded the public default flip and new permissive parameter in BREAKING_CHANGES.md next to the analogous policy-engine default-deny entry. - Added the syscall terms checkpolicy/getpolicy to the cspell dictionary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Liam Crumm <liamcrumm@gmail.com>
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
Reviewed the full diff and the new test_control_plane_fail_closed.py suite (one test per defect). All eight fixes are correct and the critical one genuinely closes a fail-open hole; approving.
- kernel_space.py (#1): the core fix. syscall() now always routes through _check_policy, and _check_policy fails closed when self._policy_engine is None -- denying anything not in _SELF_SCOPED_SYSCALLS (SYS_EXEC, IPC, signal, spawn) while keeping SYS_EXIT and own-VFS ops allowed. Because the allowlist is an explicit frozenset, any new/unknown SyscallType denies by default, which is the right posture. The infra denial is correctly classified as NOT a policy violation (no policy_violations increment, no violation signal -- gated on self._policy_engine and dispatcher) and returns a clean SyscallResult(error_code=-2) rather than raising a panic. permissive=True restores legacy allow-all as an auditable opt-in, and the existing integration/layer3 tests were updated to pass permissive=True where they exercised the no-engine plumbing. The SYS_CHECKPOLICY skip_outer_check so its handler owns the {allowed: False} advisory is handled consistently.
- #2 KernelMetrics.to_dict serialized the builtin int type object for agent_crashes; now serializes self.agent_crashes (test asserts == 0 and is not int).
- #3 policy_checks/policy_violations double-count removed from syscall(); single owner in _check_policy (test asserts +1 per syscall).
- mute_agent.py #4/#5/#6: _normalize_request gives a shape-independent view so dict and object requests validate identically; missing/unknown action_type fails closed regardless of strict_mode; strict_mode is now honored for out-of-capability actions; validate_action runs the capability validators against its parameters via the shim; and _run_validator wraps validators so a raising validator fails closed. Parity and raise-fails-closed are both covered by tests.
- governance_layer.py #7: a raising alignment validator now appends a validator_error violation and bumps max_severity so aligned=False (was silently 'log but don't fail'). #8 get_audit_log: None -> full copy, 0 -> [] (correctly guarding the -0 == 0 slice-returns-everything trap), negative -> ValueError.
BREAKING_CHANGES.md documents the fail-closed default and migration path. CI green except CodeQL=NEUTRAL (non-blocking).
There was a problem hiding this comment.
Thank you for closing these fail-open cases. Few things to fix before merging:
BREAKING_CHANGES.mddocuments kernel only. Add entries forMuteAgentValidator(dict requests andvalidate_actionnow run validators;strict_mode=Falseis now honored) andGovernanceLayer.check_alignment(a raising validator now returnsaligned=False, wasTrue;get_audit_log(0)now returns[]).mute_agent.py:329-333:_run_validatorswallows the exception atlogger.debug. A validator crash under fail-closed should belogger.warningand record the error string, matching whatgovernance_layerdoes.test_layer3_framework.py:260:permissive=Truecomment is stale after the self-scoped carve-out.
…ing-changes, test cleanup) Addresses reviewer feedback on the fail-closed hardening PR. - mute_agent.py: _run_validator now logs a raising validator at warning level with the error string (was debug), so a broken validator's fail-closed rejection is visible to operators, matching how governance_layer surfaces alignment validator_error. - BREAKING_CHANGES.md: document the MuteAgentValidator behavior changes (dict requests and validate_action now run validators; strict_mode=False is now honored) and the GovernanceLayer changes (a raising alignment validator now yields aligned=False; get_audit_log(0) now returns []), alongside the existing kernel entry. - test_layer3_framework.py: restore the plain KernelSpace() usage example. Under the self-scoped syscall carve-out a no-engine kernel already allows own-VFS reads/writes, so the permissive=True workaround and its now-inaccurate comment are removed. The test still exercises the read/write plumbing and now fails if the self-scoped carve-out regresses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Liam Crumm <liamcrumm@gmail.com>
🤖 AI Agent: code-reviewer — View details
TL;DR: 0 blockers, 2 warnings. The PR addresses critical security issues effectively, but some areas need further attention.
Action items:
Warnings:
|
🤖 AI Agent: breaking-change-detector — API Compatibility
API Compatibility
|
🤖 AI Agent: docs-sync-checker — Docs Sync
Docs Sync
Documentation is in sync. |
🤖 AI Agent: test-generator — `agent_control_plane/governance_layer.py`
|
🤖 AI Agent: security-scanner — View details
No security issues found. |
Description
Three
agent_control_planecomponents inagent_oswere not properly wired into policy governance. This PR makes each enforce what it advertises and fail closed on missing, empty, or erroring state, and fixes two metric/limit correctness bugs in the same files.Eight defects fixed.
kernel_space.py_check_policy. A no-engine kernel now denies syscalls that reach outside the agent's own sandbox (SYS_EXEC, IPC, signals, spawn) via an explicit_SELF_SCOPED_SYSCALLSallowlist, while self-scoped ops (SYS_EXIT, own-VFS read/write) stay allowed. New opt-inpermissive=Falseflag onKernelSpaceandcreate_kernelrestores full allow-all.kernel_space.pyKernelMetrics.to_dictreturned the builtininttype foragent_crashesself.agent_crashes(breaksjson.dumpsotherwise)kernel_space.pypolicy_checksandpolicy_violationsdouble-counted per syscall_check_policy; removed the duplicate increments insyscall()mute_agent.pymute_agent.pystrict_modeconfig flag was never readstrict_mode; non-strict allows well-formed out-of-capability actions, strict rejectsmute_agent.pyvalidate_actionignored itsparametersgovernance_layer.pyvalidator_errorviolation and setaligned=Falsegovernance_layer.pyget_audit_log(0)returned the whole log0returns[](guards the[-0:]whole-list trap), negative raisesValueErrorA missing-engine denial returns a clean
SyscallResult(success=False)and does not fire thepolicy_violationsignal, so it cannot escalate to SIGKILL/AgentKernelPanic.SYS_CHECKPOLICYstays gated when an engine is present and reportsallowed=Falsewhen absent.Behavior change
A no-engine
KernelSpace()now denies external syscalls (notablySYS_EXECtool execution) by default instead of allowing them. Self-scoped syscalls (SYS_EXIT, own-VFS reads/writes) remain allowed, so agents that only use their own memory and exit are unaffected. Callers that relied on no-engine tool execution must passpermissive=Trueor wire a policy engine. This is recorded inBREAKING_CHANGES.mdnext to the analogous policy-engine default-deny entry.Type of Change
Package(s) Affected
Checklist
Notes on the checklist.
ruff check --select E,F,W --ignore E501is clean on the changed files with zero new violations. A new regression filetests/test_control_plane_fail_closed.pyadds one test per defect plus a scoped-fail-closed test, each flipping the documented repro from broken to correct. The module suite shows the same pre-existing failures as the base commit (missingsqlglotand other environment dependencies), so there are no net new regressions. Documentation updated via theKernelSpacedocstrings andBREAKING_CHANGES.md.Attribution & Prior Art
Prior art / related projects (if any):
None. Original work against the existing
agent_control_planeAI Assistance
If AI tools materially shaped this change, briefly note what was used:
Leveraged copilot for development
IP, Patents, and Licensing