Skill: devongenerally-png/governance-guard (version 0.1.0)

--- name: governance-guard description: > Structural authority separation for autonomous agent actions. Three-phase governance pipeline: PROPOSE, DECIDE, PROMOTE. No action is both proposed and approved by the same computational pathway. version: 0.1.0 metadata: author: MetaCortex Dynamics LLC license: MIT openclaw: requires: bins: - node - npx install: - kind: node package: tsx bins: [tsx] --- # governance-guard Governance guard enforces structural authority separation on all agent actions through a PROPOSE-DECIDE-PROMOTE pipeline. ## When to activate Before performing any **write**, **execute**, **network**, **create**, or **delete** action. The governance pipeline MUST be invoked before the action executes. Read actions may also be governed under standard or strict policies. ## How to use ### 1. Full pipeline (recommended) Run the complete PROPOSE → DECIDE → PROMOTE pipeline in a single call: ```bash npx tsx scripts/governance.ts pipeline '<intent-json>' --policy policies/standard.yaml ``` The intent JSON must include: - `skill`: skill identifier - `tool`: tool/function being invoked - `model`: LLM model name - `actionType`: one of `read`, `write`, `execute`, `network`, `create`, `delete` - `target`: resource being acted upon - `parameters`: tool parameters (object) - `dataScope`: data categories accessed (array, e.g. `["personal", "financial"]`) - `conversationId`: current conversation ID - `messageId`: current message ID - `userInstruction`: the user message that triggered this action ### 2. Handle the verdict The pipeline returns a JSON response: - If `"governance": "approved"` — proceed with the action - If `"governance": "deny"` — do NOT proceed; inform the user with the `reason` - If `"governance": "escalate"` — present the action to the user for approval: ``` Action requires your approval: Skill: <skill> Action: <actionType> on <target> Reason: <reason> Reply APPROVE or DENY ``` Then resolve: ```bash npx tsx scripts/governance.ts resolve-escalation <intent-id> approve # or npx tsx scripts/governance.ts resolve-escalation <intent-id> deny ``` ### 3. Audit decisions ```bash npx tsx scripts/governance.ts audit --last 10 ``` ## Policy presets | Preset | Default | Description | |--------|---------|-------------| | `minimal` | approve | Blocks only credentials and destructive commands. Lowest friction. | | `standard` | deny | Allows common ops, escalates network and data access. Recommended. | | `strict` | deny | Reads only. Everything else requires explicit approval. Maximum safety. | ## Fail-closed guarantee If any error occurs during governance evaluation, the default verdict is **DENY**. Missing policy files result in DENY ALL. This is by design. The system fails safe, never open. ## Configuration Governance data is stored in `~/.openclaw/governance/`: - `policy.yaml` — active policy file - `witness.jsonl` — append-only, hash-chained audit log ## Verify witness chain ```bash npx tsx scripts/governance.ts verify ``` Any tampering with historical records is detected by recomputing the hash chain from genesis.

path: package-lock.json
content: {
  "name": "@metacortex/governance-guard",
  "version": "0.1.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "@metacortex/governance-guard",
      "version": "0.1.0",
      "license": "MIT",
      "devDependencies": {
        "@types/node": "^22.0.0",
        "tsx": "^4.19.0",
        "typescript": "^5.6.0"
      },
      "engines": {
        "node": ">=22"
      }
    },
    "node_modules/@esbuild/aix-ppc64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "aix"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/android-arm": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/android-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/android-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/darwin-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/darwin-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/freebsd-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/freebsd-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-arm": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-ia32": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-loong64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "loong64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-mips64el": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "mips64el"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-ppc64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-riscv64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "riscv64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-s390x": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "s390x"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/netbsd-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "netbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/netbsd-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "netbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openbsd-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openbsd-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openharmony-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openharmony"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/sunos-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "sunos"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/win32-arm64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/win32-ia32": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/win32-x64": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@types/node": {
      "version": "22.19.13",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "undici-types": "~6.21.0"
      }
    },
    "node_modules/esbuild": {
      "version": "0.27.3",
      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "bin": {
        "esbuild": "bin/esbuild"
      },
      "engines": {
        "node": ">=18"
      },
      "optionalDependencies": {
        "@esbuild/aix-ppc64": "0.27.3",
        "@esbuild/android-arm": "0.27.3",
        "@esbuild/android-arm64": "0.27.3",
        "@esbuild/android-x64": "0.27.3",
        "@esbuild/darwin-arm64": "0.27.3",
        "@esbuild/darwin-x64": "0.27.3",
        "@esbuild/freebsd-arm64": "0.27.3",
        "@esbuild/freebsd-x64": "0.27.3",
        "@esbuild/linux-arm": "0.27.3",
        "@esbuild/linux-arm64": "0.27.3",
        "@esbuild/linux-ia32": "0.27.3",
        "@esbuild/linux-loong64": "0.27.3",
        "@esbuild/linux-mips64el": "0.27.3",
        "@esbuild/linux-ppc64": "0.27.3",
        "@esbuild/linux-riscv64": "0.27.3",
        "@esbuild/linux-s390x": "0.27.3",
        "@esbuild/linux-x64": "0.27.3",
        "@esbuild/netbsd-arm64": "0.27.3",
        "@esbuild/netbsd-x64": "0.27.3",
        "@esbuild/openbsd-arm64": "0.27.3",
        "@esbuild/openbsd-x64": "0.27.3",
        "@esbuild/openharmony-arm64": "0.27.3",
        "@esbuild/sunos-x64": "0.27.3",
        "@esbuild/win32-arm64": "0.27.3",
        "@esbuild/win32-ia32": "0.27.3",
        "@esbuild/win32-x64": "0.27.3"
      }
    },
    "node_modules/fsevents": {
      "version": "2.3.3",
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
      }
    },
    "node_modules/get-tsconfig": {
      "version": "4.13.6",
      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "resolve-pkg-maps": "^1.0.0"
      },
      "funding": {
        "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
      }
    },
    "node_modules/resolve-pkg-maps": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "funding": {
        "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
      }
    },
    "node_modules/tsx": {
      "version": "4.21.0",
      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "esbuild": "~0.27.0",
        "get-tsconfig": "^4.7.5"
      },
      "bin": {
        "tsx": "dist/cli.mjs"
      },
      "engines": {
        "node": ">=18.0.0"
      },
      "optionalDependencies": {
        "fsevents": "~2.3.3"
      }
    },
    "node_modules/typescript": {
      "version": "5.9.3",
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "bin": {
        "tsc": "bin/tsc",
        "tsserver": "bin/tsserver"
      },
      "engines": {
        "node": ">=14.17"
      }
    },
    "node_modules/undici-types": {
      "version": "6.21.0",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    }
  }
}


path: package.json
content: {
  "name": "@metacortex/governance-guard",
  "version": "0.1.0",
  "description": "Structural authority separation for autonomous agent actions",
  "license": "MIT",
  "type": "module",
  "engines": {
    "node": ">=22"
  },
  "packageManager": "pnpm@9.15.0",
  "scripts": {
    "test": "node --import tsx --test 'tests/**/*.test.ts'",
    "test:watch": "node --import tsx --test --watch 'tests/**/*.test.ts'",
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "tsx": "^4.19.0",
    "typescript": "^5.6.0"
  }
}


path: policies/minimal.yaml
content: version: "0.1"
default_verdict: approve

rules:
  - name: block-credentials
    match:
      target_pattern: "**/*.env"
    verdict: deny
    reason: "Credential files are never accessible"

  - name: block-ssh-keys
    match:
      target_pattern: "**/.ssh/**"
    verdict: deny
    reason: "SSH keys are never accessible"

  - name: block-destructive-shell
    match:
      action_type: execute
      tool_pattern: "shell.rm"
    verdict: deny
    reason: "Destructive shell commands require manual approval"

sensitive_data:
  - category: credentials
    patterns:
      - "**/*.env"
      - "**/.ssh/**"
      - "**/credentials*"
    action: deny


path: policies/standard.yaml
content: version: "0.1"
default_verdict: deny

rules:
  - name: allow-read-local
    match:
      action_type: read
      target_pattern: "~/**"
    verdict: approve
    reason: "Local file reads are permitted"

  - name: allow-read-workspace
    match:
      action_type: read
      target_pattern: "./**"
    verdict: approve
    reason: "Workspace file reads are permitted"

  - name: allow-write-workspace
    match:
      action_type: write
      target_pattern: "./**"
    verdict: approve
    reason: "Writes within workspace are permitted"

  - name: allow-create-workspace
    match:
      action_type: create
      target_pattern: "./**"
    verdict: approve
    reason: "File creation within workspace is permitted"

  - name: block-destructive-shell
    match:
      action_type: delete
      tool_pattern: "shell.*"
    verdict: deny
    reason: "Destructive shell commands require manual approval"

  - name: escalate-network
    match:
      action_type: network
    verdict: escalate
    reason: "External network requests require user confirmation"

  - name: escalate-delete
    match:
      action_type: delete
    verdict: escalate
    reason: "Deletion requires user approval"

  - name: block-profile-creation
    match:
      action_type: create
      data_scope:
        - personal
        - identity
    verdict: deny
    reason: "Creating profiles on external services is not authorized"

sensitive_data:
  - category: credentials
    patterns:
      - "**/*.env"
      - "**/.ssh/**"
      - "**/credentials*"
      - "**/*secret*"
    action: deny

  - category: financial
    patterns:
      - "**/*wallet*"
      - "**/*payment*"
    action: escalate

  - category: personal
    patterns:
      - "**/*personal*"
      - "**/*private*"
    action: escalate


path: policies/strict.yaml
content: version: "0.1"
default_verdict: deny

rules:
  - name: allow-read-workspace
    match:
      action_type: read
      target_pattern: "./**"
    verdict: approve
    reason: "Read access within workspace only"

  - name: escalate-write
    match:
      action_type: write
    verdict: escalate
    reason: "Write actions require explicit user approval"

  - name: escalate-execute
    match:
      action_type: execute
    verdict: escalate
    reason: "Execute actions require explicit user approval"

  - name: escalate-network
    match:
      action_type: network
    verdict: escalate
    reason: "Network actions require explicit user approval"

  - name: escalate-create
    match:
      action_type: create
    verdict: escalate
    reason: "Create actions require explicit user approval"

  - name: escalate-delete
    match:
      action_type: delete
    verdict: escalate
    reason: "Delete actions require explicit user approval"

sensitive_data:
  - category: credentials
    patterns:
      - "**/*.env"
      - "**/.ssh/**"
      - "**/credentials*"
      - "**/*secret*"
      - "**/*token*"
    action: deny

  - category: keys
    patterns:
      - "**/*.pem"
      - "**/*.key"
      - "**/*.p12"
    action: deny


path: README.md
content: # governance-guard

