Skip to content

fix(control-plane): fail closed and honor inputs across kernel/mute-agent/governance - #3263

Merged
MohammadHaroonAbuomar merged 4 commits into
mainfrom
fix/control-plane-fail-closed-enforcement
Jul 9, 2026
Merged

MohammadHaroonAbuomar merged 4 commits into
mainfrom
fix/control-plane-fail-closed-enforcement

Conversation

@liamcrumm

@liamcrumm liamcrumm commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Description

Three agent_control_plane components in agent_os were 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.

Area Defect Fix
kernel_space.py #1 no-engine kernel allowed every syscall (fail-open) Route all syscalls through _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_SYSCALLS allowlist, while self-scoped ops (SYS_EXIT, own-VFS read/write) stay allowed. New opt-in permissive=False flag on KernelSpace and create_kernel restores full allow-all.
kernel_space.py #2 KernelMetrics.to_dict returned the builtin int type for agent_crashes Serialize self.agent_crashes (breaks json.dumps otherwise)
kernel_space.py #3 policy_checks and policy_violations double-counted per syscall Single owner in _check_policy; removed the duplicate increments in syscall()
mute_agent.py #4 dict-shaped requests bypassed capability validators Normalize object-or-dict into a shape-independent view and always run validators
mute_agent.py #5 strict_mode config flag was never read Honor strict_mode; non-strict allows well-formed out-of-capability actions, strict rejects
mute_agent.py #6 validate_action ignored its parameters Run matching capability validators against the parameters
governance_layer.py #7 a raising alignment validator was reported compliant On raise, append a validator_error violation and set aligned=False
governance_layer.py #8 get_audit_log(0) returned the whole log 0 returns [] (guards the [-0:] whole-list trap), negative raises ValueError

A missing-engine denial returns a clean SyscallResult(success=False) and does not fire the policy_violation signal, so it cannot escalate to SIGKILL/AgentKernelPanic. SYS_CHECKPOLICY stays gated when an engine is present and reports allowed=False when absent.

Behavior change

A no-engine KernelSpace() now denies external syscalls (notably SYS_EXEC tool 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 pass permissive=True or wire a policy engine. This is recorded in BREAKING_CHANGES.md next to the analogous policy-engine default-deny entry.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Maintenance (dependency updates, CI/CD, refactoring)
  • Security fix

Package(s) Affected

  • agent-os-kernel
  • agent-mesh
  • agent-runtime
  • agent-sre
  • agent-governance
  • docs / root

Checklist

  • My code follows the project style guidelines (ruff check)
  • I have added tests that prove my fix/feature works
  • All new and existing tests pass (pytest)
  • I have updated documentation as needed
  • I have signed the Microsoft CLA

Notes on the checklist. ruff check --select E,F,W --ignore E501 is clean on the changed files with zero new violations. A new regression file tests/test_control_plane_fail_closed.py adds 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 (missing sqlglot and other environment dependencies), so there are no net new regressions. Documentation updated via the KernelSpace docstrings and BREAKING_CHANGES.md.

Attribution & Prior Art

  • This contribution does not contain code copied or derived from other projects without attribution
  • Any external projects that inspired this design are credited in code comments or documentation
  • If this PR implements functionality similar to an existing open-source project, I have listed it below

Prior art / related projects (if any):
None. Original work against the existing agent_control_plane

AI Assistance

  • I can explain every meaningful change in this PR: what it does, why, and what tradeoffs were considered
  • I have run tests and verification appropriate for this change
  • No part of this PR was autonomously submitted by an AI agent without my review
  • I have not used AI to generate review comments on others' PRs

If AI tools materially shaped this change, briefly note what was used:
Leveraged copilot for development

IP, Patents, and Licensing

  • This contribution does not implement patent-pending or patent-encumbered techniques
  • This contribution does not require an NDA or licensing agreement to understand or use
  • Any AI tools used have terms compatible with the MIT License

…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>
@github-actions github-actions Bot added the tests label Jul 6, 2026
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

PR Review Summary

Check Status Details
🔍 Code Review ⚠️ Missing No current-run comment
🛡️ Security Scan ⚠️ Missing No current-run comment
🔄 Breaking Changes ⚠️ Missing No current-run comment
📝 Docs Sync ⚠️ Missing No current-run comment
🧪 Test Coverage ⚠️ Missing No current-run comment

Verdict: ⚠️ AI review incomplete; ready for human review

AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@github-actions github-actions Bot added the size/XL Extra large PR (500+ lines) label Jul 6, 2026
…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>
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 6, 2026
@liamcrumm
liamcrumm marked this pull request as ready for review July 6, 2026 21:28

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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).

@MohammadHaroonAbuomar MohammadHaroonAbuomar left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you for closing these fail-open cases. Few things to fix before merging:

  • BREAKING_CHANGES.md documents kernel only. Add entries for MuteAgentValidator (dict requests and validate_action now run validators; strict_mode=False is now honored) and GovernanceLayer.check_alignment (a raising validator now returns aligned=False, was True; get_audit_log(0) now returns []).
  • mute_agent.py:329-333: _run_validator swallows the exception at logger.debug. A validator crash under fail-closed should be logger.warning and record the error string, matching what governance_layer does.
  • test_layer3_framework.py:260: permissive=True comment 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>
