Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/governance-control-plane.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,34 @@ call upstream tools.

The Make target is `governance-rollout-control`.

## Operator Approval Workflow

The operator approval workflow records an expiring approval record for a
governance mutation. It supports rollout, rollback, policy_override, and break_glass approvals.
Approval records are local evidence only: they preserve
`changed_runtime_state: false`, set `requires_apply_step: true`, and do not
apply deployments, override policy, activate break-glass, contact a hosted
service, or call upstream tools.

```bash
mcp-broker governance approve \
--state-dir ~/mcp/mcp-broker/state \
--request-type rollout \
--operator release-operator \
--reason "approve staged rollout" \
--expires-at 2026-07-04T06:30:00Z \
--action-id 0001-broker-a-canary
```

Rollout and rollback approvals require at least one rollout action id.
Policy override approvals require at least one policy path. Break-glass
approvals require a break-glass record id. Every approval record includes the
operator, reason, expiration timestamp, target, approval id, and audit path.

The approval store writes records under `governance-approvals/records/` and
appends audit events to `governance-approvals/audit.jsonl`. The Make target is
`governance-approve`.

## Fleet Status Export And Collection

Fleet status is a redacted export derived from the local
Expand Down
2 changes: 1 addition & 1 deletion docs/phase-foundation-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ status collection, rollout control, approvals, and a reference control plane.
no-secret validation.
- [x] Add a rollout controller that turns simulator decisions into auditable
actions.
- [ ] Add operator approval workflow for mutating rollout, rollback, policy
- [x] Add operator approval workflow for mutating rollout, rollback, policy
override, and break-glass.
- [ ] Add a minimal reference control plane that exercises governance without
centralizing tool execution.
Expand Down
37 changes: 36 additions & 1 deletion mk/runtime.mk
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: runtime-layout doctor broker-secrets-sync broker-start broker-stop broker-status broker-wait broker-reap broker-smoke tools-count facade-smoke codex-facade-smoke claude-facade-smoke agy-facade-smoke profile-validation codex-profile-validation claude-profile-validation agy-profile-validation discovery-parity codex-claude-discovery-parity codex-deferred-acceptance secret-import-env deployment-stage deployment-rollback deployment-recover governance-pull governance-apply governance-rollback governance-rollout-control fleet-status-collect break-glass-create break-glass-status service-plan launchagent-install launchagent-load launchagent-uninstall launchagent-unload systemd-install systemd-load systemd-uninstall systemd-unload windows-install windows-load windows-unload linux-container-smoke linux-release-gate windows-powershell-smoke config-backup config-render codex-app-policy project-mcp-audit project-mcp-migrate config-rollback profile-snippet
.PHONY: runtime-layout doctor broker-secrets-sync broker-start broker-stop broker-status broker-wait broker-reap broker-smoke tools-count facade-smoke codex-facade-smoke claude-facade-smoke agy-facade-smoke profile-validation codex-profile-validation claude-profile-validation agy-profile-validation discovery-parity codex-claude-discovery-parity codex-deferred-acceptance secret-import-env deployment-stage deployment-rollback deployment-recover governance-pull governance-apply governance-rollback governance-rollout-control governance-approve fleet-status-collect break-glass-create break-glass-status service-plan launchagent-install launchagent-load launchagent-uninstall launchagent-unload systemd-install systemd-load systemd-uninstall systemd-unload windows-install windows-load windows-unload linux-container-smoke linux-release-gate windows-powershell-smoke config-backup config-render codex-app-policy project-mcp-audit project-mcp-migrate config-rollback profile-snippet

runtime-layout: ## Create configured runtime directories
$(call log_step,"Runtime layout")
Expand Down Expand Up @@ -184,6 +184,41 @@ governance-rollout-control: runtime-layout ## Record local rollout-control audit
$$CREATED_AT_ARG
$(call log_success,"Governance rollout-control completed")