Structural authority separation for autonomous agent actions. An [OpenClaw](https://openclaw.dev) skill that interposes a three-phase governance pipeline between agent intent and execution.

**Core principle**: PROPOSE ≠ DECIDE ≠ PROMOTE. The agent proposes actions. A separate, deterministic policy engine decides admissibility. Only approved actions are promoted to execution. Every transition is witnessed.

## Quick start

```bash
# Install
npm install

# Run the governance pipeline
npx tsx scripts/governance.ts pipeline \
  '{"skill":"browser","tool":"fetch","model":"claude","actionType":"network","target":"https://api.example.com","parameters":{},"dataScope":[],"conversationId":"c1","messageId":"m1","userInstruction":"fetch the API"}' \
  --policy policies/standard.yaml
```

## Architecture

```
Agent Intent  →  PROPOSE  →  DECIDE  →  PROMOTE  →  Execution
                   │            │           │
              Serialize    Evaluate     Gate on
              + hash       policy       approval
                           (no LLM)    + freshness
```

| Phase | Authority | Implementation |
|-------|-----------|---------------|
| PROPOSE | Agent (LLM) | Structured intent capture with SHA-256 hash binding |
| DECIDE | Policy Engine (deterministic) | Pure function: policy + intent → verdict. No LLM. |
| PROMOTE | Execution Gate | Approve verdict + hash match + freshness check |

## Policy presets

| Preset | Default | Use case |
|--------|---------|----------|
| `minimal` | approve | Low friction. Blocks only credentials and destructive commands. |
| `standard` | deny | Recommended. Allows common ops, escalates network/delete. |
| `strict` | deny | Maximum safety. Reads only, everything else escalates. |

See [references/policy-schema.md](references/policy-schema.md) for the full policy file specification.

## Witness chain

Every governance decision is recorded as a hash-chained witness record in `~/.openclaw/governance/witness.jsonl`. The chain is tamper-evident: any modification to a historical record invalidates all subsequent hashes.

```bash
# Verify chain integrity
npx tsx scripts/governance.ts verify

# Audit recent decisions
npx tsx scripts/governance.ts audit --last 10
```

## Security properties

| Property | Guarantee |
|----------|-----------|
| Authority separation | DECIDE is a pure function with no LLM invocation |
| Fail-closed | System failures → deny. Missing policy → deny. Errors → deny. |
| Tamper evidence | Hash-chained witness log detects any historical modification |
| Verdict freshness | 30-second expiry prevents stale approval replay |
| Policy atomicity | Failed validation retains previous valid policy |

## Development

```bash
npm install
npm test                    # Run test suite
npm run typecheck           # TypeScript type checking
```

## License

MIT — MetaCortex Dynamics LLC


path: references/policy-schema.md
content: # Policy File Schema

Policy files are YAML documents stored at `~/.openclaw/governance/policy.yaml`.

## Top-level fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `version` | string | Yes | Schema version. Currently `"0.1"` |
| `default_verdict` | `"deny"` \| `"approve"` | Yes | Verdict when no rules match. Use `deny` for fail-closed. |
| `rules` | array | No | Ordered list of policy rules. First match wins. |
| `sensitive_data` | array | No | Sensitive data patterns. Checked before rules. |

## Rule fields

Each rule in the `rules` array:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Unique rule identifier |
| `match` | object | Yes | Matching conditions (all must be true) |
| `verdict` | `"approve"` \| `"deny"` \| `"escalate"` | Yes | Action to take on match |
| `reason` | string | No | Human-readable explanation |

## Match conditions

All specified conditions must be true for a rule to match (AND logic):

| Field | Type | Semantics |
|-------|------|-----------|
| `action_type` | string or string[] | Exact match against intent action type |
| `target_pattern` | string | Glob pattern against action target. `~` expands to home dir. `!` prefix for negation. |
| `tool_pattern` | string | Glob pattern against `skill.tool` identifier |
| `skill_pattern` | string | Glob pattern against skill identifier |
| `data_scope` | string[] | Array intersection with intent's data_scope (at least one match) |

## Glob patterns

- `*` matches any characters within a single path segment
- `**` matches any characters across path segments (recursive)
- `{a,b,c}` matches any of the alternatives
- `!pattern` negates the match (target does NOT match pattern)
- `~` at the start expands to the user's home directory

## Sensitive data rules

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `category` | string | Yes | Category name (e.g., `credentials`, `financial`) |
| `patterns` | string[] | No | Glob patterns to match against action target |
| `action` | `"deny"` \| `"escalate"` | Yes | Action when matched |

Sensitive data rules are evaluated before regular rules. If the action target matches any pattern, or the intent's `data_scope` contains the category name, the sensitive data rule fires.

## Evaluation order

1. Sensitive data rules (any match → immediate verdict)
2. Regular rules in declaration order (first match wins)
3. `default_verdict` if nothing matched

## Example

```yaml
version: "0.1"
default_verdict: deny

rules:
  - name: allow-read-workspace
    match:
      action_type: read
      target_pattern: "./**"
    verdict: approve
    reason: "Reads within workspace are safe"

  - name: escalate-network
    match:
      action_type: network
    verdict: escalate
    reason: "Network access needs user confirmation"

sensitive_data:
  - category: credentials
    patterns:
      - "**/*.env"
      - "**/.ssh/**"
    action: deny
```


path: scripts/governance.ts
content: /**
 * governance-guard CLI orchestrator.
 *
 * Commands:
 *   propose <json>                     Create an ActionIntent from raw JSON
 *   decide <intent-json> [--policy p]  Evaluate intent against policy
 *   promote <intent-json> <verdict-json>  Check if action should proceed
 *   pipeline <json> [--policy p]       Full PROPOSE → DECIDE → PROMOTE
 *   resolve-escalation <id> <approve|deny>  Resolve pending escalation
 *   audit [--last N]                   View recent witness records
 *   verify                             Verify witness chain integrity
 */

import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { homedir } from "node:os";
import { createIntent, validateIntent } from "./intent.js";
import type { ActionIntent, CreateIntentParams } from "./intent.js";
import { evaluate, computeVerdictHash } from "./policy-engine.js";
import type { Verdict } from "./policy-engine.js";
import { parsePolicyFile } from "./yaml-parse.js";
import type { PolicyFile } from "./yaml-parse.js";
import { openWitnessLog } from "./witness.js";
import type { ExecutionStatus } from "./witness.js";

// ── Configuration ────────────────────────────────────────────────────

export interface GovernanceConfig {
  policyPath: string;
  witnessPath: string;
  maxVerdictAge: number;   // seconds
  escalationTimeout: number; // seconds
}

const DEFAULT_GOV_DIR = join(homedir(), ".openclaw", "governance");
const DEFAULT_POLICY_PATH = join(DEFAULT_GOV_DIR, "policy.yaml");
const DEFAULT_WITNESS_PATH = join(DEFAULT_GOV_DIR, "witness.jsonl");
const DEFAULT_MAX_VERDICT_AGE = 30;
const DEFAULT_ESCALATION_TIMEOUT = 300;

function defaultConfig(): GovernanceConfig {
  return {
    policyPath: DEFAULT_POLICY_PATH,
    witnessPath: DEFAULT_WITNESS_PATH,
    maxVerdictAge: DEFAULT_MAX_VERDICT_AGE,
    escalationTimeout: DEFAULT_ESCALATION_TIMEOUT,
  };
}

// ── Policy loading (fail-closed) ─────────────────────────────────────

async function loadPolicy(policyPath: string): Promise<PolicyFile> {
  const content = await readFile(policyPath, "utf8");
  return parsePolicyFile(content);
}

function failClosedPolicy(): PolicyFile {
  return {
    version: "0.1",
    default_verdict: "deny",
    rules: [],
    sensitive_data: [],
  };
}

// ── Verdict freshness ────────────────────────────────────────────────

function isVerdictFresh(verdict: Verdict, maxAge: number): boolean {
  const verdictTime = new Date(verdict.timestamp).getTime();
  const now = Date.now();
  return (now - verdictTime) / 1000 <= maxAge;
}

// ── Pipeline functions ───────────────────────────────────────────────

export function propose(raw: unknown): ActionIntent {
  // If raw is already a full ActionIntent, validate and return
  if (typeof raw === "object" && raw !== null && "intent_hash" in raw) {
    const result = validateIntent(raw);
    if (result.valid) return result.intent;
    throw new Error(`Invalid ActionIntent: ${result.errors.map((e) => `${e.field}: ${e.message}`).join("; ")}`);
  }

  // Otherwise, create from params
  const params = raw as Record<string, unknown>;
  return createIntent({
    skill: String(params["skill"] ?? ""),
    tool: String(params["tool"] ?? ""),
    model: String(params["model"] ?? ""),
    actionType: String(params["actionType"] ?? params["action_type"] ?? "") as CreateIntentParams["actionType"],
    target: String(params["target"] ?? ""),
    parameters: (params["parameters"] ?? {}) as Record<string, unknown>,
    dataScope: Array.isArray(params["dataScope"] ?? params["data_scope"])
      ? (params["dataScope"] ?? params["data_scope"]) as string[]
      : [],
    conversationId: String(params["conversationId"] ?? params["conversation_id"] ?? ""),
    messageId: String(params["messageId"] ?? params["message_id"] ?? ""),
    userInstruction: String(params["userInstruction"] ?? params["user_instruction"] ?? ""),
  });
}

export async function decide(
  intent: ActionIntent,
  config: GovernanceConfig,
): Promise<Verdict> {
  let policy: PolicyFile;
  try {
    policy = await loadPolicy(config.policyPath);
  } catch {
    // Fail-closed: if policy can't be loaded, deny everything
    policy = failClosedPolicy();
  }

  const now = new Date().toISOString();

  try {
    return evaluate(intent, policy, now);
  } catch {
    // Fail-closed: any error in evaluation → deny
    const rule = "__error__";
    return {
      decision: "deny",
      intent_hash: intent.intent_hash,
      rule_matched: rule,
      reason: "Internal error during policy evaluation; fail-closed deny",
      timestamp: now,
      verdict_hash: computeVerdictHash("deny", intent.intent_hash, rule, now),
    };
  }
}

export function promote(
  intent: ActionIntent,
  verdict: Verdict,
  maxVerdictAge: number,
): boolean {
  const authorized = verdict.decision === "approve";
  const hashMatch = verdict.intent_hash === intent.intent_hash;
  const notExpired = isVerdictFresh(verdict, maxVerdictAge);
  return authorized && hashMatch && notExpired;
}

export async function pipeline(
  raw: unknown,
  config: GovernanceConfig,
): Promise<{ intent: ActionIntent; verdict: Verdict; promoted: boolean; record: import("./witness.js").WitnessRecord }> {
  // PROPOSE
  const intent = propose(raw);

  // DECIDE
  const verdict = await decide(intent, config);

  // PROMOTE
  const promoted = promote(intent, verdict, config.maxVerdictAge);

  // Determine execution status
  let status: ExecutionStatus;
  if (verdict.decision === "escalate") {
    status = "escalated";
  } else if (promoted) {
    status = "executed";
  } else {
    status = "blocked";
  }

  // WITNESS
  const log = await openWitnessLog(config.witnessPath);
  const record = await log.append(intent, verdict, status);

  return { intent, verdict, promoted, record };
}

export async function resolveEscalation(
  intentId: string,
  decision: "approve" | "deny",
  config: GovernanceConfig,
): Promise<import("./witness.js").WitnessRecord> {
  const log = await openWitnessLog(config.witnessPath);
  const existing = log.getByIntentId(intentId);

  if (existing === null) {
    throw new Error(`No witness record found for intent ${intentId}`);
  }
  if (existing.execution_result.status !== "escalated") {
    throw new Error(`Intent ${intentId} is not in escalated state (status: ${existing.execution_result.status})`);
  }

  // Check escalation timeout
  const escalatedAt = new Date(existing.execution_result.timestamp).getTime();
  const elapsed = (Date.now() - escalatedAt) / 1000;
  if (elapsed > config.escalationTimeout) {
    const status: ExecutionStatus = "user_denied";
    return log.append(existing.intent, existing.verdict, status);
  }

  const status: ExecutionStatus = decision === "approve" ? "user_approved" : "user_denied";
  return log.append(existing.intent, existing.verdict, status);
}

export async function audit(
  config: GovernanceConfig,
  last?: number,
): Promise<import("./witness.js").WitnessRecord[]> {
  const log = await openWitnessLog(config.witnessPath);
  return last !== undefined ? log.getLast(last) : log.getLast(50);
}

export async function verify(
  config: GovernanceConfig,
): Promise<{ valid: boolean; brokenAt?: number; reason?: string; totalRecords: number }> {
  const log = await openWitnessLog(config.witnessPath);
  const result = log.verifyChain();
  return { ...result, totalRecords: log.getSequence() };
}

// ── CLI ──────────────────────────────────────────────────────────────

function parseArgs(argv: string[]): { command: string; args: string[]; flags: Record<string, string> } {
  const args: string[] = [];
  const flags: Record<string, string> = {};
  let command = "";

  for (let i = 2; i < argv.length; i++) {
    const arg = argv[i]!;
    if (!command && !arg.startsWith("-")) {
      command = arg;
    } else if (arg.startsWith("--")) {
      const eqIdx = arg.indexOf("=");
      if (eqIdx !== -1) {
        flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
      } else if (i + 1 < argv.length && !argv[i + 1]!.startsWith("-")) {
        flags[arg.slice(2)] = argv[++i]!;
      } else {
        flags[arg.slice(2)] = "true";
      }
    } else {
      args.push(arg);
    }
  }

  return { command, args, flags };
}

function output(data: unknown): void {
  console.log(JSON.stringify(data, null, 2));
}

async function main(): Promise<void> {
  const { command, args, flags } = parseArgs(process.argv);
  const config = defaultConfig();

  if (flags["policy"]) config.policyPath = flags["policy"];
  if (flags["witness"]) config.witnessPath = flags["witness"];

  try {
    switch (command) {
      case "propose": {
        if (args.length < 1) {
          console.error("Usage: governance propose <json>");
          process.exit(1);
        }
        const raw = JSON.parse(args[0]!);
        const intent = propose(raw);
        output(intent);
        break;
      }

      case "decide": {
        if (args.length < 1) {
          console.error("Usage: governance decide <intent-json> [--policy path]");
          process.exit(1);
        }
        const intent = JSON.parse(args[0]!) as ActionIntent;
        const verdict = await decide(intent, config);
        output(verdict);
        break;
      }

      case "promote": {
        if (args.length < 2) {
          console.error("Usage: governance promote <intent-json> <verdict-json>");
          process.exit(1);
        }
        const intent = JSON.parse(args[0]!) as ActionIntent;
        const verdict = JSON.parse(args[1]!) as Verdict;
        const result = promote(intent, verdict, config.maxVerdictAge);
        output({ promoted: result });
        break;
      }

      case "pipeline": {
        if (args.length < 1) {
          console.error("Usage: governance pipeline <json> [--policy path]");
          process.exit(1);
        }
        const raw = JSON.parse(args[0]!);
        const result = await pipeline(raw, config);
        output({
          governance: result.verdict.decision === "approve" ? "approved" : result.verdict.decision,
          action: `${result.intent.action.type}:${result.intent.action.target}`,
          rule: result.verdict.rule_matched,
          reason: result.verdict.reason,
          promoted: result.promoted,
          intent_id: result.intent.id,
          witness_sequence: result.record.sequence,
        });
        break;
      }

      case "resolve-escalation": {
        if (args.length < 2) {
          console.error("Usage: governance resolve-escalation <intent-id> <approve|deny>");
          process.exit(1);
        }
        const decision = args[1] as "approve" | "deny";
        if (decision !== "approve" && decision !== "deny") {
          console.error("Decision must be 'approve' or 'deny'");
          process.exit(1);
        }
        const record = await resolveEscalation(args[0]!, decision, config);
        output({
          governance: decision === "approve" ? "user_approved" : "user_denied",
          intent_id: args[0],
          witness_sequence: record.sequence,
        });
        break;
      }

      case "audit": {
        const last = flags["last"] ? parseInt(flags["last"], 10) : undefined;
        const records = await audit(config, last);
        for (const record of records) {
          const r = record;
          console.log(
            `[${r.sequence}] ${r.execution_result.status.toUpperCase()} ` +
            `${r.intent.action.type}:${r.intent.action.target} ` +
            `(${r.verdict.rule_matched}) — ${r.verdict.reason}`
          );
        }
        break;
      }

      case "verify": {
        const result = await verify(config);
        if (result.valid) {
          console.log(`Witness chain valid. ${result.totalRecords} records verified.`);
        } else {
          console.error(`Witness chain BROKEN at record ${result.brokenAt}: ${result.reason}`);
          process.exit(1);
        }
        break;
      }

      case "--help":
      case "help":
      case "": {
        console.log(`governance-guard v0.1.0 — Structural authority separation

Commands:
  propose <json>                        Create an ActionIntent
  decide <intent-json> [--policy p]     Evaluate against policy
  promote <intent-json> <verdict-json>  Check promotion eligibility
  pipeline <json> [--policy p]          Full PROPOSE → DECIDE → PROMOTE
  resolve-escalation <id> <approve|deny>  Resolve user escalation
  audit [--last N]                      View recent witness records
  verify                                Verify witness chain integrity

Options:
  --policy <path>    Path to policy YAML file
  --witness <path>   Path to witness JSONL file`);
        break;
      }

      default:
        console.error(`Unknown command: ${command}. Run with --help for usage.`);
        process.exit(1);
    }
  } catch (err) {
    console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
    process.exit(1);
  }
}

// Run CLI if executed directly
const isMain = process.argv[1]?.endsWith("governance.ts") || process.argv[1]?.endsWith("governance.js");
if (isMain) {
  main();
}


path: scripts/intent.ts
content: /**
 * PROPOSE phase: ActionIntent serialization and cryptographic binding.
 *
 * Captures agent tool calls as structured ActionIntent objects with
 * SHA-256 hash binding for tamper-evident governance evaluation.
 */

import { createHash, randomUUID } from "node:crypto";

// ── Types ────────────────────────────────────────────────────────────

export type ActionType = "read" | "write" | "execute" | "network" | "create" | "delete";

const VALID_ACTION_TYPES: ReadonlySet<string> = new Set([
  "read", "write", "execute", "network", "create", "delete",
]);

export interface ActionIntent {
  id: string;
  timestamp: string;
  source: {
    skill: string;
    tool: string;
    model: string;
  };
  action: {
    type: ActionType;
    target: string;
    parameters: Record<string, unknown>;
    data_scope: string[];
  };
  context: {
    conversation_id: string;
    message_id: string;
    user_instruction: string;
  };
  intent_hash: string;
}

export interface CreateIntentParams {
  skill: string;
  tool: string;
  model: string;
  actionType: ActionType;
  target: string;
  parameters: Record<string, unknown>;
  dataScope: string[];
  conversationId: string;
  messageId: string;
  userInstruction: string;
}

// ── Canonical serialization ──────────────────────────────────────────

/**
 * Produces deterministic JSON: sorted keys at all levels, no whitespace.
 * This is the canonical form used for hash computation.
 */
export function canonicalize(obj: unknown): string {
  if (obj === null || obj === undefined) return "null";
  if (typeof obj === "string") return JSON.stringify(obj);
  if (typeof obj === "number" || typeof obj === "boolean") return String(obj);
  if (Array.isArray(obj)) {
    return "[" + obj.map(canonicalize).join(",") + "]";
  }
  if (typeof obj === "object") {
    const keys = Object.keys(obj as Record<string, unknown>).sort();
    const pairs = keys.map(
      (k) => JSON.stringify(k) + ":" + canonicalize((obj as Record<string, unknown>)[k])
    );
    return "{" + pairs.join(",") + "}";
  }
  return String(obj);
}

/**
 * SHA-256 hash of canonical JSON serialization.
 */
export function computeHash(data: string): string {
  return createHash("sha256").update(data, "utf8").digest("hex");
}

/**
 * Compute the intent_hash for an ActionIntent (excludes the hash field itself).
 */
export function computeIntentHash(intent: Omit<ActionIntent, "intent_hash">): string {
  return computeHash(canonicalize(intent));
}

// ── Intent creation ──────────────────────────────────────────────────

export function createIntent(params: CreateIntentParams): ActionIntent {
  const partial = {
    id: randomUUID(),
    timestamp: new Date().toISOString(),
    source: {
      skill: params.skill,
      tool: params.tool,
      model: params.model,
    },
    action: {
      type: params.actionType,
      target: params.target,
      parameters: params.parameters,
      data_scope: params.dataScope,
    },
    context: {
      conversation_id: params.conversationId,
      message_id: params.messageId,
      user_instruction: params.userInstruction,
    },
  };

  const intent_hash = computeIntentHash(partial);
  return { ...partial, intent_hash };
}

// ── Validation ───────────────────────────────────────────────────────

export interface ValidationError {
  field: string;
  message: string;
}

export function validateIntent(intent: unknown): { valid: true; intent: ActionIntent } | { valid: false; errors: ValidationError[] } {
  const errors: ValidationError[] = [];

  if (intent === null || typeof intent !== "object") {
    return { valid: false, errors: [{ field: "root", message: "Intent must be an object" }] };
  }

  const obj = intent as Record<string, unknown>;

  // Top-level fields
  if (typeof obj["id"] !== "string" || obj["id"] === "") {
    errors.push({ field: "id", message: "Must be a non-empty string" });
  }
  if (typeof obj["timestamp"] !== "string" || obj["timestamp"] === "") {
    errors.push({ field: "timestamp", message: "Must be a non-empty ISO 8601 string" });
  }

  // Source
  if (obj["source"] === null || typeof obj["source"] !== "object") {
    errors.push({ field: "source", message: "Must be an object" });
  } else {
    const src = obj["source"] as Record<string, unknown>;
    if (typeof src["skill"] !== "string") errors.push({ field: "source.skill", message: "Must be a string" });
    if (typeof src["tool"] !== "string") errors.push({ field: "source.tool", message: "Must be a string" });
    if (typeof src["model"] !== "string") errors.push({ field: "source.model", message: "Must be a string" });
  }

  // Action
  if (obj["action"] === null || typeof obj["action"] !== "object") {
    errors.push({ field: "action", message: "Must be an object" });
  } else {
    const act = obj["action"] as Record<string, unknown>;
    if (typeof act["type"] !== "string" || !VALID_ACTION_TYPES.has(act["type"])) {
      errors.push({ field: "action.type", message: `Must be one of: ${[...VALID_ACTION_TYPES].join(", ")}` });
    }
    if (typeof act["target"] !== "string") {
      errors.push({ field: "action.target", message: "Must be a string" });
    }
    if (act["parameters"] === null || typeof act["parameters"] !== "object" || Array.isArray(act["parameters"])) {
      errors.push({ field: "action.parameters", message: "Must be an object" });
    }
    if (!Array.isArray(act["data_scope"])) {
      errors.push({ field: "action.data_scope", message: "Must be an array" });
    }
  }

  // Context
  if (obj["context"] === null || typeof obj["context"] !== "object") {
    errors.push({ field: "context", message: "Must be an object" });
  } else {
    const ctx = obj["context"] as Record<string, unknown>;
    if (typeof ctx["conversation_id"] !== "string") errors.push({ field: "context.conversation_id", message: "Must be a string" });
    if (typeof ctx["message_id"] !== "string") errors.push({ field: "context.message_id", message: "Must be a string" });
    if (typeof ctx["user_instruction"] !== "string") errors.push({ field: "context.user_instruction", message: "Must be a string" });
  }

  // Intent hash
  if (typeof obj["intent_hash"] !== "string" || obj["intent_hash"] === "") {
    errors.push({ field: "intent_hash", message: "Must be a non-empty string" });
  }

  if (errors.length > 0) return { valid: false, errors };

  // Verify hash integrity
  const typedObj = obj as unknown as ActionIntent;
  const { intent_hash: existingHash, ...rest } = typedObj;
  const expectedHash = computeIntentHash(rest);
  if (existingHash !== expectedHash) {
    errors.push({ field: "intent_hash", message: "Hash does not match canonical serialization" });
    return { valid: false, errors };
  }

  return { valid: true, intent: typedObj };
}


path: scripts/policy-engine.ts
content: /**
 * DECIDE phase: Pure, deterministic policy evaluation.
 *
 * evaluate() is a pure function: (ActionIntent, PolicyFile, timestamp) → Verdict.
 * No LLM invocation, no I/O, no side effects. This is what makes authority
 * separation structural rather than behavioral.
 */

import os from "node:os";
import type { ActionIntent } from "./intent.js";
import { computeHash, canonicalize } from "./intent.js";
import type { PolicyFile, PolicyRule, SensitiveDataRule } from "./yaml-parse.js";

// ── Types ────────────────────────────────────────────────────────────

export type VerdictDecision = "approve" | "deny" | "escalate";

export interface Verdict {
  decision: VerdictDecision;
  intent_hash: string;
  rule_matched: string;
  reason: string;
  timestamp: string;
  verdict_hash: string;
}

// ── Hash computation ─────────────────────────────────────────────────

export function computeVerdictHash(
  decision: string,
  intentHash: string,
  rule: string,
  timestamp: string,
): string {
  return computeHash(canonicalize({ decision, intent_hash: intentHash, rule, timestamp }));
}

// ── Glob matching ────────────────────────────────────────────────────

/**
 * Expand ~ to home directory in patterns and paths.
 */
function expandTilde(p: string): string {
  if (p.startsWith("~/") || p === "~") {
    return os.homedir().replace(/\\/g, "/") + p.slice(1);
  }
  return p;
}

/**
 * Normalize to forward slashes for cross-platform matching.
 */
function normalize(p: string): string {
  return p.replace(/\\/g, "/");
}

/**
 * Expand brace patterns: "{a,b,c}" → ["a", "b", "c"].
 * Returns null if no braces found.
 */
function expandBraces(pattern: string): string[] | null {
  const start = pattern.indexOf("{");
  if (start === -1) return null;
  const end = pattern.indexOf("}", start);
  if (end === -1) return null;

  const prefix = pattern.slice(0, start);
  const suffix = pattern.slice(end + 1);
  const alternatives = pattern.slice(start + 1, end).split(",");

  return alternatives.map((alt) => prefix + alt.trim() + suffix);
}

/**
 * Match a path against a glob pattern.
 * Uses path.matchesGlob (Node 22+) with tilde expansion and brace handling.
 */
export function matchGlob(target: string, pattern: string): boolean {
  const normTarget = normalize(expandTilde(target));
  const normPattern = normalize(expandTilde(pattern));

  // Handle negation: "!pattern" means "does NOT match pattern"
  if (normPattern.startsWith("!")) {
    return !matchGlobInner(normTarget, normPattern.slice(1));
  }

  return matchGlobInner(normTarget, normPattern);
}

/**
 * Convert a glob pattern to a RegExp.
 * Handles *, **, ?, and dotfiles (no special dotfile treatment).
 */
function globToRegex(pattern: string): RegExp {
  let re = "^";
  let i = 0;
  while (i < pattern.length) {
    const ch = pattern[i]!;
    if (ch === "*") {
      if (pattern[i + 1] === "*") {
        // ** matches everything including /
        if (pattern[i + 2] === "/") {
          re += "(?:.*/)?";
          i += 3;
        } else {
          re += ".*";
          i += 2;
        }
      } else {
        // * matches everything except /
        re += "[^/]*";
        i++;
      }
    } else if (ch === "?") {
      re += "[^/]";
      i++;
    } else if (".+^${}()|[]\\".includes(ch)) {
      re += "\\" + ch;
      i++;
    } else {
      re += ch;
      i++;
    }
  }
  re += "$";
  return new RegExp(re);
}

function matchGlobInner(target: string, pattern: string): boolean {
  // Try brace expansion
  const expanded = expandBraces(pattern);
  if (expanded !== null) {
    return expanded.some((p) => matchGlobInner(target, p));
  }

  try {
    return globToRegex(pattern).test(target);
  } catch {
    return false;
  }
}

// ── Rule matching ────────────────────────────────────────────────────

/**
 * Check if an ActionIntent matches a single policy rule.
 */
export function matchRule(intent: ActionIntent, rule: PolicyRule): boolean {
  const match = rule.match;

  // action_type: exact match (string) or any-of match (string[])
  if (match.action_type !== undefined) {
    if (Array.isArray(match.action_type)) {
      if (!match.action_type.includes(intent.action.type)) return false;
    } else {
      if (match.action_type !== intent.action.type) return false;
    }
  }

  // target_pattern: glob against action target
  if (match.target_pattern !== undefined) {
    if (!matchGlob(intent.action.target, match.target_pattern)) return false;
  }

  // tool_pattern: glob against "skill:tool" or just "tool"
  if (match.tool_pattern !== undefined) {
    const toolId = `${intent.source.skill}.${intent.source.tool}`;
    // Match against both the combined id and just the tool name
    if (!matchGlob(toolId, match.tool_pattern) && !matchGlob(intent.source.tool, match.tool_pattern)) {
      return false;
    }
  }

  // skill_pattern: glob against skill identifier
  if (match.skill_pattern !== undefined) {
    if (!matchGlob(intent.source.skill, match.skill_pattern)) return false;
  }

  // data_scope: array intersection (at least one common element)
  if (match.data_scope !== undefined && match.data_scope.length > 0) {
    const intentScopes = new Set(intent.action.data_scope);
    const hasIntersection = match.data_scope.some((s) => intentScopes.has(s));
    if (!hasIntersection) return false;
  }

  return true;
}

/**
 * Check if intent target matches any sensitive data pattern.
 */
export function checkSensitiveData(
  intent: ActionIntent,
  rules: SensitiveDataRule[],
): SensitiveDataRule | null {
  for (const rule of rules) {
    for (const pattern of rule.patterns) {
      if (matchGlob(intent.action.target, pattern)) {
        return rule;
      }
    }
    // Also check data_scope overlap
    const intentScopes = new Set(intent.action.data_scope);
    if (intentScopes.has(rule.category)) {
      return rule;
    }
  }
  return null;
}

// ── Core DECIDE function ─────────────────────────────────────────────

/**
 * Pure, deterministic policy evaluation.
 *
 * @param intent - The ActionIntent to evaluate
 * @param policy - The active PolicyFile
 * @param now    - ISO 8601 timestamp (injected for testable determinism)
 * @returns Verdict with decision, matched rule, and hash binding
 */
export function evaluate(
  intent: ActionIntent,
  policy: PolicyFile,
  now: string,
): Verdict {
  // Check sensitive data rules first (these override regular rules)
  const sensitiveMatch = checkSensitiveData(intent, policy.sensitive_data);
  if (sensitiveMatch !== null) {
    const decision = sensitiveMatch.action;
    const rule = `__sensitive_data__:${sensitiveMatch.category}`;
    return {
      decision,
      intent_hash: intent.intent_hash,
      rule_matched: rule,
      reason: `Sensitive data category '${sensitiveMatch.category}' detected`,
      timestamp: now,
      verdict_hash: computeVerdictHash(decision, intent.intent_hash, rule, now),
    };
  }

  // Evaluate rules in order; first match wins
  for (const rule of policy.rules) {
    if (matchRule(intent, rule)) {
      return {
        decision: rule.verdict,
        intent_hash: intent.intent_hash,
        rule_matched: rule.name,
        reason: rule.reason ?? `Matched rule: ${rule.name}`,
        timestamp: now,
        verdict_hash: computeVerdictHash(rule.verdict, intent.intent_hash, rule.name, now),
      };
    }
  }

  // No rule matched: apply default verdict (fail-closed = deny)
  const rule = "__default__";
  return {
    decision: policy.default_verdict,
    intent_hash: intent.intent_hash,
    rule_matched: rule,
    reason: "No matching rule; default policy applied",
    timestamp: now,
    verdict_hash: computeVerdictHash(policy.default_verdict, intent.intent_hash, rule, now),
  };
}


path: scripts/witness.ts
content: /**
 * Hash-chained witness log: tamper-evident audit trail.
 *
 * Every governance decision produces a WitnessRecord appended to a JSONL file.
 * Records are hash-chained: any tampering invalidates all subsequent hashes.
 */

import { readFile, appendFile, writeFile, mkdir } from "node:fs/promises";
import { dirname } from "node:path";
import type { ActionIntent } from "./intent.js";
import { computeHash, canonicalize } from "./intent.js";
import type { Verdict } from "./policy-engine.js";

// ── Types ────────────────────────────────────────────────────────────

export type ExecutionStatus =
  | "executed"
  | "blocked"
  | "escalated"
  | "user_approved"
  | "user_denied";

export interface ExecutionResult {
  status: ExecutionStatus;
  timestamp: string;
}

export interface WitnessRecord {
  sequence: number;
  intent: ActionIntent;
  verdict: Verdict;
  execution_result: ExecutionResult;
  prev_hash: string;
  record_hash: string;
}

// ── Constants ────────────────────────────────────────────────────────

const GENESIS_HASH = computeHash("governance-guard:genesis:v0.1");

// ── Hash computation ─────────────────────────────────────────────────

export function computeRecordHash(
  sequence: number,
  intentHash: string,
  verdictHash: string,
  executionResult: ExecutionResult,
  prevHash: string,
): string {
  return computeHash(
    canonicalize({
      execution_result: executionResult,
      intent_hash: intentHash,
      prev_hash: prevHash,
      sequence,
      verdict_hash: verdictHash,
    }),
  );
}

// ── WitnessLog implementation ────────────────────────────────────────

export interface WitnessLog {
  append(
    intent: ActionIntent,
    verdict: Verdict,
    status: ExecutionStatus,
  ): Promise<WitnessRecord>;
  getLatest(): WitnessRecord | null;
  getLast(n: number): WitnessRecord[];
  getByIntentId(id: string): WitnessRecord | null;
  verifyChain(): { valid: boolean; brokenAt?: number; reason?: string };
  getSequence(): number;
}

class WitnessLogImpl implements WitnessLog {
  private records: WitnessRecord[] = [];
  private intentIndex = new Map<string, number>();

  constructor(private filePath: string) {}

  /** Load existing records from JSONL file. Skips malformed trailing lines. */
  async load(): Promise<void> {
    let content: string;
    try {
      content = await readFile(this.filePath, "utf8");
    } catch (err) {
      if ((err as NodeJS.ErrnoException).code === "ENOENT") {
        // File doesn't exist yet — start fresh
        await mkdir(dirname(this.filePath), { recursive: true });
        await writeFile(this.filePath, "", "utf8");
        return;
      }
      throw err;
    }

    if (content.trim() === "") return;

    const lines = content.trim().split("\n");
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i]!.trim();
      if (line === "") continue;
      try {
        const record = JSON.parse(line) as WitnessRecord;
        this.records.push(record);
        this.intentIndex.set(record.intent.id, i);
      } catch {
        // Skip malformed trailing lines (partial write recovery)
        // Only tolerate malformed lines at the end
        if (i < lines.length - 1) {
          throw new Error(`Malformed witness record at line ${i + 1} (not trailing)`);
        }
      }
    }
  }

  async append(
    intent: ActionIntent,
    verdict: Verdict,
    status: ExecutionStatus,
  ): Promise<WitnessRecord> {
    const sequence = this.records.length;
    const prevHash =
      sequence === 0 ? GENESIS_HASH : this.records[sequence - 1]!.record_hash;

    const executionResult: ExecutionResult = {
      status,
      timestamp: new Date().toISOString(),
    };

    const recordHash = computeRecordHash(
      sequence,
      intent.intent_hash,
      verdict.verdict_hash,
      executionResult,
      prevHash,
    );

    const record: WitnessRecord = {
      sequence,
      intent,
      verdict,
      execution_result: executionResult,
      prev_hash: prevHash,
      record_hash: recordHash,
    };

    // Append to file (JSONL: one record per line)
    const line = JSON.stringify(record) + "\n";
    await appendFile(this.filePath, line, "utf8");

    // Update in-memory state
    this.records.push(record);
    this.intentIndex.set(intent.id, sequence);

    return record;
  }

  getLatest(): WitnessRecord | null {
    return this.records.length > 0 ? this.records[this.records.length - 1]! : null;
  }

  getLast(n: number): WitnessRecord[] {
    const start = Math.max(0, this.records.length - n);
    return this.records.slice(start);
  }

  getByIntentId(id: string): WitnessRecord | null {
    const idx = this.intentIndex.get(id);
    return idx !== undefined ? this.records[idx] ?? null : null;
  }

  verifyChain(): { valid: boolean; brokenAt?: number; reason?: string } {
    if (this.records.length === 0) return { valid: true };

    let expectedPrevHash = GENESIS_HASH;

    for (let i = 0; i < this.records.length; i++) {
      const record = this.records[i]!;

      // Verify prev_hash chain
      if (record.prev_hash !== expectedPrevHash) {
        return {
          valid: false,
          brokenAt: i,
          reason: `prev_hash mismatch at sequence ${i}: expected ${expectedPrevHash}, got ${record.prev_hash}`,
        };
      }

      // Verify record_hash
      const computed = computeRecordHash(
        record.sequence,
        record.intent.intent_hash,
        record.verdict.verdict_hash,
        record.execution_result,
        record.prev_hash,
      );

      if (record.record_hash !== computed) {
        return {
          valid: false,
          brokenAt: i,
          reason: `record_hash mismatch at sequence ${i}: expected ${computed}, got ${record.record_hash}`,
        };
      }

      expectedPrevHash = record.record_hash;
    }

    return { valid: true };
  }

  getSequence(): number {
    return this.records.length;
  }
}

