Initial commit: Agent Governance System Phase 8
Phase 8 Production Hardening with complete governance infrastructure: - Vault integration with tiered policies (T0-T4) - DragonflyDB state management - SQLite audit ledger - Pipeline DSL and templates - Promotion/revocation engine - Checkpoint system for session persistence - Health manager and circuit breaker for fault tolerance - GitHub/Slack integrations - Architectural test pipeline with bug watcher, suggestion engine, council review - Multi-agent chaos testing framework Test Results: - Governance tests: 68/68 passing - E2E workflow: 16/16 passing - Phase 2 Vault: 14/14 passing - Integration tests: 27/27 passing Coverage: 57.6% average across 12 phases Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
# Wrappers
|
||||
|
||||
> Tool wrappers for sandboxed execution
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains tool wrappers for sandboxed execution.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `tf-governed.sh` | Shell script |
|
||||
| `ansible-governed.sh` | Shell script |
|
||||
|
||||
## Interfaces / APIs
|
||||
|
||||
*Document any APIs, CLI commands, or interfaces here.*
|
||||
|
||||
## Status
|
||||
|
||||
** Not Started**
|
||||
|
||||
See [STATUS.md](./STATUS.md) for detailed progress tracking.
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
Part of the [Agent Governance System](/opt/agent-governance/docs/ARCHITECTURE.md).
|
||||
|
||||
Parent: [Project Root](/opt/agent-governance)
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-23 23:25:09 UTC*
|
||||
@@ -0,0 +1,35 @@
|
||||
# Status: Wrappers
|
||||
|
||||
## Current Phase
|
||||
|
||||
** NOT STARTED**
|
||||
|
||||
## Tasks
|
||||
|
||||
| Status | Task | Updated |
|
||||
|--------|------|---------|
|
||||
| ☐ | *No tasks defined* | - |
|
||||
|
||||
## Dependencies
|
||||
|
||||
*No external dependencies.*
|
||||
|
||||
## Issues / Blockers
|
||||
|
||||
*No current issues or blockers.*
|
||||
|
||||
## Activity Log
|
||||
|
||||
### 2026-01-23 23:25:39 UTC
|
||||
- **Phase**: COMPLETE
|
||||
- **Action**: Tool wrappers for sandboxed execution
|
||||
- **Details**: Phase updated to complete
|
||||
|
||||
### 2026-01-23 23:25:09 UTC
|
||||
- **Phase**: NOT STARTED
|
||||
- **Action**: Initialized
|
||||
- **Details**: Status tracking initialized for this directory.
|
||||
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-23 23:25:39 UTC*
|
||||
Executable
+434
@@ -0,0 +1,434 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Governed Ansible Wrapper
|
||||
# ========================
|
||||
# Enforces check-mode-first and full audit trail.
|
||||
# Part of Phase 3: Execution Pipeline.
|
||||
#
|
||||
# Usage:
|
||||
# ansible-governed.sh check <playbook> [ansible options]
|
||||
# ansible-governed.sh run <playbook> --check-id=<id> [ansible options]
|
||||
# ansible-governed.sh adhoc <pattern> -m <module> [options]
|
||||
#
|
||||
# Rules:
|
||||
# 1. All playbook runs MUST have a corresponding check-mode artifact
|
||||
# 2. Check outputs are stored with hashes for verification
|
||||
# 3. All operations are logged to the governance ledger
|
||||
# 4. Ad-hoc commands require Tier 2+
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
GOVERNANCE_DIR="/opt/agent-governance"
|
||||
EVIDENCE_DIR="${GOVERNANCE_DIR}/evidence"
|
||||
PREFLIGHT_DIR="${GOVERNANCE_DIR}/preflight"
|
||||
ARTIFACT_DIR="${EVIDENCE_DIR}/ansible"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Ensure directories exist
|
||||
mkdir -p "${ARTIFACT_DIR}"
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_ok() {
|
||||
echo -e "${GREEN}[OK]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
log_block() {
|
||||
echo -e "${RED}[BLOCKED]${NC} $1"
|
||||
}
|
||||
|
||||
# Generate unique artifact ID
|
||||
generate_artifact_id() {
|
||||
echo "ans-$(date +%Y%m%d-%H%M%S)-$(openssl rand -hex 4)"
|
||||
}
|
||||
|
||||
# Get agent info from environment or default
|
||||
get_agent_info() {
|
||||
AGENT_ID="${AGENT_ID:-cli-user}"
|
||||
AGENT_TIER="${AGENT_TIER:-1}"
|
||||
}
|
||||
|
||||
# Run preflight checks
|
||||
run_preflight() {
|
||||
local targets="$1"
|
||||
|
||||
log_info "Running preflight checks..."
|
||||
|
||||
if [[ -f "${PREFLIGHT_DIR}/preflight.py" ]]; then
|
||||
cd "${PREFLIGHT_DIR}"
|
||||
python3 preflight.py ${targets} --action ansible --tier "${AGENT_TIER}" --agent-id "${AGENT_ID}" --quiet
|
||||
return $?
|
||||
else
|
||||
log_warn "Preflight system not available, skipping checks"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract inventory hosts from playbook
|
||||
extract_hosts() {
|
||||
local playbook="$1"
|
||||
|
||||
# Simple extraction - in production would parse YAML properly
|
||||
grep -E "^\s*hosts:" "${playbook}" 2>/dev/null | head -1 | sed 's/.*hosts:\s*//' | tr -d '"'"'" || echo "all"
|
||||
}
|
||||
|
||||
# Store check artifact
|
||||
store_check_artifact() {
|
||||
local check_output="$1"
|
||||
local artifact_id="$2"
|
||||
local playbook="$3"
|
||||
|
||||
local artifact_path="${ARTIFACT_DIR}/${artifact_id}"
|
||||
mkdir -p "${artifact_path}"
|
||||
|
||||
# Store check output
|
||||
echo "${check_output}" > "${artifact_path}/check-output.txt"
|
||||
|
||||
# Copy playbook for reference
|
||||
cp "${playbook}" "${artifact_path}/playbook.yml" 2>/dev/null || true
|
||||
|
||||
# Calculate checksum of playbook
|
||||
sha256sum "${playbook}" > "${artifact_path}/playbook.sha256" 2>/dev/null || true
|
||||
|
||||
# Store metadata
|
||||
cat > "${artifact_path}/metadata.json" << EOF
|
||||
{
|
||||
"artifact_id": "${artifact_id}",
|
||||
"type": "ansible_check",
|
||||
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"agent_id": "${AGENT_ID}",
|
||||
"agent_tier": ${AGENT_TIER},
|
||||
"playbook": "${playbook}",
|
||||
"playbook_checksum": "$(sha256sum ${playbook} 2>/dev/null | cut -d' ' -f1 || echo 'unknown')"
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "${artifact_path}"
|
||||
}
|
||||
|
||||
# Verify check artifact before run
|
||||
verify_check_artifact() {
|
||||
local artifact_id="$1"
|
||||
local playbook="$2"
|
||||
|
||||
local artifact_path="${ARTIFACT_DIR}/${artifact_id}"
|
||||
|
||||
if [[ ! -d "${artifact_path}" ]]; then
|
||||
log_error "Check artifact not found: ${artifact_id}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify playbook hasn't changed
|
||||
if [[ -f "${artifact_path}/playbook.sha256" ]]; then
|
||||
local stored_checksum=$(cat "${artifact_path}/playbook.sha256" | cut -d' ' -f1)
|
||||
local current_checksum=$(sha256sum "${playbook}" | cut -d' ' -f1)
|
||||
|
||||
if [[ "${stored_checksum}" != "${current_checksum}" ]]; then
|
||||
log_error "Playbook has changed since check was run!"
|
||||
log_error " Stored: ${stored_checksum}"
|
||||
log_error " Current: ${current_checksum}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
log_ok "Check artifact verified: ${artifact_id}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Log to governance ledger
|
||||
log_to_ledger() {
|
||||
local action="$1"
|
||||
local status="$2"
|
||||
local details="$3"
|
||||
|
||||
local ledger_entry=$(cat << EOF
|
||||
{
|
||||
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"agent_id": "${AGENT_ID}",
|
||||
"agent_tier": ${AGENT_TIER},
|
||||
"tool": "ansible",
|
||||
"action": "${action}",
|
||||
"status": "${status}",
|
||||
"details": "${details}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "${ledger_entry}" >> "${EVIDENCE_DIR}/ansible-ledger.jsonl"
|
||||
}
|
||||
|
||||
# Main command handlers
|
||||
cmd_check() {
|
||||
local playbook="$1"
|
||||
shift
|
||||
|
||||
get_agent_info
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "GOVERNED ANSIBLE CHECK"
|
||||
echo "=========================================="
|
||||
echo "Agent: ${AGENT_ID} (Tier ${AGENT_TIER})"
|
||||
echo "Playbook: ${playbook}"
|
||||
echo ""
|
||||
|
||||
if [[ ! -f "${playbook}" ]]; then
|
||||
log_error "Playbook not found: ${playbook}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract hosts for preflight
|
||||
local hosts=$(extract_hosts "${playbook}")
|
||||
log_info "Target hosts: ${hosts}"
|
||||
|
||||
# For preflight, use a sample target
|
||||
local preflight_target="sandbox-vm-01"
|
||||
|
||||
# Run preflight
|
||||
if ! run_preflight "${preflight_target}"; then
|
||||
log_block "Preflight checks failed"
|
||||
log_to_ledger "check" "BLOCKED" "Preflight failed"
|
||||
exit 1
|
||||
fi
|
||||
log_ok "Preflight checks passed"
|
||||
|
||||
# Generate artifact ID
|
||||
local artifact_id=$(generate_artifact_id)
|
||||
|
||||
log_info "Running ansible-playbook --check --diff..."
|
||||
log_info "Check will be stored as artifact: ${artifact_id}"
|
||||
|
||||
# Run check mode
|
||||
local check_output
|
||||
if check_output=$(ansible-playbook --check --diff "${playbook}" "$@" 2>&1); then
|
||||
log_ok "Ansible check completed"
|
||||
|
||||
# Store artifact
|
||||
local artifact_path=$(store_check_artifact "${check_output}" "${artifact_id}" "${playbook}")
|
||||
log_ok "Check artifact stored: ${artifact_path}"
|
||||
|
||||
log_to_ledger "check" "SUCCESS" "artifact_id=${artifact_id}"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "CHECK OUTPUT"
|
||||
echo "=========================================="
|
||||
echo "${check_output}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "CHECK COMPLETE"
|
||||
echo "=========================================="
|
||||
echo "Artifact ID: ${artifact_id}"
|
||||
echo "To run this playbook:"
|
||||
echo " ansible-governed.sh run ${playbook} --check-id=${artifact_id}"
|
||||
echo "=========================================="
|
||||
else
|
||||
log_error "Ansible check failed"
|
||||
echo "${check_output}"
|
||||
log_to_ledger "check" "FAILED" "ansible error"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_run() {
|
||||
local playbook="$1"
|
||||
shift
|
||||
|
||||
get_agent_info
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "GOVERNED ANSIBLE RUN"
|
||||
echo "=========================================="
|
||||
echo "Agent: ${AGENT_ID} (Tier ${AGENT_TIER})"
|
||||
echo "Playbook: ${playbook}"
|
||||
echo ""
|
||||
|
||||
if [[ ! -f "${playbook}" ]]; then
|
||||
log_error "Playbook not found: ${playbook}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse arguments
|
||||
local check_artifact_id=""
|
||||
local remaining_args=()
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--check-id=*)
|
||||
check_artifact_id="${arg#*=}"
|
||||
;;
|
||||
*)
|
||||
remaining_args+=("$arg")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Require check artifact
|
||||
if [[ -z "${check_artifact_id}" ]]; then
|
||||
log_block "Run requires a check artifact"
|
||||
echo ""
|
||||
echo "Usage: ansible-governed.sh run <playbook> --check-id=<artifact_id>"
|
||||
echo ""
|
||||
echo "First run: ansible-governed.sh check <playbook>"
|
||||
echo "Then use the artifact ID provided."
|
||||
log_to_ledger "run" "BLOCKED" "no check artifact"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify check artifact
|
||||
if ! verify_check_artifact "${check_artifact_id}" "${playbook}"; then
|
||||
log_block "Check verification failed"
|
||||
log_to_ledger "run" "BLOCKED" "verification failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run preflight again
|
||||
local preflight_target="sandbox-vm-01"
|
||||
if ! run_preflight "${preflight_target}"; then
|
||||
log_block "Preflight checks failed"
|
||||
log_to_ledger "run" "BLOCKED" "Preflight failed"
|
||||
exit 1
|
||||
fi
|
||||
log_ok "Preflight checks passed"
|
||||
|
||||
log_info "Running playbook (check artifact: ${check_artifact_id})..."
|
||||
|
||||
# Store artifact path for evidence
|
||||
local artifact_path="${ARTIFACT_DIR}/${check_artifact_id}"
|
||||
|
||||
# Run playbook
|
||||
local run_output
|
||||
if run_output=$(ansible-playbook "${playbook}" "${remaining_args[@]}" 2>&1); then
|
||||
log_ok "Ansible run completed"
|
||||
|
||||
# Store run evidence
|
||||
echo "${run_output}" > "${artifact_path}/run-output.txt"
|
||||
|
||||
cat > "${artifact_path}/run-result.json" << EOF
|
||||
{
|
||||
"ran_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"agent_id": "${AGENT_ID}",
|
||||
"status": "SUCCESS"
|
||||
}
|
||||
EOF
|
||||
|
||||
log_to_ledger "run" "SUCCESS" "artifact_id=${check_artifact_id}"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "RUN OUTPUT"
|
||||
echo "=========================================="
|
||||
echo "${run_output}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "RUN COMPLETE"
|
||||
echo "=========================================="
|
||||
echo "Check artifact: ${check_artifact_id}"
|
||||
echo "Evidence stored at: ${artifact_path}"
|
||||
echo "=========================================="
|
||||
else
|
||||
log_error "Ansible run failed"
|
||||
echo "${run_output}"
|
||||
|
||||
# Still store output as evidence
|
||||
echo "${run_output}" > "${artifact_path}/run-output.txt"
|
||||
|
||||
cat > "${artifact_path}/run-result.json" << EOF
|
||||
{
|
||||
"ran_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"agent_id": "${AGENT_ID}",
|
||||
"status": "FAILED"
|
||||
}
|
||||
EOF
|
||||
|
||||
log_to_ledger "run" "FAILED" "ansible error"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_adhoc() {
|
||||
get_agent_info
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "GOVERNED ANSIBLE AD-HOC"
|
||||
echo "=========================================="
|
||||
echo "Agent: ${AGENT_ID} (Tier ${AGENT_TIER})"
|
||||
echo ""
|
||||
|
||||
# Ad-hoc requires Tier 2+
|
||||
if [[ "${AGENT_TIER}" -lt 2 ]]; then
|
||||
log_block "Ad-hoc commands require Tier 2+ (current: Tier ${AGENT_TIER})"
|
||||
log_to_ledger "adhoc" "BLOCKED" "insufficient tier"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_warn "Ad-hoc commands bypass check-first requirement"
|
||||
log_info "All ad-hoc commands are logged for audit"
|
||||
|
||||
# Log the command
|
||||
log_to_ledger "adhoc" "EXECUTED" "args=$*"
|
||||
|
||||
# Run ansible
|
||||
ansible "$@"
|
||||
}
|
||||
|
||||
# Show usage
|
||||
usage() {
|
||||
echo "Governed Ansible Wrapper"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " ansible-governed.sh check <playbook> [ansible options]"
|
||||
echo " ansible-governed.sh run <playbook> --check-id=<artifact_id> [options]"
|
||||
echo " ansible-governed.sh adhoc <pattern> -m <module> [options]"
|
||||
echo ""
|
||||
echo "Environment Variables:"
|
||||
echo " AGENT_ID - Agent identifier (default: cli-user)"
|
||||
echo " AGENT_TIER - Agent trust tier 0-4 (default: 1)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " ansible-governed.sh check site.yml -i inventory.ini"
|
||||
echo " ansible-governed.sh run site.yml --check-id=ans-20260123-120000-abc"
|
||||
echo " AGENT_TIER=2 ansible-governed.sh adhoc all -m ping"
|
||||
}
|
||||
|
||||
# Main
|
||||
case "${1:-}" in
|
||||
check)
|
||||
shift
|
||||
cmd_check "$@"
|
||||
;;
|
||||
run)
|
||||
shift
|
||||
cmd_run "$@"
|
||||
;;
|
||||
adhoc)
|
||||
shift
|
||||
cmd_adhoc "$@"
|
||||
;;
|
||||
-h|--help|help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Executable
+399
@@ -0,0 +1,399 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Governed Terraform Wrapper
|
||||
# ==========================
|
||||
# Enforces plan-before-apply and full audit trail.
|
||||
# Part of Phase 3: Execution Pipeline.
|
||||
#
|
||||
# Usage:
|
||||
# tf-governed.sh plan <dir> [--target=<target>]
|
||||
# tf-governed.sh apply <dir> [--plan=<plan_file>]
|
||||
# tf-governed.sh destroy <dir> [--plan=<plan_file>]
|
||||
#
|
||||
# Rules:
|
||||
# 1. All applies MUST have a corresponding plan artifact
|
||||
# 2. Plans are stored with checksums for verification
|
||||
# 3. All operations are logged to the governance ledger
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
GOVERNANCE_DIR="/opt/agent-governance"
|
||||
EVIDENCE_DIR="${GOVERNANCE_DIR}/evidence"
|
||||
PREFLIGHT_DIR="${GOVERNANCE_DIR}/preflight"
|
||||
ARTIFACT_DIR="${EVIDENCE_DIR}/terraform"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Ensure directories exist
|
||||
mkdir -p "${ARTIFACT_DIR}"
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_ok() {
|
||||
echo -e "${GREEN}[OK]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
log_block() {
|
||||
echo -e "${RED}[BLOCKED]${NC} $1"
|
||||
}
|
||||
|
||||
# Generate unique artifact ID
|
||||
generate_artifact_id() {
|
||||
echo "tf-$(date +%Y%m%d-%H%M%S)-$(openssl rand -hex 4)"
|
||||
}
|
||||
|
||||
# Get agent info from environment or default
|
||||
get_agent_info() {
|
||||
AGENT_ID="${AGENT_ID:-cli-user}"
|
||||
AGENT_TIER="${AGENT_TIER:-1}"
|
||||
}
|
||||
|
||||
# Run preflight checks
|
||||
run_preflight() {
|
||||
local targets="$1"
|
||||
|
||||
log_info "Running preflight checks..."
|
||||
|
||||
if [[ -f "${PREFLIGHT_DIR}/preflight.py" ]]; then
|
||||
cd "${PREFLIGHT_DIR}"
|
||||
python3 preflight.py ${targets} --action terraform --tier "${AGENT_TIER}" --agent-id "${AGENT_ID}" --quiet
|
||||
return $?
|
||||
else
|
||||
log_warn "Preflight system not available, skipping checks"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Store plan artifact
|
||||
store_plan_artifact() {
|
||||
local plan_file="$1"
|
||||
local artifact_id="$2"
|
||||
local tf_dir="$3"
|
||||
|
||||
local artifact_path="${ARTIFACT_DIR}/${artifact_id}"
|
||||
mkdir -p "${artifact_path}"
|
||||
|
||||
# Copy plan binary
|
||||
cp "${plan_file}" "${artifact_path}/tfplan.binary"
|
||||
|
||||
# Generate plan JSON
|
||||
terraform -chdir="${tf_dir}" show -json "${plan_file}" > "${artifact_path}/tfplan.json" 2>/dev/null || true
|
||||
|
||||
# Generate human-readable plan
|
||||
terraform -chdir="${tf_dir}" show "${plan_file}" > "${artifact_path}/tfplan.txt" 2>/dev/null || true
|
||||
|
||||
# Calculate checksums
|
||||
sha256sum "${plan_file}" > "${artifact_path}/tfplan.sha256"
|
||||
|
||||
# Store metadata
|
||||
cat > "${artifact_path}/metadata.json" << EOF
|
||||
{
|
||||
"artifact_id": "${artifact_id}",
|
||||
"type": "terraform_plan",
|
||||
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"agent_id": "${AGENT_ID}",
|
||||
"agent_tier": ${AGENT_TIER},
|
||||
"tf_dir": "${tf_dir}",
|
||||
"checksum": "$(sha256sum ${plan_file} | cut -d' ' -f1)"
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "${artifact_path}"
|
||||
}
|
||||
|
||||
# Verify plan artifact before apply
|
||||
verify_plan_artifact() {
|
||||
local plan_file="$1"
|
||||
local artifact_id="$2"
|
||||
|
||||
local artifact_path="${ARTIFACT_DIR}/${artifact_id}"
|
||||
|
||||
if [[ ! -d "${artifact_path}" ]]; then
|
||||
log_error "Plan artifact not found: ${artifact_id}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify checksum
|
||||
local stored_checksum=$(cat "${artifact_path}/tfplan.sha256" | cut -d' ' -f1)
|
||||
local current_checksum=$(sha256sum "${plan_file}" | cut -d' ' -f1)
|
||||
|
||||
if [[ "${stored_checksum}" != "${current_checksum}" ]]; then
|
||||
log_error "Plan checksum mismatch!"
|
||||
log_error " Stored: ${stored_checksum}"
|
||||
log_error " Current: ${current_checksum}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_ok "Plan artifact verified: ${artifact_id}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Log to governance ledger
|
||||
log_to_ledger() {
|
||||
local action="$1"
|
||||
local status="$2"
|
||||
local details="$3"
|
||||
|
||||
local ledger_entry=$(cat << EOF
|
||||
{
|
||||
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"agent_id": "${AGENT_ID}",
|
||||
"agent_tier": ${AGENT_TIER},
|
||||
"tool": "terraform",
|
||||
"action": "${action}",
|
||||
"status": "${status}",
|
||||
"details": "${details}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "${ledger_entry}" >> "${EVIDENCE_DIR}/terraform-ledger.jsonl"
|
||||
}
|
||||
|
||||
# Main command handlers
|
||||
cmd_plan() {
|
||||
local tf_dir="$1"
|
||||
shift
|
||||
|
||||
get_agent_info
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "GOVERNED TERRAFORM PLAN"
|
||||
echo "=========================================="
|
||||
echo "Agent: ${AGENT_ID} (Tier ${AGENT_TIER})"
|
||||
echo "Directory: ${tf_dir}"
|
||||
echo ""
|
||||
|
||||
# Extract targets for preflight (simplified)
|
||||
local targets="sandbox-vm-01" # Default target for preflight
|
||||
|
||||
# Run preflight
|
||||
if ! run_preflight "${targets}"; then
|
||||
log_block "Preflight checks failed"
|
||||
log_to_ledger "plan" "BLOCKED" "Preflight failed"
|
||||
exit 1
|
||||
fi
|
||||
log_ok "Preflight checks passed"
|
||||
|
||||
# Generate artifact ID
|
||||
local artifact_id=$(generate_artifact_id)
|
||||
local plan_file="${tf_dir}/tfplan-${artifact_id}.binary"
|
||||
|
||||
log_info "Running terraform plan..."
|
||||
log_info "Plan will be stored as artifact: ${artifact_id}"
|
||||
|
||||
# Run terraform plan
|
||||
if terraform -chdir="${tf_dir}" plan -out="${plan_file}" "$@"; then
|
||||
log_ok "Terraform plan completed"
|
||||
|
||||
# Store artifact
|
||||
local artifact_path=$(store_plan_artifact "${plan_file}" "${artifact_id}" "${tf_dir}")
|
||||
log_ok "Plan artifact stored: ${artifact_path}"
|
||||
|
||||
log_to_ledger "plan" "SUCCESS" "artifact_id=${artifact_id}"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "PLAN COMPLETE"
|
||||
echo "=========================================="
|
||||
echo "Artifact ID: ${artifact_id}"
|
||||
echo "To apply this plan:"
|
||||
echo " tf-governed.sh apply ${tf_dir} --plan=${artifact_id}"
|
||||
echo "=========================================="
|
||||
else
|
||||
log_error "Terraform plan failed"
|
||||
log_to_ledger "plan" "FAILED" "terraform error"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_apply() {
|
||||
local tf_dir="$1"
|
||||
shift
|
||||
|
||||
get_agent_info
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "GOVERNED TERRAFORM APPLY"
|
||||
echo "=========================================="
|
||||
echo "Agent: ${AGENT_ID} (Tier ${AGENT_TIER})"
|
||||
echo "Directory: ${tf_dir}"
|
||||
echo ""
|
||||
|
||||
# Parse arguments
|
||||
local plan_artifact_id=""
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--plan=*)
|
||||
plan_artifact_id="${arg#*=}"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Require plan artifact
|
||||
if [[ -z "${plan_artifact_id}" ]]; then
|
||||
log_block "Apply requires a plan artifact"
|
||||
echo ""
|
||||
echo "Usage: tf-governed.sh apply <dir> --plan=<artifact_id>"
|
||||
echo ""
|
||||
echo "First run: tf-governed.sh plan <dir>"
|
||||
echo "Then use the artifact ID provided."
|
||||
log_to_ledger "apply" "BLOCKED" "no plan artifact"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local artifact_path="${ARTIFACT_DIR}/${plan_artifact_id}"
|
||||
local plan_file="${artifact_path}/tfplan.binary"
|
||||
|
||||
# Verify plan exists
|
||||
if [[ ! -f "${plan_file}" ]]; then
|
||||
log_block "Plan artifact not found: ${plan_artifact_id}"
|
||||
log_to_ledger "apply" "BLOCKED" "artifact not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify plan checksum
|
||||
if ! verify_plan_artifact "${plan_file}" "${plan_artifact_id}"; then
|
||||
log_block "Plan verification failed"
|
||||
log_to_ledger "apply" "BLOCKED" "checksum mismatch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run preflight again before apply
|
||||
local targets="sandbox-vm-01"
|
||||
if ! run_preflight "${targets}"; then
|
||||
log_block "Preflight checks failed"
|
||||
log_to_ledger "apply" "BLOCKED" "Preflight failed"
|
||||
exit 1
|
||||
fi
|
||||
log_ok "Preflight checks passed"
|
||||
|
||||
log_info "Applying plan: ${plan_artifact_id}"
|
||||
|
||||
# Copy plan to tf_dir for apply
|
||||
cp "${plan_file}" "${tf_dir}/tfplan.binary"
|
||||
|
||||
# Run terraform apply
|
||||
if terraform -chdir="${tf_dir}" apply "${tf_dir}/tfplan.binary"; then
|
||||
log_ok "Terraform apply completed"
|
||||
|
||||
# Store apply evidence
|
||||
cat > "${artifact_path}/apply-result.json" << EOF
|
||||
{
|
||||
"applied_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"agent_id": "${AGENT_ID}",
|
||||
"status": "SUCCESS"
|
||||
}
|
||||
EOF
|
||||
|
||||
log_to_ledger "apply" "SUCCESS" "artifact_id=${plan_artifact_id}"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "APPLY COMPLETE"
|
||||
echo "=========================================="
|
||||
echo "Plan artifact: ${plan_artifact_id}"
|
||||
echo "Evidence stored at: ${artifact_path}"
|
||||
echo "=========================================="
|
||||
else
|
||||
log_error "Terraform apply failed"
|
||||
log_to_ledger "apply" "FAILED" "terraform error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f "${tf_dir}/tfplan.binary"
|
||||
}
|
||||
|
||||
cmd_destroy() {
|
||||
local tf_dir="$1"
|
||||
shift
|
||||
|
||||
get_agent_info
|
||||
|
||||
log_warn "DESTROY requires Tier 3+ access"
|
||||
|
||||
if [[ "${AGENT_TIER}" -lt 3 ]]; then
|
||||
log_block "Destroy requires Tier 3+ (current: Tier ${AGENT_TIER})"
|
||||
log_to_ledger "destroy" "BLOCKED" "insufficient tier"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "GOVERNED TERRAFORM DESTROY"
|
||||
echo "=========================================="
|
||||
echo "Agent: ${AGENT_ID} (Tier ${AGENT_TIER})"
|
||||
echo "Directory: ${tf_dir}"
|
||||
echo ""
|
||||
|
||||
log_warn "This is a destructive operation!"
|
||||
|
||||
# For destroy, we still require plan-first
|
||||
log_info "To destroy, first create a destroy plan:"
|
||||
echo " terraform -chdir=${tf_dir} plan -destroy -out=destroy.plan"
|
||||
echo " Then use: tf-governed.sh apply ${tf_dir} --plan=<artifact_id>"
|
||||
|
||||
log_to_ledger "destroy" "INFO" "destroy guidance provided"
|
||||
}
|
||||
|
||||
# Show usage
|
||||
usage() {
|
||||
echo "Governed Terraform Wrapper"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " tf-governed.sh plan <directory> [terraform options]"
|
||||
echo " tf-governed.sh apply <directory> --plan=<artifact_id>"
|
||||
echo " tf-governed.sh destroy <directory>"
|
||||
echo ""
|
||||
echo "Environment Variables:"
|
||||
echo " AGENT_ID - Agent identifier (default: cli-user)"
|
||||
echo " AGENT_TIER - Agent trust tier 0-4 (default: 1)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " AGENT_ID=agent-001 tf-governed.sh plan ./infra"
|
||||
echo " tf-governed.sh apply ./infra --plan=tf-20260123-120000-abc123"
|
||||
}
|
||||
|
||||
# Main
|
||||
case "${1:-}" in
|
||||
plan)
|
||||
shift
|
||||
cmd_plan "$@"
|
||||
;;
|
||||
apply)
|
||||
shift
|
||||
cmd_apply "$@"
|
||||
;;
|
||||
destroy)
|
||||
shift
|
||||
cmd_destroy "$@"
|
||||
;;
|
||||
-h|--help|help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user