governance-approve: runtime-layout ## Record an expiring local approval for a governance mutation
@test -n "$(GOVERNANCE_APPROVAL_REQUEST_TYPE)" || { $(call log_error,"Set GOVERNANCE_APPROVAL_REQUEST_TYPE"); exit 2; }
@test -n "$(GOVERNANCE_APPROVAL_OPERATOR)" || { $(call log_error,"Set GOVERNANCE_APPROVAL_OPERATOR"); exit 2; }
@test -n "$(GOVERNANCE_APPROVAL_REASON)" || { $(call log_error,"Set GOVERNANCE_APPROVAL_REASON"); exit 2; }
@test -n "$(GOVERNANCE_APPROVAL_EXPIRES_AT)" || { $(call log_error,"Set GOVERNANCE_APPROVAL_EXPIRES_AT"); exit 2; }
@if [[ -n "$(GOVERNANCE_APPROVAL_CREATED_AT)" ]]; then \
CREATED_AT_ARG="--created-at $(GOVERNANCE_APPROVAL_CREATED_AT)"; \
else \
CREATED_AT_ARG=""; \
fi; \
ACTION_ARGS=""; \
POLICY_ARGS=""; \
for ACTION_ID in $(GOVERNANCE_APPROVAL_ACTION_IDS); do \
ACTION_ARGS="$$ACTION_ARGS --action-id $$ACTION_ID"; \
done; \
for POLICY_PATH in $(GOVERNANCE_APPROVAL_POLICY_PATHS); do \
POLICY_ARGS="$$POLICY_ARGS --policy-path $$POLICY_PATH"; \
done; \
if [[ -n "$(GOVERNANCE_APPROVAL_BREAK_GLASS_RECORD_ID)" ]]; then \
BREAK_GLASS_ARG="--break-glass-record-id $(GOVERNANCE_APPROVAL_BREAK_GLASS_RECORD_ID)"; \
else \
BREAK_GLASS_ARG=""; \
fi; \
PYTHONPATH="$(PYTHONPATH)" $(PYTHON) -m mcp_broker.cli governance approve \
--state-dir "$(STATE_DIR)" \
--request-type "$(GOVERNANCE_APPROVAL_REQUEST_TYPE)" \
--operator "$(GOVERNANCE_APPROVAL_OPERATOR)" \
--reason "$(GOVERNANCE_APPROVAL_REASON)" \
--expires-at "$(GOVERNANCE_APPROVAL_EXPIRES_AT)" \
$$ACTION_ARGS \
$$POLICY_ARGS \
$$BREAK_GLASS_ARG \
$$CREATED_AT_ARG
$(call log_success,"Governance approval recorded")

fleet-status-collect: runtime-layout ## Prepare a redacted fleet-status collection envelope without uploading
@test -n "$(FLEET_STATUS_TARGET)" || { $(call log_error,"Set FLEET_STATUS_TARGET"); exit 2; }
@test -n "$(FLEET_STATUS_AUTH_REF)" || { $(call log_error,"Set FLEET_STATUS_AUTH_REF"); exit 2; }
Expand Down
47 changes: 47 additions & 0 deletions src/mcp_broker/cli_governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import argparse
from pathlib import Path

from mcp_broker.governance_approval import main as governance_approval_main
from mcp_broker.governance_pull import main as governance_pull_main
from mcp_broker.governance_rollout_controller import main as rollout_controller_main

Expand All @@ -24,11 +25,14 @@ def add_governance_parser(
_add_apply_parser(governance_subparsers)
_add_rollback_parser(governance_subparsers)
_add_rollout_control_parser(governance_subparsers)
_add_approve_parser(governance_subparsers)


def handle_governance(args: argparse.Namespace) -> int:
if args.governance_command == "rollout-control":
return _handle_rollout_control(args)
if args.governance_command == "approve":
return _handle_approve(args)
argv = [args.governance_command, "--state-dir", str(args.state_dir.expanduser())]
if args.governance_command == "pull":
argv.extend(
Expand Down Expand Up @@ -77,6 +81,30 @@ def _handle_rollout_control(args: argparse.Namespace) -> int:
return rollout_controller_main(argv)


def _handle_approve(args: argparse.Namespace) -> int:
argv = [
"--state-dir",
str(args.state_dir.expanduser()),
"--request-type",
args.request_type,
"--operator",
args.operator,
"--reason",
args.reason,
"--expires-at",
args.expires_at,
]
for action_id in args.action_id:
argv.extend(["--action-id", action_id])
for policy_path in args.policy_path:
argv.extend(["--policy-path", policy_path])
if args.break_glass_record_id:
argv.extend(["--break-glass-record-id", args.break_glass_record_id])
if args.created_at:
argv.extend(["--created-at", args.created_at])
return governance_approval_main(argv)


def _add_pull_parser(
governance_subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
) -> None:
Expand Down Expand Up @@ -132,3 +160,22 @@ def _add_rollout_control_parser(
rollout_parser.add_argument("--bundle-digest", required=True)
rollout_parser.add_argument("--created-at")
rollout_parser.set_defaults(handler=handle_governance)


def _add_approve_parser(
governance_subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
) -> None:
approve_parser = governance_subparsers.add_parser(
"approve",
help="Record an expiring local approval for a governance mutation",
)
approve_parser.add_argument("--state-dir", required=True, type=Path)
approve_parser.add_argument("--request-type", required=True)
approve_parser.add_argument("--operator", required=True)
approve_parser.add_argument("--reason", required=True)
approve_parser.add_argument("--expires-at", required=True)
approve_parser.add_argument("--action-id", action="append", default=[])
approve_parser.add_argument("--policy-path", action="append", default=[])
approve_parser.add_argument("--break-glass-record-id")
approve_parser.add_argument("--created-at")
approve_parser.set_defaults(handler=handle_governance)
Loading