// ── Factory ──────────────────────────────────────────────────────────

export async function openWitnessLog(filePath: string): Promise<WitnessLog> {
  const log = new WitnessLogImpl(filePath);
  await log.load();
  return log;
}

export { GENESIS_HASH };


path: scripts/yaml-parse.ts
content: /**
 * Minimal YAML subset parser for governance policy files.
 *
 * Supports: scalars, string arrays (dash + bracket), nested objects (3 levels),
 * comments, quoted strings, booleans, integers.
 * Rejects: anchors, aliases, tags, flow mappings, folded/literal blocks, multi-doc.
 */

export interface PolicyFile {
  version: string;
  default_verdict: "deny" | "approve";
  rules: PolicyRule[];
  sensitive_data: SensitiveDataRule[];
}

export interface PolicyRule {
  name: string;
  match: {
    action_type?: string | string[];
    target_pattern?: string;
    skill_pattern?: string;
    tool_pattern?: string;
    data_scope?: string[];
  };
  verdict: "approve" | "deny" | "escalate";
  reason?: string;
}

export interface SensitiveDataRule {
  category: string;
  patterns: string[];
  action: "deny" | "escalate";
}

// ── Tokenizer helpers ────────────────────────────────────────────────

function stripComment(line: string): string {
  let inSingle = false;
  let inDouble = false;
  for (let i = 0; i < line.length; i++) {
    const ch = line[i]!;
    if (ch === "'" && !inDouble) inSingle = !inSingle;
    else if (ch === '"' && !inSingle) inDouble = !inDouble;
    else if (ch === "#" && !inSingle && !inDouble) return line.slice(0, i);
  }
  return line;
}