@github-actions

github-actions Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: code-reviewer — View details

AI-generated review output. Treat it as untrusted analysis and verify before acting.

TL;DR: 0 blockers, 2 warnings. The PR addresses critical security issues effectively, but some areas need further attention.

# Sev Issue Where
1 Warn Potential backward compatibility issues due to breaking changes. BREAKING_CHANGES.md, kernel_space.py
2 Warn Lack of explicit test coverage details for all eight fixed defects. PR description, tests/test_control_plane_fail_closed.py

Action items:

  1. None.

Warnings:

# Warning Where Follow-up
1 Ensure downstream users are adequately informed about breaking changes, especially for KernelSpace and MuteAgentValidator. BREAKING_CHANGES.md, kernel_space.py Fine as follow-up PR.
2 Confirm or expand test coverage for all eight defects, especially edge cases (e.g., get_audit_log(0), strict_mode, permissive=True). PR description, tests/test_control_plane_fail_closed.py Fine as follow-up PR.

@github-actions

github-actions Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: breaking-change-detector — API Compatibility

AI-generated review output. Treat it as untrusted analysis and verify before acting.

API Compatibility

Severity Change Impact
High KernelSpace now denies external syscalls by default when no policy engine is configured. A new permissive parameter must be explicitly set to True to allow all syscalls in this case. Existing code relying on the previous permissive behavior (allowing all syscalls without a policy engine) will now fail unless updated to use permissive=True.
High MuteAgentValidator.validate_request now validates dict-shaped requests. Previously unvalidated dict-shaped requests that bypassed capability validators will now be rejected if they fail validation.
High MuteAgentValidator.validate_action now validates parameters. Previously ignored parameters in validate_action are now validated, potentially rejecting previously accepted inputs.
High MuteAgentConfig.strict_mode is now honored. Previously ignored strict_mode configuration is now enforced. Code relying on the default behavior may need to explicitly set strict_mode=False to maintain prior behavior.
High GovernanceLayer.check_alignment now fails closed when a validator raises an exception. Validators that previously raised exceptions without causing a failure will now result in aligned=False and a validator_error violation.
Medium GovernanceLayer.get_audit_log(0) now returns an empty list instead of the entire log. Code relying on get_audit_log(0) to return the full log must now pass None or no argument to achieve the same behavior.
Medium KernelMetrics.to_dict now serializes agent_crashes correctly. Code relying on the previous incorrect behavior (returning the int type instead of the value) may break.

@github-actions

github-actions Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: docs-sync-checker — Docs Sync

AI-generated review output. Treat it as untrusted analysis and verify before acting.

Docs Sync

  • KernelSpace in agent_control_plane/kernel_space.py -- docstring updated for new permissive parameter and fail-closed behavior.
  • BREAKING_CHANGES.md -- updated with detailed entries for the fail-closed behavior in KernelSpace, MuteAgentValidator fixes, and GovernanceLayer changes.

Documentation is in sync.

@github-actions

github-actions Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: test-generator — `agent_control_plane/governance_layer.py`

AI-generated review output. Treat it as untrusted analysis and verify before acting.

agent_control_plane/governance_layer.py

  • test_check_alignment_validator_error -- Test that a raising alignment validator appends a validator_error violation and sets aligned=False.
  • test_get_audit_log_zero_limit -- Test that get_audit_log(0) returns an empty list and does not return the entire log.
  • test_get_audit_log_negative_limit -- Test that get_audit_log raises a ValueError when a negative limit is provided.

agent_control_plane/kernel_space.py

  • test_kernelspace_no_policy_engine_external_syscalls -- Test that external syscalls (e.g., SYS_EXEC) are denied when no policy engine is configured.
  • test_kernelspace_no_policy_engine_self_scoped_syscalls -- Test that self-scoped syscalls (e.g., SYS_EXIT, VFS operations) are allowed without a policy engine.
  • test_kernelspace_permissive_mode -- Test that permissive=True allows all syscalls even without a policy engine.
  • test_kernelmetrics_to_dict_agent_crashes -- Test that KernelMetrics.to_dict correctly serializes agent_crashes as an integer.

@github-actions

github-actions Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: security-scanner — View details

AI-generated review output. Treat it as untrusted analysis and verify before acting.

No security issues found.

@MohammadHaroonAbuomar
MohammadHaroonAbuomar enabled auto-merge (squash) July 9, 2026 08:06
@MohammadHaroonAbuomar
MohammadHaroonAbuomar merged commit 43d7bae into main Jul 9, 2026
126 checks passed
@MohammadHaroonAbuomar
MohammadHaroonAbuomar deleted the fix/control-plane-fail-closed-enforcement branch July 9, 2026 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/XL Extra large PR (500+ lines) tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants