# Gate and Ensure Actions for signet-eval

## System Context

signet-eval is a Rust binary that enforces deterministic policy on AI agent tool calls. It sits between Claude Code and the tools the agent uses (Bash, file editing, etc). Every tool call passes through signet-eval as a PreToolUse hook -- it reads JSON from stdin describing the tool call, evaluates it against YAML policy rules (first-match-wins), and writes allow/deny/ask JSON to stdout. ~25ms end-to-end.

Current action types: Allow, Deny, Ask. This feature adds two new action types: **Gate** and **Ensure**.

## Consequence Map

- **Critical**: Without gate/ensure, the AI agent repeatedly uses the wrong GitHub account for remote operations -- causing failed pushes, wrong-account deploys, and leaked credentials to the wrong org. This happened 5+ times in a single session.
- **High**: A compromised or malicious check script in ~/.signet/checks/ could execute arbitrary code with user privileges. The checks directory must be tamper-proof.
- **Medium**: A hung check script blocks the hook indefinitely, making the AI agent unresponsive. Timeout enforcement is required.
- **Low**: Excessive ensure checks on high-frequency tool calls degrade hook latency from 25ms to 5+ seconds.

## What We're Building

### Gate Action

A rule action that requires a specific prior command to have been executed in the current session before allowing the matched command. Gate checks the vault's action log (SQLite ledger) for a matching entry.

- Deterministic -- no shell execution, no side effects
- Enforceable -- the agent can't fake log entries (signet-eval writes them)
- Uses existing action log infrastructure
- On failure: deny with a message telling the agent what to run first

PolicyRule YAML example:

    - name: require-auth-switch
      tool_pattern: ".*"
      conditions:
        - "contains(parameters, 'jmcentire')"
      action: gate
      gate:
        requires_prior: "gh auth switch --user jmcentire"
        within: 50
      reason: "Must switch to jmcentire account before operating on jmcentire repos"

Evaluation flow:
1. Rule matches (tool pattern + conditions)
2. Query vault action log: "Does any of the last N entries contain `requires_prior` string?"
3. If found: allow
4. If not found: deny with reason + message about what to run

### Ensure Action

A rule action that runs a validation script before allowing the matched command. Scripts live in `~/.signet/checks/` (a locked, protected directory). Policy references scripts by name only.

- Check script contract: exit 0 = pass, non-zero = fail
- On failure: deny with a message. The agent must voluntarily fix the issue. signet-eval does NOT auto-execute fixes.
- Subprocess execution with hard timeout (default 5s, configurable)
- Script can block on user input, implement complex logic, inspect system state

PolicyRule YAML example:

    - name: github-identity-guard
      tool_pattern: "Bash"
      conditions:
        - "any_of(parameters, 'git push', 'git pull', 'git fetch', 'git clone', 'gh ')"
      action: ensure
      ensure:
        check: "gh-identity-matches-remote"
        timeout: 10
        message: "GitHub identity must match the target repository owner. Run the appropriate 'gh auth switch' command."
      reason: "Prevents wrong-account operations on GitHub"

Evaluation flow:
1. Rule matches (tool pattern + conditions)
2. Resolve script path: ~/.signet/checks/{check_name}
3. Validate: script exists, is executable, name has no path traversal
4. Spawn subprocess, wait with timeout
5. Exit 0: allow
6. Exit non-zero: deny with ensure.message + script stderr (truncated)
7. Timeout: kill process, deny with timeout message

### Motivating Use Case: GitHub Identity Guard

Two GitHub accounts:
- **jmcentire** (personal) -- pushes to jmcentire/ repos
- **jmc-wander** (work) -- pushes to wandercom/ repos

Check script `gh-identity-matches-remote`:
1. Parse current repo's git remote URL to extract org/user
2. Run `gh auth status` to get active account
3. If remote contains "jmcentire" and user is jmcentire -> exit 0
4. If remote contains "wandercom" and user is jmc-wander -> exit 0
5. If remote contains neither -> prompt user which account to use, switch, exit 0 (or exit 1 if declined)
6. If mismatch -> print error explaining mismatch, exit 1

## Boundary Conditions

### Scope
- Two new Decision variants: Gate, Ensure
- PolicyRule gains optional gate and ensure config structs
- New self-protection locked rule for ~/.signet/checks/
- Check script directory convention
- Validation for new rule types
- Backward compatible -- existing allow/deny/ask rules unchanged

### Non-Goals
- Auto-executing fix commands (agent must comply voluntarily)
- Network-based checks (scripts are local only)
- Dynamic check script installation via MCP (scripts are manually placed)
- Chaining multiple ensure checks in a single rule

### Constraints
- No state mutation in the policy evaluation path (gate queries log, ensure spawns subprocess but doesn't modify signet state)
- Check scripts execute with user privileges -- directory must be tamper-proof
- Must work in both hook mode (subprocess, stdin/stdout) and MCP server mode
- Pause mode must NOT bypass checks directory protection
- HMAC policy signing covers gate/ensure fields

## Done When

1. Gate action evaluates correctly: matches prior log entry -> allow, no match -> deny with message
2. Ensure action evaluates correctly: script passes -> allow, fails -> deny, missing -> deny, timeout -> deny
3. ~/.signet/checks/ protected by locked self-protection rule (highest priority, pause-immune)
4. Policy validation catches: missing gate/ensure config, path traversal in script names, unknown fields
5. MCP tools handle gate/ensure rules (add, edit, list, display)
6. GitHub identity guard check script works end-to-end
7. All existing tests pass (backward compatible)
8. New tests: gate happy/sad path, ensure happy/sad/timeout/missing/traversal, self-protection, adversarial