function indentOf(line: string): number {
  let n = 0;
  for (const ch of line) {
    if (ch === " ") n++;
    else if (ch === "\t") throw new Error("Tabs are not allowed in YAML indentation");
    else break;
  }
  return n;
}

function unquote(val: string): string {
  val = val.trim();
  if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
    return val.slice(1, -1);
  }
  return val;
}

function coerce(val: string): string | number | boolean {
  const trimmed = val.trim();
  if (trimmed === "true") return true;
  if (trimmed === "false") return false;
  if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10);
  return unquote(trimmed);
}

function parseInlineArray(val: string): unknown[] {
  const inner = val.slice(1, -1).trim();
  if (inner === "") return [];
  return inner.split(",").map((item) => coerce(item.trim()));
}

// ── Preprocessor: strip comments, blank lines, produce (indent, content) ──

interface Line {
  indent: number;
  content: string;
}

function preprocess(input: string): Line[] {
  const result: Line[] = [];
  for (const raw of input.split(/\r?\n/)) {
    const stripped = stripComment(raw).trimEnd();
    if (stripped.trim() === "") continue;
    result.push({ indent: indentOf(stripped), content: stripped.trim() });
  }
  return result;
}

// ── Recursive descent parser ─────────────────────────────────────────

/**
 * Parse a block of lines at a given minimum indent level into a value.
 * Returns the parsed value and the number of lines consumed.
 */
function parseBlock(lines: Line[], start: number, minIndent: number): { value: unknown; consumed: number } {
  if (start >= lines.length) return { value: {}, consumed: 0 };

  const firstLine = lines[start]!;

  // Is this an array? (first line starts with "- ")
  if (firstLine.content.startsWith("- ")) {
    return parseArray(lines, start, firstLine.indent);
  }

  // Otherwise it's a mapping
  return parseMapping(lines, start, minIndent);
}

function parseMapping(lines: Line[], start: number, minIndent: number): { value: Record<string, unknown>; consumed: number } {
  const obj: Record<string, unknown> = {};
  let pos = start;

  while (pos < lines.length) {
    const line = lines[pos]!;
    if (line.indent < minIndent) break;

    const { content } = line;

    // Must be a key: value pair
    const colonIdx = content.indexOf(":");
    if (colonIdx === -1) break;

    const key = content.slice(0, colonIdx).trim();
    const val = content.slice(colonIdx + 1).trim();

    if (val === "") {
      // Block value follows: could be nested mapping or array
      pos++;
      if (pos < lines.length && lines[pos]!.indent > line.indent) {
        const childIndent = lines[pos]!.indent;
        const child = parseBlock(lines, pos, childIndent);
        obj[key] = child.value;
        pos += child.consumed;
      }
    } else if (val.startsWith("[") && val.endsWith("]")) {
      obj[key] = parseInlineArray(val);
      pos++;
    } else {
      obj[key] = coerce(val);
      pos++;
    }
  }

  return { value: obj, consumed: pos - start };
}

function parseArray(lines: Line[], start: number, arrayIndent: number): { value: unknown[]; consumed: number } {
  const arr: unknown[] = [];
  let pos = start;

  while (pos < lines.length) {
    const line = lines[pos]!;
    if (line.indent !== arrayIndent || !line.content.startsWith("- ")) break;

    const itemContent = line.content.slice(2).trim();

    // Is this a scalar array item or start of an object?
    const colonIdx = itemContent.indexOf(":");
    if (colonIdx !== -1) {
      // Object array item: "- key: value"
      const key = itemContent.slice(0, colonIdx).trim();
      const val = itemContent.slice(colonIdx + 1).trim();
      const obj: Record<string, unknown> = {};

      if (val === "") {
        // The value is a nested block
        pos++;
        if (pos < lines.length && lines[pos]!.indent > arrayIndent + 2) {
          const child = parseBlock(lines, pos, lines[pos]!.indent);
          obj[key] = child.value;
          pos += child.consumed;
        }
      } else {
        obj[key] = coerce(val);
        pos++;
      }

      // Collect sibling key-value pairs at indent > arrayIndent (typically arrayIndent + 4)
      while (pos < lines.length) {
        const sibLine = lines[pos]!;
        if (sibLine.indent <= arrayIndent) break;
        if (sibLine.content.startsWith("- ")) break;

        const sibColonIdx = sibLine.content.indexOf(":");
        if (sibColonIdx === -1) break;

        const sibKey = sibLine.content.slice(0, sibColonIdx).trim();
        const sibVal = sibLine.content.slice(sibColonIdx + 1).trim();

        if (sibVal === "") {
          // Nested block under this sibling key
          pos++;
          if (pos < lines.length && lines[pos]!.indent > sibLine.indent) {
            const child = parseBlock(lines, pos, lines[pos]!.indent);
            obj[sibKey] = child.value;
            pos += child.consumed;
          }
        } else if (sibVal.startsWith("[") && sibVal.endsWith("]")) {
          obj[sibKey] = parseInlineArray(sibVal);
          pos++;
        } else {
          obj[sibKey] = coerce(sibVal);
          pos++;
        }
      }

      arr.push(obj);
    } else {
      // Scalar array item
      arr.push(coerce(itemContent));
      pos++;
    }
  }

  return { value: arr, consumed: pos - start };
}

// ── Public API ───────────────────────────────────────────────────────

export function parseYaml(input: string): unknown {
  const lines = preprocess(input);
  if (lines.length === 0) return {};
  const { value } = parseBlock(lines, 0, 0);
  return value;
}

// ── Policy file validator ────────────────────────────────────────────

export function parsePolicyFile(yamlContent: string): PolicyFile {
  const raw = parseYaml(yamlContent) as Record<string, unknown>;

  // Version
  if (typeof raw["version"] !== "string") {
    throw new Error("Policy file missing required 'version' field");
  }

  // Default verdict
  const dv = raw["default_verdict"];
  if (dv !== "deny" && dv !== "approve") {
    throw new Error(`Invalid default_verdict: '${String(dv)}'. Must be 'deny' or 'approve'`);
  }

  // Rules
  const rawRules = raw["rules"];
  const rules: PolicyRule[] = [];
  if (Array.isArray(rawRules)) {
    for (const r of rawRules) {
      const rule = r as Record<string, unknown>;
      if (typeof rule["name"] !== "string") {
        throw new Error("Policy rule missing required 'name' field");
      }
      const verdict = rule["verdict"];
      if (verdict !== "approve" && verdict !== "deny" && verdict !== "escalate") {
        throw new Error(`Rule '${rule["name"]}': invalid verdict '${String(verdict)}'`);
      }
      const match = (rule["match"] ?? {}) as Record<string, unknown>;
      rules.push({
        name: rule["name"] as string,
        match: {
          action_type: match["action_type"] as string | string[] | undefined,
          target_pattern: match["target_pattern"] as string | undefined,
          skill_pattern: match["skill_pattern"] as string | undefined,
          tool_pattern: match["tool_pattern"] as string | undefined,
          data_scope: match["data_scope"] as string[] | undefined,
        },
        verdict: verdict as "approve" | "deny" | "escalate",
        reason: rule["reason"] as string | undefined,
      });
    }
  }

  // Sensitive data
  const rawSensitive = raw["sensitive_data"];
  const sensitiveData: SensitiveDataRule[] = [];
  if (Array.isArray(rawSensitive)) {
    for (const s of rawSensitive) {
      const sd = s as Record<string, unknown>;
      if (typeof sd["category"] !== "string") {
        throw new Error("Sensitive data rule missing 'category'");
      }
      const action = sd["action"];
      if (action !== "deny" && action !== "escalate") {
        throw new Error(`Sensitive data '${sd["category"]}': invalid action '${String(action)}'`);
      }
      sensitiveData.push({
        category: sd["category"] as string,
        patterns: Array.isArray(sd["patterns"]) ? (sd["patterns"] as string[]) : [],
        action: action as "deny" | "escalate",
      });
    }
  }

  return {
    version: raw["version"] as string,
    default_verdict: dv,
    rules,
    sensitive_data: sensitiveData,
  };
}


path: tests/fixtures/invalid-policy.yaml
content: default_verdict: maybe
rules:
  - verdict: deny


path: tests/fixtures/valid-policy.yaml
content: version: "0.1"
default_verdict: deny
rules:
  - name: allow-read
    match:
      action_type: read
    verdict: approve
    reason: "All reads permitted"
  - name: block-delete
    match:
      action_type: delete
    verdict: deny
    reason: "Deletions blocked"
sensitive_data:
  - category: credentials
    patterns:
      - "**/*.env"
    action: deny


path: tests/governance.test.ts
content: /**
 * Integration and adversarial tests for the full governance pipeline.
 * Covers T-007, T-008, T-009, I-001..I-006, A-001..A-005 from the spec.
 */

import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { propose, promote, pipeline, resolveEscalation, verify } from "../scripts/governance.js";
import { createIntent } from "../scripts/intent.js";
import { evaluate, computeVerdictHash } from "../scripts/policy-engine.js";
import { parsePolicyFile } from "../scripts/yaml-parse.js";
import type { Verdict } from "../scripts/policy-engine.js";
import type { GovernanceConfig } from "../scripts/governance.js";
import { mkdtempSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";

// ── Helpers ──────────────────────────────────────────────────────────

function tmpDir(): string {
  return mkdtempSync(join(tmpdir(), "governance-integ-"));
}

function makeConfig(dir: string, policyContent?: string): GovernanceConfig {
  const policyPath = join(dir, "policy.yaml");
  const witnessPath = join(dir, "witness.jsonl");

  if (policyContent) {
    writeFileSync(policyPath, policyContent, "utf8");
  }

  return {
    policyPath,
    witnessPath,
    maxVerdictAge: 30,
    escalationTimeout: 300,
  };
}

const STANDARD_POLICY = `version: "0.1"
default_verdict: deny
rules:
  - name: allow-read-workspace
    match:
      action_type: read
      target_pattern: "./**"
    verdict: approve
    reason: "Workspace reads permitted"
  - name: escalate-network
    match:
      action_type: network
    verdict: escalate
    reason: "Network requires approval"
  - name: block-delete-shell
    match:
      action_type: delete
      tool_pattern: "shell.*"
    verdict: deny
    reason: "Destructive shell commands blocked"
  - name: block-profile-creation
    match:
      action_type: create
      data_scope:
        - personal
        - identity
    verdict: deny
    reason: "Profile creation not authorized"
sensitive_data:
  - category: credentials
    patterns:
      - "**/*.env"
    action: deny`;

// ── T-007: Mismatched intent_hash → PROMOTE false ────────────────────

describe("T-007: intent_hash mismatch", () => {
  it("rejects promote when intent_hash does not match verdict", () => {
    const intent = createIntent({
      skill: "test", tool: "bash", model: "claude",
      actionType: "read", target: "./file.txt",
      parameters: {}, dataScope: [],
      conversationId: "c1", messageId: "m1", userInstruction: "read",
    });

    const verdict: Verdict = {
      decision: "approve",
      intent_hash: "wrong_hash_" + "0".repeat(53),
      rule_matched: "test-rule",
      reason: "test",
      timestamp: new Date().toISOString(),
      verdict_hash: "0".repeat(64),
    };

    assert.equal(promote(intent, verdict, 30), false);
  });
});

// ── T-008: Expired verdict → PROMOTE false ───────────────────────────

describe("T-008: verdict expiry", () => {
  it("rejects verdict older than MAX_VERDICT_AGE", () => {
    const intent = createIntent({
      skill: "test", tool: "bash", model: "claude",
      actionType: "read", target: "./file.txt",
      parameters: {}, dataScope: [],
      conversationId: "c1", messageId: "m1", userInstruction: "read",
    });

    // Verdict from 60 seconds ago
    const oldTime = new Date(Date.now() - 60_000).toISOString();
    const verdict: Verdict = {
      decision: "approve",
      intent_hash: intent.intent_hash,
      rule_matched: "test-rule",
      reason: "test",
      timestamp: oldTime,
      verdict_hash: computeVerdictHash("approve", intent.intent_hash, "test-rule", oldTime),
    };

    assert.equal(promote(intent, verdict, 30), false);
  });

  it("accepts verdict within MAX_VERDICT_AGE", () => {
    const intent = createIntent({
      skill: "test", tool: "bash", model: "claude",
      actionType: "read", target: "./file.txt",
      parameters: {}, dataScope: [],
      conversationId: "c1", messageId: "m1", userInstruction: "read",
    });

    const now = new Date().toISOString();
    const verdict: Verdict = {
      decision: "approve",
      intent_hash: intent.intent_hash,
      rule_matched: "test-rule",
      reason: "test",
      timestamp: now,
      verdict_hash: computeVerdictHash("approve", intent.intent_hash, "test-rule", now),
    };

    assert.equal(promote(intent, verdict, 30), true);
  });
});

// ── I-001: Shell rm -rf blocked ──────────────────────────────────────

describe("I-001: destructive shell blocked", () => {
  it("blocks shell delete with standard policy", async () => {
    const dir = tmpDir();
    const config = makeConfig(dir, STANDARD_POLICY);

    const result = await pipeline({
      skill: "shell",
      tool: "rm",
      model: "claude",
      actionType: "delete",
      target: "/home/user/important",
      parameters: { flags: "-rf" },
      dataScope: [],
      conversationId: "c1",
      messageId: "m1",
      userInstruction: "clean up files",
    }, config);

    assert.equal(result.verdict.decision, "deny");
    assert.equal(result.promoted, false);
    assert.equal(result.record.execution_result.status, "blocked");
  });
});

// ── I-002: Local file read approved ──────────────────────────────────

describe("I-002: local read approved", () => {
  it("approves workspace file read with standard policy", async () => {
    const dir = tmpDir();
    const config = makeConfig(dir, STANDARD_POLICY);

    const result = await pipeline({
      skill: "editor",
      tool: "read",
      model: "claude",
      actionType: "read",
      target: "./src/main.ts",
      parameters: {},
      dataScope: [],
      conversationId: "c1",
      messageId: "m1",
      userInstruction: "read the main file",
    }, config);

    assert.equal(result.verdict.decision, "approve");
    assert.equal(result.promoted, true);
    assert.equal(result.record.execution_result.status, "executed");
  });
});

// ── I-003: External HTTP escalated ───────────────────────────────────

describe("I-003: network escalated", () => {
  it("escalates external network request", async () => {
    const dir = tmpDir();
    const config = makeConfig(dir, STANDARD_POLICY);

    const result = await pipeline({
      skill: "browser",
      tool: "fetch",
      model: "claude",
      actionType: "network",
      target: "https://api.example.com",
      parameters: {},
      dataScope: [],
      conversationId: "c1",
      messageId: "m1",
      userInstruction: "fetch data",
    }, config);

    assert.equal(result.verdict.decision, "escalate");
    assert.equal(result.promoted, false);
    assert.equal(result.record.execution_result.status, "escalated");
  });
});

// ── I-004: Profile creation blocked by data_scope ────────────────────

describe("I-004: profile creation blocked", () => {
  it("blocks create action with personal data scope", async () => {
    const dir = tmpDir();
    const config = makeConfig(dir, STANDARD_POLICY);

    const result = await pipeline({
      skill: "moltmatch",
      tool: "create-profile",
      model: "claude",
      actionType: "create",
      target: "api.moltmatch.com",
      parameters: { name: "Test User" },
      dataScope: ["personal", "identity"],
      conversationId: "c1",
      messageId: "m1",
      userInstruction: "create a dating profile",
    }, config);

    assert.equal(result.verdict.decision, "deny");
    assert.equal(result.promoted, false);
  });
});

// ── I-005: Policy hot-reload ─────────────────────────────────────────

describe("I-005: policy update", () => {
  it("uses updated policy after file change", async () => {
    const dir = tmpDir();
    const config = makeConfig(dir, STANDARD_POLICY);

    // First call: network is escalated
    const result1 = await pipeline({
      skill: "browser", tool: "fetch", model: "claude",
      actionType: "network", target: "https://api.example.com",
      parameters: {}, dataScope: [],
      conversationId: "c1", messageId: "m1", userInstruction: "fetch",
    }, config);
    assert.equal(result1.verdict.decision, "escalate");

    // Update policy to approve network
    const newPolicy = `version: "0.1"
default_verdict: deny
rules:
  - name: allow-network
    match:
      action_type: network
    verdict: approve
    reason: "Network now approved"`;
    writeFileSync(config.policyPath, newPolicy, "utf8");

    // Second call: network should be approved
    const result2 = await pipeline({
      skill: "browser", tool: "fetch", model: "claude",
      actionType: "network", target: "https://api.example.com",
      parameters: {}, dataScope: [],
      conversationId: "c1", messageId: "m1", userInstruction: "fetch",
    }, config);
    assert.equal(result2.verdict.decision, "approve");
  });
});

// ── I-006: Missing policy → fail-closed ──────────────────────────────

describe("I-006: fail-closed on missing policy", () => {
  it("denies everything when policy file is missing", async () => {
    const dir = tmpDir();
    const config = makeConfig(dir); // No policy file written

    const result = await pipeline({
      skill: "test", tool: "bash", model: "claude",
      actionType: "read", target: "./README.md",
      parameters: {}, dataScope: [],
      conversationId: "c1", messageId: "m1", userInstruction: "read",
    }, config);

    assert.equal(result.verdict.decision, "deny");
    assert.equal(result.promoted, false);
  });
});

// ── A-001: Spoofed intent_hash detected ──────────────────────────────

describe("A-001: spoofed intent_hash", () => {
  it("recomputes hash and detects mismatch", () => {
    const raw = {
      id: "fake-id",
      timestamp: "2026-01-01T00:00:00Z",
      source: { skill: "evil", tool: "hack", model: "gpt" },
      action: { type: "execute", target: "/bin/sh", parameters: {}, data_scope: [] },
      context: { conversation_id: "c", message_id: "m", user_instruction: "hack" },
      intent_hash: "0000000000000000000000000000000000000000000000000000000000000000",
    };

    // propose() should reject or recompute
    // When passed a full intent with wrong hash, validation should fail
    assert.throws(() => propose(raw), /Hash does not match/);
  });
});

// ── A-003: Agent attempts to modify policy file ──────────────────────

describe("A-003: policy file protection", () => {
  it("write to policy path is blocked by sensitive data rule", () => {
    // Standard policy should deny writes to governance config
    const policy = parsePolicyFile(STANDARD_POLICY);
    const intent = createIntent({
      skill: "editor", tool: "write", model: "claude",
      actionType: "write", target: "~/.openclaw/governance/policy.yaml",
      parameters: {}, dataScope: [],
      conversationId: "c1", messageId: "m1", userInstruction: "modify policy",
    });

    const verdict = evaluate(intent, policy, new Date().toISOString());
    // Should be denied by default (no write rule in standard policy matches this path)
    assert.equal(verdict.decision, "deny");
  });
});

// ── A-005: Replay attack prevention ──────────────────────────────────

describe("A-005: replay attack", () => {
  it("rejects resubmitting approved verdict for different intent", () => {
    const intent1 = createIntent({
      skill: "test", tool: "bash", model: "claude",
      actionType: "read", target: "./safe.txt",
      parameters: {}, dataScope: [],
      conversationId: "c1", messageId: "m1", userInstruction: "read",
    });

    const intent2 = createIntent({
      skill: "evil", tool: "rm", model: "claude",
      actionType: "delete", target: "/etc/passwd",
      parameters: {}, dataScope: [],
      conversationId: "c1", messageId: "m2", userInstruction: "delete",
    });

    const now = new Date().toISOString();
    const verdict: Verdict = {
      decision: "approve",
      intent_hash: intent1.intent_hash,
      rule_matched: "allow-reads",
      reason: "Safe read",
      timestamp: now,
      verdict_hash: computeVerdictHash("approve", intent1.intent_hash, "allow-reads", now),
    };

    // Verdict for intent1 should not promote intent2
    assert.equal(promote(intent1, verdict, 30), true);  // Original: works
    assert.equal(promote(intent2, verdict, 30), false);  // Replay: rejected
  });
});

// ── Escalation resolution ────────────────────────────────────────────

describe("escalation resolution", () => {
  it("resolves escalation with approve → user_approved", async () => {
    const dir = tmpDir();
    const config = makeConfig(dir, STANDARD_POLICY);

    // Create an escalated action
    const result = await pipeline({
      skill: "browser", tool: "fetch", model: "claude",
      actionType: "network", target: "https://api.example.com",
      parameters: {}, dataScope: [],
      conversationId: "c1", messageId: "m1", userInstruction: "fetch",
    }, config);

    assert.equal(result.record.execution_result.status, "escalated");

    // Resolve with approve
    const resolved = await resolveEscalation(result.intent.id, "approve", config);
    assert.equal(resolved.execution_result.status, "user_approved");
  });

  it("resolves escalation with deny → user_denied", async () => {
    const dir = tmpDir();
    const config = makeConfig(dir, STANDARD_POLICY);

    const result = await pipeline({
      skill: "browser", tool: "fetch", model: "claude",
      actionType: "network", target: "https://api.example.com",
      parameters: {}, dataScope: [],
      conversationId: "c1", messageId: "m1", userInstruction: "fetch",
    }, config);

    const resolved = await resolveEscalation(result.intent.id, "deny", config);
    assert.equal(resolved.execution_result.status, "user_denied");
  });
});

// ── Witness chain integrity across pipeline operations ───────────────

describe("witness chain integrity", () => {
  it("maintains valid chain across 10 operations", async () => {
    const dir = tmpDir();
    const config = makeConfig(dir, STANDARD_POLICY);

    for (let i = 0; i < 10; i++) {
      await pipeline({
        skill: "test", tool: "read", model: "claude",
        actionType: "read", target: `./file${i}.txt`,
        parameters: {}, dataScope: [],
        conversationId: "c1", messageId: `m${i}`, userInstruction: `read file ${i}`,
      }, config);
    }

    const result = await verify(config);
    assert.equal(result.valid, true);
    assert.equal(result.totalRecords, 10);
  });
});


path: tests/intent.test.ts
content: import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
  createIntent,
  canonicalize,
  computeHash,
  computeIntentHash,
  validateIntent,
} from "../scripts/intent.js";
import type { CreateIntentParams } from "../scripts/intent.js";

const SAMPLE_PARAMS: CreateIntentParams = {
  skill: "browser-automation",
  tool: "fetch",
  model: "claude-opus-4-20250514",
  actionType: "network",
  target: "https://api.example.com/data",
  parameters: { method: "GET" },
  dataScope: ["personal"],
  conversationId: "conv-123",
  messageId: "msg-456",
  userInstruction: "Fetch the user data from the API",
};

describe("canonicalize", () => {
  it("sorts object keys alphabetically at all levels", () => {
    const result = canonicalize({ z: 1, a: 2, m: { z: 3, a: 4 } });
    assert.equal(result, '{"a":2,"m":{"a":4,"z":3},"z":1}');
  });

  it("produces minified JSON (no whitespace)", () => {
    const result = canonicalize({ key: "value", arr: [1, 2, 3] });
    assert.ok(!result.includes(" "));
    assert.ok(!result.includes("\n"));
  });

  it("handles nested objects and arrays", () => {
    const result = canonicalize({ a: [{ c: 1, b: 2 }] });
    assert.equal(result, '{"a":[{"b":2,"c":1}]}');
  });

  it("is idempotent", () => {
    const obj = { z: 1, a: { y: 2, b: [3, 4] } };
    assert.equal(canonicalize(obj), canonicalize(obj));
  });

  it("handles null and primitives", () => {
    assert.equal(canonicalize(null), "null");
    assert.equal(canonicalize("hello"), '"hello"');
    assert.equal(canonicalize(42), "42");
    assert.equal(canonicalize(true), "true");
  });
});

describe("computeHash", () => {
  it("produces a 64-character hex string (SHA-256)", () => {
    const hash = computeHash("test data");
    assert.equal(hash.length, 64);
    assert.match(hash, /^[0-9a-f]{64}$/);
  });

  it("is deterministic", () => {
    assert.equal(computeHash("same input"), computeHash("same input"));
  });

  it("changes with different input", () => {
    assert.notEqual(computeHash("input1"), computeHash("input2"));
  });
});

describe("createIntent", () => {
  it("generates a valid UUID v4 id", () => {
    const intent = createIntent(SAMPLE_PARAMS);
    assert.match(intent.id, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
  });

  it("generates ISO 8601 timestamp", () => {
    const intent = createIntent(SAMPLE_PARAMS);
    const parsed = new Date(intent.timestamp);
    assert.ok(!isNaN(parsed.getTime()), "Timestamp should be valid ISO 8601");
    assert.ok(intent.timestamp.includes("T"), "Should contain T separator");
  });

  it("computes SHA-256 intent_hash from canonical serialization", () => {
    const intent = createIntent(SAMPLE_PARAMS);
    assert.match(intent.intent_hash, /^[0-9a-f]{64}$/);

    // Verify the hash matches recomputation
    const { intent_hash: _hash, ...rest } = intent;
    const recomputed = computeIntentHash(rest);
    assert.equal(intent.intent_hash, recomputed);
  });

  it("produces different hashes for different inputs", () => {
    const intent1 = createIntent(SAMPLE_PARAMS);
    const intent2 = createIntent({ ...SAMPLE_PARAMS, target: "https://other.com" });
    assert.notEqual(intent1.intent_hash, intent2.intent_hash);
  });

  it("maps all fields correctly", () => {
    const intent = createIntent(SAMPLE_PARAMS);
    assert.equal(intent.source.skill, "browser-automation");
    assert.equal(intent.source.tool, "fetch");
    assert.equal(intent.source.model, "claude-opus-4-20250514");
    assert.equal(intent.action.type, "network");
    assert.equal(intent.action.target, "https://api.example.com/data");
    assert.deepEqual(intent.action.parameters, { method: "GET" });
    assert.deepEqual(intent.action.data_scope, ["personal"]);
    assert.equal(intent.context.conversation_id, "conv-123");
    assert.equal(intent.context.message_id, "msg-456");
    assert.equal(intent.context.user_instruction, "Fetch the user data from the API");
  });
});

describe("validateIntent", () => {
  it("accepts valid ActionIntent", () => {
    const intent = createIntent(SAMPLE_PARAMS);
    const result = validateIntent(intent);
    assert.equal(result.valid, true);
  });

  it("rejects null input", () => {
    const result = validateIntent(null);
    assert.equal(result.valid, false);
  });

  it("rejects missing required fields", () => {
    const result = validateIntent({ id: "test" });
    assert.equal(result.valid, false);
    if (!result.valid) {
      assert.ok(result.errors.length > 0);
    }
  });

  it("rejects invalid action_type", () => {
    const intent = createIntent(SAMPLE_PARAMS);
    const modified = { ...intent, action: { ...intent.action, type: "invalid" } };
    const result = validateIntent(modified);
    assert.equal(result.valid, false);
  });

  it("rejects non-string target", () => {
    const intent = createIntent(SAMPLE_PARAMS);
    const modified = { ...intent, action: { ...intent.action, target: 42 } };
    const result = validateIntent(modified);
    assert.equal(result.valid, false);
  });

  it("rejects tampered intent_hash", () => {
    const intent = createIntent(SAMPLE_PARAMS);
    const tampered = { ...intent, intent_hash: "0".repeat(64) };
    const result = validateIntent(tampered);
    assert.equal(result.valid, false);
    if (!result.valid) {
      assert.ok(result.errors.some((e) => e.field === "intent_hash"));
    }
  });

  it("rejects missing source object", () => {
    const result = validateIntent({
      id: "test",
      timestamp: new Date().toISOString(),
      action: { type: "read", target: ".", parameters: {}, data_scope: [] },
      context: { conversation_id: "c", message_id: "m", user_instruction: "u" },
      intent_hash: "abc",
    });
    assert.equal(result.valid, false);
  });
});


path: tests/policy-engine.test.ts
content: import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { evaluate, matchRule, matchGlob, checkSensitiveData, computeVerdictHash } from "../scripts/policy-engine.js";
import { createIntent } from "../scripts/intent.js";
import type { ActionIntent, CreateIntentParams } from "../scripts/intent.js";
import type { PolicyFile, PolicyRule } from "../scripts/yaml-parse.js";

// ── Helpers ──────────────────────────────────────────────────────────

const NOW = "2026-02-26T12:00:00.000Z";

function makeIntent(overrides: Partial<CreateIntentParams> = {}): ActionIntent {
  return createIntent({
    skill: "test-skill",
    tool: "test-tool",
    model: "claude",
    actionType: "read",
    target: "./src/main.ts",
    parameters: {},
    dataScope: [],
    conversationId: "c1",
    messageId: "m1",
    userInstruction: "test",
    ...overrides,
  });
}

function makePolicy(overrides: Partial<PolicyFile> = {}): PolicyFile {
  return {
    version: "0.1",
    default_verdict: "deny",
    rules: [],
    sensitive_data: [],
    ...overrides,
  };
}

// ── T-001: No matching rule, default deny ────────────────────────────

describe("T-001: default deny", () => {
  it("returns deny when no rules match and default is deny", () => {
    const intent = makeIntent();
    const policy = makePolicy({ default_verdict: "deny" });
    const verdict = evaluate(intent, policy, NOW);
    assert.equal(verdict.decision, "deny");
    assert.equal(verdict.rule_matched, "__default__");
  });
});

// ── T-002: No matching rule, default approve ─────────────────────────

describe("T-002: default approve", () => {
  it("returns approve when no rules match and default is approve", () => {
    const intent = makeIntent();
    const policy = makePolicy({ default_verdict: "approve" });
    const verdict = evaluate(intent, policy, NOW);
    assert.equal(verdict.decision, "approve");
    assert.equal(verdict.rule_matched, "__default__");
  });
});

// ── T-003: Matching approve rule ─────────────────────────────────────

describe("T-003: approve rule", () => {
  it("returns approve with correct rule_matched", () => {
    const intent = makeIntent({ actionType: "read", target: "./README.md" });
    const policy = makePolicy({
      rules: [{
        name: "allow-reads",
        match: { action_type: "read" },
        verdict: "approve",
        reason: "Reads are safe",
      }],
    });
    const verdict = evaluate(intent, policy, NOW);
    assert.equal(verdict.decision, "approve");
    assert.equal(verdict.rule_matched, "allow-reads");
    assert.equal(verdict.reason, "Reads are safe");
  });
});

// ── T-004: Matching deny rule ────────────────────────────────────────

describe("T-004: deny rule", () => {
  it("returns deny with correct reason", () => {
    const intent = makeIntent({ actionType: "delete", target: "/tmp/file.txt" });
    const policy = makePolicy({
      rules: [{
        name: "block-delete",
        match: { action_type: "delete" },
        verdict: "deny",
        reason: "Deletion not allowed",
      }],
    });
    const verdict = evaluate(intent, policy, NOW);
    assert.equal(verdict.decision, "deny");
    assert.equal(verdict.rule_matched, "block-delete");
    assert.equal(verdict.reason, "Deletion not allowed");
  });
});

// ── T-005: Matching escalate rule ────────────────────────────────────

describe("T-005: escalate rule", () => {
  it("returns escalate for matching rule", () => {
    const intent = makeIntent({ actionType: "network", target: "https://api.example.com" });
    const policy = makePolicy({
      rules: [{
        name: "escalate-network",
        match: { action_type: "network" },
        verdict: "escalate",
        reason: "Network requires approval",
      }],
    });
    const verdict = evaluate(intent, policy, NOW);
    assert.equal(verdict.decision, "escalate");
    assert.equal(verdict.rule_matched, "escalate-network");
  });
});

// ── First-match-wins semantics ───────────────────────────────────────

describe("first-match-wins", () => {
  it("returns the first matching rule, not later ones", () => {
    const intent = makeIntent({ actionType: "read" });
    const policy = makePolicy({
      rules: [
        { name: "deny-reads", match: { action_type: "read" }, verdict: "deny" },
        { name: "allow-reads", match: { action_type: "read" }, verdict: "approve" },
      ],
    });
    const verdict = evaluate(intent, policy, NOW);
    assert.equal(verdict.decision, "deny");
    assert.equal(verdict.rule_matched, "deny-reads");
  });
});

// ── matchGlob ────────────────────────────────────────────────────────

describe("matchGlob", () => {
  it("matches single-segment wildcard", () => {
    assert.ok(matchGlob("./src/main.ts", "./**"));
  });

  it("matches recursive wildcard", () => {
    assert.ok(matchGlob("./src/deep/nested/file.ts", "./**"));
  });

  it("rejects non-matching patterns", () => {
    assert.ok(!matchGlob("/etc/passwd", "./**"));
  });

  it("handles negation pattern", () => {
    assert.ok(matchGlob("https://api.example.com", "!*.local"));
    assert.ok(!matchGlob("server.local", "!*.local"));
  });
});

// ── matchRule ────────────────────────────────────────────────────────

describe("matchRule", () => {
  it("matches action_type exact", () => {
    const intent = makeIntent({ actionType: "write" });
    const rule: PolicyRule = { name: "test", match: { action_type: "write" }, verdict: "deny" };
    assert.ok(matchRule(intent, rule));
  });

  it("matches action_type array (any-of)", () => {
    const intent = makeIntent({ actionType: "delete" });
    const rule: PolicyRule = {
      name: "test",
      match: { action_type: ["write", "delete"] },
      verdict: "deny",
    };
    assert.ok(matchRule(intent, rule));
  });

  it("fails when action_type doesn't match", () => {
    const intent = makeIntent({ actionType: "read" });
    const rule: PolicyRule = { name: "test", match: { action_type: "write" }, verdict: "deny" };
    assert.ok(!matchRule(intent, rule));
  });

  it("matches data_scope intersection", () => {
    const intent = makeIntent({ dataScope: ["personal", "financial"] });
    const rule: PolicyRule = {
      name: "test",
      match: { data_scope: ["personal", "identity"] },
      verdict: "deny",
    };
    assert.ok(matchRule(intent, rule));
  });

  it("fails when data_scope has no intersection", () => {
    const intent = makeIntent({ dataScope: ["financial"] });
    const rule: PolicyRule = {
      name: "test",
      match: { data_scope: ["personal", "identity"] },
      verdict: "deny",
    };
    assert.ok(!matchRule(intent, rule));
  });

  it("matches when rule has no data_scope (wildcard)", () => {
    const intent = makeIntent({ dataScope: ["personal"] });
    const rule: PolicyRule = { name: "test", match: { action_type: "read" }, verdict: "approve" };
    assert.ok(matchRule(intent, rule));
  });

  it("matches target_pattern glob", () => {
    const intent = makeIntent({ target: "./src/app.ts" });
    const rule: PolicyRule = {
      name: "test",
      match: { target_pattern: "./**" },
      verdict: "approve",
    };
    assert.ok(matchRule(intent, rule));
  });
});

// ── checkSensitiveData ───────────────────────────────────────────────

describe("checkSensitiveData", () => {
  const sensitiveRules = [
    { category: "credentials", patterns: ["**/*.env", "**/.ssh/**"], action: "deny" as const },
    { category: "financial", patterns: ["**/*wallet*"], action: "escalate" as const },
  ];

  it("detects credential file patterns", () => {
    const intent = makeIntent({ target: "./config/.env" });
    const result = checkSensitiveData(intent, sensitiveRules);
    assert.ok(result !== null);
    assert.equal(result!.category, "credentials");
  });

  it("detects ssh key patterns", () => {
    const intent = makeIntent({ target: "/home/user/.ssh/id_rsa" });
    const result = checkSensitiveData(intent, sensitiveRules);
    assert.ok(result !== null);
    assert.equal(result!.category, "credentials");
  });

  it("detects data_scope category overlap", () => {
    const intent = makeIntent({ dataScope: ["financial"] });
    const result = checkSensitiveData(intent, sensitiveRules);
    assert.ok(result !== null);
    assert.equal(result!.category, "financial");
  });

  it("returns null for non-sensitive targets", () => {
    const intent = makeIntent({ target: "./src/main.ts" });
    const result = checkSensitiveData(intent, sensitiveRules);
    assert.equal(result, null);
  });
});

// ── Fail-closed behavior ─────────────────────────────────────────────

describe("fail-closed", () => {
  it("returns deny on empty policy (no rules)", () => {
    const intent = makeIntent();
    const policy = makePolicy({ default_verdict: "deny", rules: [] });
    const verdict = evaluate(intent, policy, NOW);
    assert.equal(verdict.decision, "deny");
  });
});

// ── Determinism ──────────────────────────────────────────────────────

describe("determinism", () => {
  it("same intent + same policy = same verdict hash", () => {
    const intent = makeIntent();
    const policy = makePolicy({
      rules: [{ name: "test", match: { action_type: "read" }, verdict: "approve" }],
    });

    const v1 = evaluate(intent, policy, NOW);
    const v2 = evaluate(intent, policy, NOW);
    assert.equal(v1.verdict_hash, v2.verdict_hash);
    assert.equal(v1.decision, v2.decision);
    assert.equal(v1.rule_matched, v2.rule_matched);
  });

  it("verdict_hash is deterministic for identical inputs (100 iterations)", () => {
    const intent = makeIntent();
    const policy = makePolicy();
    const first = evaluate(intent, policy, NOW);
    for (let i = 0; i < 100; i++) {
      const v = evaluate(intent, policy, NOW);
      assert.equal(v.verdict_hash, first.verdict_hash);
    }
  });
});

// ── Verdict hash computation ─────────────────────────────────────────

describe("computeVerdictHash", () => {
  it("produces consistent hash for same inputs", () => {
    const h1 = computeVerdictHash("deny", "abc123", "rule1", NOW);
    const h2 = computeVerdictHash("deny", "abc123", "rule1", NOW);
    assert.equal(h1, h2);
  });

  it("produces different hash for different inputs", () => {
    const h1 = computeVerdictHash("deny", "abc123", "rule1", NOW);
    const h2 = computeVerdictHash("approve", "abc123", "rule1", NOW);
    assert.notEqual(h1, h2);
  });
});

// ── Sensitive data takes priority over rules ─────────────────────────

describe("sensitive data priority", () => {
  it("sensitive data match overrides regular approve rule", () => {
    const intent = makeIntent({ target: "./secrets/.env", actionType: "read" });
    const policy = makePolicy({
      rules: [{ name: "allow-all-reads", match: { action_type: "read" }, verdict: "approve" }],
      sensitive_data: [{ category: "credentials", patterns: ["**/*.env"], action: "deny" }],
    });
    const verdict = evaluate(intent, policy, NOW);
    assert.equal(verdict.decision, "deny");
    assert.ok(verdict.rule_matched.includes("__sensitive_data__"));
  });
});


path: tests/witness.test.ts
content: import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { openWitnessLog, computeRecordHash, GENESIS_HASH } from "../scripts/witness.js";
import { createIntent } from "../scripts/intent.js";
import { evaluate } from "../scripts/policy-engine.js";
import type { PolicyFile } from "../scripts/yaml-parse.js";
import { mkdtempSync, writeFileSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";

// ── Helpers ──────────────────────────────────────────────────────────

const NOW = "2026-02-26T12:00:00.000Z";

function tmpWitnessPath(): string {
  const dir = mkdtempSync(join(tmpdir(), "governance-test-"));
  return join(dir, "witness.jsonl");
}

function sampleIntent() {
  return createIntent({
    skill: "test",
    tool: "bash",
    model: "claude",
    actionType: "read",
    target: "./README.md",
    parameters: {},
    dataScope: [],
    conversationId: "c1",
    messageId: "m1",
    userInstruction: "read the readme",
  });
}

function sampleVerdict(intent: ReturnType<typeof sampleIntent>) {
  const policy: PolicyFile = {
    version: "0.1",
    default_verdict: "approve",
    rules: [],
    sensitive_data: [],
  };
  return evaluate(intent, policy, NOW);
}

// ── Tests ────────────────────────────────────────────────────────────

describe("openWitnessLog", () => {
  it("creates witness.jsonl if it does not exist", async () => {
    const path = tmpWitnessPath();
    const log = await openWitnessLog(path);
    assert.equal(log.getSequence(), 0);
    assert.equal(log.getLatest(), null);
  });

  it("opens existing file and rebuilds in-memory index", async () => {
    const path = tmpWitnessPath();

    // Write some records
    const log1 = await openWitnessLog(path);
    const intent = sampleIntent();
    const verdict = sampleVerdict(intent);
    await log1.append(intent, verdict, "executed");

    // Reopen and verify
    const log2 = await openWitnessLog(path);
    assert.equal(log2.getSequence(), 1);
    const latest = log2.getLatest();
    assert.ok(latest !== null);
    assert.equal(latest!.intent.id, intent.id);
  });

  it("recovers from trailing garbage", async () => {
    const path = tmpWitnessPath();

    // Write a valid record then append garbage
    const log1 = await openWitnessLog(path);
    const intent = sampleIntent();
    const verdict = sampleVerdict(intent);
    await log1.append(intent, verdict, "executed");

    // Append garbage
    const content = readFileSync(path, "utf8");
    writeFileSync(path, content + '{"broken json\n', "utf8");

    // Should recover
    const log2 = await openWitnessLog(path);
    assert.equal(log2.getSequence(), 1);
  });
});

describe("append", () => {
  it("writes valid JSONL line", async () => {
    const path = tmpWitnessPath();
    const log = await openWitnessLog(path);
    const intent = sampleIntent();
    const verdict = sampleVerdict(intent);

    await log.append(intent, verdict, "executed");

    const content = readFileSync(path, "utf8").trim();
    const record = JSON.parse(content);
    assert.equal(record.sequence, 0);
    assert.equal(record.intent.id, intent.id);
  });

  it("increments sequence number", async () => {
    const path = tmpWitnessPath();
    const log = await openWitnessLog(path);

    for (let i = 0; i < 3; i++) {
      const intent = sampleIntent();
      const verdict = sampleVerdict(intent);
      const record = await log.append(intent, verdict, "executed");
      assert.equal(record.sequence, i);
    }
    assert.equal(log.getSequence(), 3);
  });

  it("sets genesis prev_hash correctly", async () => {
    const path = tmpWitnessPath();
    const log = await openWitnessLog(path);
    const intent = sampleIntent();
    const verdict = sampleVerdict(intent);

    const record = await log.append(intent, verdict, "executed");
    assert.equal(record.prev_hash, GENESIS_HASH);
  });

  it("chains prev_hash to previous record_hash", async () => {
    const path = tmpWitnessPath();
    const log = await openWitnessLog(path);

    const intent1 = sampleIntent();
    const verdict1 = sampleVerdict(intent1);
    const record1 = await log.append(intent1, verdict1, "executed");

    const intent2 = sampleIntent();
    const verdict2 = sampleVerdict(intent2);
    const record2 = await log.append(intent2, verdict2, "blocked");

    assert.equal(record2.prev_hash, record1.record_hash);
  });

  it("computes record_hash correctly", async () => {
    const path = tmpWitnessPath();
    const log = await openWitnessLog(path);
    const intent = sampleIntent();
    const verdict = sampleVerdict(intent);

    const record = await log.append(intent, verdict, "executed");

    const expected = computeRecordHash(
      record.sequence,
      record.intent.intent_hash,
      record.verdict.verdict_hash,
      record.execution_result,
      record.prev_hash,
    );
    assert.equal(record.record_hash, expected);
  });
});

describe("verifyChain", () => {
  it("verifies valid chain returns { valid: true }", async () => {
    const path = tmpWitnessPath();
    const log = await openWitnessLog(path);

    for (let i = 0; i < 5; i++) {
      const intent = sampleIntent();
      const verdict = sampleVerdict(intent);
      await log.append(intent, verdict, "executed");
    }

    const result = log.verifyChain();
    assert.equal(result.valid, true);
  });

  it("handles empty log as valid", async () => {
    const path = tmpWitnessPath();
    const log = await openWitnessLog(path);
    const result = log.verifyChain();
    assert.equal(result.valid, true);
  });

  // T-010: Tampered record detection
  it("detects tampered record", async () => {
    const path = tmpWitnessPath();
    const log = await openWitnessLog(path);

    for (let i = 0; i < 3; i++) {
      const intent = sampleIntent();
      const verdict = sampleVerdict(intent);
      await log.append(intent, verdict, "executed");
    }

    // Tamper with the file: modify a record's hash
    const content = readFileSync(path, "utf8");
    const lines = content.trim().split("\n");
    const record1 = JSON.parse(lines[1]!);
    record1.record_hash = "0".repeat(64);
    lines[1] = JSON.stringify(record1);
    writeFileSync(path, lines.join("\n") + "\n", "utf8");

    // Reopen and verify
    const log2 = await openWitnessLog(path);
    const result = log2.verifyChain();
    assert.equal(result.valid, false);
    assert.equal(result.brokenAt, 1);
  });
});

describe("getLast", () => {
  it("returns last N records in order", async () => {
    const path = tmpWitnessPath();
    const log = await openWitnessLog(path);

    for (let i = 0; i < 5; i++) {
      const intent = sampleIntent();
      const verdict = sampleVerdict(intent);
      await log.append(intent, verdict, "executed");
    }

    const last3 = log.getLast(3);
    assert.equal(last3.length, 3);
    assert.equal(last3[0]!.sequence, 2);
    assert.equal(last3[1]!.sequence, 3);
    assert.equal(last3[2]!.sequence, 4);
  });

  it("returns all records if N > total", async () => {
    const path = tmpWitnessPath();
    const log = await openWitnessLog(path);

    const intent = sampleIntent();
    const verdict = sampleVerdict(intent);
    await log.append(intent, verdict, "executed");

    const all = log.getLast(100);
    assert.equal(all.length, 1);
  });
});

describe("getByIntentId", () => {
  it("finds record by intent id", async () => {
    const path = tmpWitnessPath();
    const log = await openWitnessLog(path);

    const intent = sampleIntent();
    const verdict = sampleVerdict(intent);
    await log.append(intent, verdict, "executed");

    const found = log.getByIntentId(intent.id);
    assert.ok(found !== null);
    assert.equal(found!.intent.id, intent.id);
  });

  it("returns null for unknown id", async () => {
    const path = tmpWitnessPath();
    const log = await openWitnessLog(path);

    const found = log.getByIntentId("nonexistent");
    assert.equal(found, null);
  });
});


path: tests/yaml-parse.test.ts
content: import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { parseYaml, parsePolicyFile } from "../scripts/yaml-parse.js";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

// Resolve project root relative to this test file
const __dirname_test = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname_test, "..");

describe("parseYaml", () => {
  it("parses scalar key-value pairs", () => {
    const result = parseYaml('name: foo\nversion: "0.1"') as Record<string, unknown>;
    assert.equal(result["name"], "foo");
    assert.equal(result["version"], "0.1");
  });

  it("parses integer values", () => {
    const result = parseYaml("count: 42\nnegative: -7") as Record<string, unknown>;
    assert.equal(result["count"], 42);
    assert.equal(result["negative"], -7);
  });

  it("parses boolean values", () => {
    const result = parseYaml("enabled: true\ndisabled: false") as Record<string, unknown>;
    assert.equal(result["enabled"], true);
    assert.equal(result["disabled"], false);
  });

  it("parses string arrays with dash syntax", () => {
    const result = parseYaml("items:\n  - foo\n  - bar\n  - baz") as Record<string, unknown>;
    assert.deepEqual(result["items"], ["foo", "bar", "baz"]);
  });

  it("parses inline arrays with bracket syntax", () => {
    const result = parseYaml('tags: [a, b, c]') as Record<string, unknown>;
    assert.deepEqual(result["tags"], ["a", "b", "c"]);
  });

  it("parses nested objects via indentation", () => {
    const yaml = "parent:\n  child1: val1\n  child2: val2";
    const result = parseYaml(yaml) as Record<string, unknown>;
    assert.deepEqual(result["parent"], { child1: "val1", child2: "val2" });
  });

  it("strips comments", () => {
    const result = parseYaml("name: foo # this is a comment") as Record<string, unknown>;
    assert.equal(result["name"], "foo");
  });

  it("handles quoted strings (single and double)", () => {
    const result = parseYaml(`single: 'hello world'\ndouble: "hello world"`) as Record<string, unknown>;
    assert.equal(result["single"], "hello world");
    assert.equal(result["double"], "hello world");
  });

  it("handles empty input as empty object", () => {
    const result = parseYaml("");
    assert.deepEqual(result, {});
  });

  it("handles empty lines between entries", () => {
    const result = parseYaml("a: 1\n\nb: 2\n\nc: 3") as Record<string, unknown>;
    assert.equal(result["a"], 1);
    assert.equal(result["b"], 2);
    assert.equal(result["c"], 3);
  });

  it("rejects tabs in indentation", () => {
    assert.throws(() => parseYaml("name:\n\tchild: val"), /Tabs/);
  });
});

describe("parsePolicyFile", () => {
  it("parses minimal.yaml correctly", () => {
    const content = readFileSync(join(projectRoot, "policies", "minimal.yaml"), "utf8");
    const policy = parsePolicyFile(content);
    assert.equal(policy.version, "0.1");
    assert.equal(policy.default_verdict, "approve");
    assert.ok(policy.rules.length >= 1);
    assert.equal(policy.rules[0]!.name, "block-credentials");
    assert.equal(policy.rules[0]!.verdict, "deny");
  });

  it("parses standard.yaml correctly", () => {
    const content = readFileSync(join(projectRoot, "policies", "standard.yaml"), "utf8");
    const policy = parsePolicyFile(content);
    assert.equal(policy.version, "0.1");
    assert.equal(policy.default_verdict, "deny");
    assert.ok(policy.rules.length >= 5);
    // First rule should be allow-read-local
    assert.equal(policy.rules[0]!.name, "allow-read-local");
    assert.equal(policy.rules[0]!.verdict, "approve");
  });

  it("parses strict.yaml correctly", () => {
    const content = readFileSync(join(projectRoot, "policies", "strict.yaml"), "utf8");
    const policy = parsePolicyFile(content);
    assert.equal(policy.version, "0.1");
    assert.equal(policy.default_verdict, "deny");
    assert.ok(policy.rules.length >= 1);
    assert.equal(policy.rules[0]!.name, "allow-read-workspace");
  });

  it("rejects missing version field", () => {
    assert.throws(
      () => parsePolicyFile("default_verdict: deny\nrules:\n  - name: test\n    verdict: deny"),
      /version/,
    );
  });

  it("rejects invalid default_verdict", () => {
    assert.throws(
      () => parsePolicyFile('version: "0.1"\ndefault_verdict: maybe'),
      /default_verdict/,
    );
  });

  it("rejects rules without name", () => {
    assert.throws(
      () => parsePolicyFile('version: "0.1"\ndefault_verdict: deny\nrules:\n  - verdict: deny'),
      /name/,
    );
  });

  it("rejects rules without verdict", () => {
    assert.throws(
      () => parsePolicyFile('version: "0.1"\ndefault_verdict: deny\nrules:\n  - name: test'),
      /verdict/,
    );
  });

  it("parses sensitive_data rules", () => {
    const yaml = `version: "0.1"
default_verdict: deny
sensitive_data:
  - category: credentials
    action: deny
    patterns:
      - "**/*.env"
      - "**/.ssh/**"`;
    const policy = parsePolicyFile(yaml);
    assert.equal(policy.sensitive_data.length, 1);
    assert.equal(policy.sensitive_data[0]!.category, "credentials");
    assert.equal(policy.sensitive_data[0]!.action, "deny");
    assert.deepEqual(policy.sensitive_data[0]!.patterns, ["**/*.env", "**/.ssh/**"]);
  });
});


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2024",
    "module": "Node16",
    "moduleResolution": "Node16",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist",
    "rootDir": ".",
    "declaration": true,
    "sourceMap": true,
    "resolveJsonModule": true,
    "forceConsistentCasingInFileNames": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  },
  "include": ["scripts/**/*.ts", "tests/**/*.ts"],
  "exclude": ["node_modules", "dist"]
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)