Enforcement Layer Reference
This page is a Reference. It describes the enforcement machinery. Consult it when you need to know exactly what a guard checks, what a gate validates, or what the audit log schema contains. For background on why enforcement works the way it does, see Understanding the VECTOR Architecture.
Enforcement Components
VECTOR's enforcement layer has four components:
| Component | Stage | Description |
|---|---|---|
| Dispatch Guard | Pre-spawn | Hierarchy compliance, mode check, scope validation |
| Gate System | During task | Quality checkpoints at planning, code review, and integration |
| Schema Validators | On config/belief write | Structure and integrity checks |
| Audit Trail | Continuous | Immutable append-only log of all system actions |
Dispatch Guard
The Dispatch Guard runs before any PM or worker session is spawned. A spawn request that fails the Dispatch Guard is rejected. No spawn occurs.
Checks
Mode check
Reads state/vector-mode.json. Allowed spawn types per mode:
| Mode | Autonomous spawns | Manual spawns |
|---|---|---|
autonomous |
Allowed | Allowed |
directed |
Blocked | Allowed (Chief-initiated only) |
emergency |
Blocked | Blocked |
Hierarchy check
| From | May spawn | May not spawn |
|---|---|---|
| VECTOR (Tier 0) | PMs (Tier 1) | Workers (Tier 2), other VECTOR instances |
| PM (Tier 1) | Workers (Tier 2) | Other PMs, VECTOR |
| Worker (Tier 2) | Nothing | Anything |
A Tier 1 PM attempting to spawn another PM is a Dispatch Guard violation. The spawn is rejected and the violation is logged to audit_log.
Ticket existence check
Every spawn must reference a valid ticket ID in state/vector.db. A spawn request with no corresponding tickets record is rejected.
OS scope check
Any spawn that requests browser, terminal, Peekaboo, or system configuration access is rejected unless the spawner is VECTOR (Tier 0).
Mode State File
Location: state/vector-mode.json
{
"mode": "autonomous",
"changed_at": "2026-02-27T00:00:00Z",
"changed_by": "chief",
"reason": null
}
| Field | Type | Values |
|---|---|---|
mode |
string | autonomous | directed | emergency |
changed_at |
ISO 8601 datetime | — |
changed_by |
string | chief | vector | system |
reason |
string | null | Human-readable reason for mode change |
Failsafe Triggers
| Trigger phrase | Effect | Mode set |
|---|---|---|
stop |
No new autonomous spawns | directed |
emergency stop |
Kill all active workers immediately | emergency |
resume |
Resume autonomous operation | autonomous |
State is preserved on all mode transitions. In-flight worker output is not discarded.
Gate System
Quality gates are checkpoints in the task lifecycle. Gates are not optional. A task that cannot present a gate ID for a required stage is not accepted as complete.
Gate A — Plan Review
When: Before any worker is spawned.
Inputs required:
spec_path— path to the task spec markdown filepm_id— PM issuing the gate check
Checks performed:
| Check | Pass condition |
|---|---|
| Spec file exists | File at spec_path is readable |
| Spec is non-empty | More than 100 bytes |
| Scope is bounded | No scope phrases like "rewrite entire codebase", "migrate all data" without explicit Chief approval flag |
| Risk documented | Spec contains a risk section |
| No file conflicts | No in-flight tasks in tickets table working on same files |
Output on pass: Gate ID in format GATE-A-YYYYMMDD-NNN
Output on fail:
Gate A FAILED for [task description]:
- [specific failure reason 1]
- [specific failure reason 2]
Action: Address failures and resubmit.
Required usage: Gate ID must be embedded in the worker task spec as [GATE:GATE-A-YYYYMMDD-NNN]. Worker spawns without a Gate A ID are rejected by the Dispatch Guard.
Gate B — Code Review
When: After worker completes, before merge.
Inputs required:
output_dir— path to the directory containing worker outputpm_id— PM issuing the gate check
Review method: git diff main..task/TASK-xxx — the PM reviews the actual diff, not the worker's self-report or PTY output.
Checks performed:
| Check | Pass condition |
|---|---|
| Scope compliance | All changed files are within the declared task scope |
| No regressions | Existing test suite passes |
| Security scan | No plaintext secrets, no injection vectors in changed files |
| Coverage | New functionality has corresponding test coverage |
| Readability | Code quality meets PM judgment threshold |
Output on pass: Gate ID in format GATE-B-YYYYMMDD-NNN
Output on fail:
Gate B FAILED for [task description]:
- [specific failure: file, line, reason]
- [specific failure: file, line, reason]
Action: Address failures, resubmit Gate B.
Loop behavior: On Gate B failure, the PM sends specific fix requirements to the same Codex worker session. The loop continues until Gate B passes or timeout is reached (15 minutes per worker session).
Gate C — Integration Check
When: Before merge to main branch.
Inputs required:
branch— the task branch name (e.g.,task/TASK-20260226-010)pm_id— PM issuing the gate check
Checks performed:
| Check | Pass condition |
|---|---|
| Branch is clean | No uncommitted changes on branch |
| No merge conflicts | git merge-base --is-ancestor succeeds |
| Dependent services healthy | SENTINEL health check returns green |
| CUSTOM_PATCHES.md review | Changed files not listed as NEVER-AUTO-MERGE in CUSTOM_PATCHES.md |
| Build succeeds | npm run build exits 0 on branch |
Output on pass: Gate ID in format GATE-C-YYYYMMDD-NNN
CUSTOM_PATCHES.md conflict behavior: Any file listed in CUSTOM_PATCHES.md is automatically classified as DANGER. Gate C will not pass for a branch containing DANGER-classified changes without explicit Chief approval recorded in the ticket.
Gate ID Format
GATE-{TYPE}-{YYYYMMDD}-{NNN}
Examples:
GATE-A-20260227-001
GATE-B-20260227-014
GATE-C-20260227-007
| Segment | Format | Description |
|---|---|---|
| TYPE | A | B | C |
Gate type |
| YYYYMMDD | 8-digit date | UTC date of gate check |
| NNN | 3-digit zero-padded integer | Sequential counter per day per type |
Gate IDs are unique across the system. They are stored in audit_log.gate_id for traceability.
Schema Validators
openclaw.json Validator
Location: scripts/validate-openclaw-config.js
When to run: Before any change to openclaw.json is saved. The gateway has no fallback on invalid config — a malformed config crashes the daemon on next restart.
Checks performed:
| Field | Valid values | Invalid examples |
|---|---|---|
agent.model |
String starting with anthropic/ or openai/ |
claude-opus (missing provider prefix) |
auth.profiles[*].mode |
api_key | oauth | token |
api-key (hyphens not underscores) |
gateway.port |
Integer 1024–65535 | "18789" (string not integer) |
plugins.entries[*].enabled |
Boolean | "true" (string not boolean) |
| Credential values | Must not be present | Any field containing sk-, Bearer , or 40+ char strings |
Output on pass:
✓ openclaw.json is valid
✓ No credentials detected in config
✓ All required keys present
Output on fail:
✗ Validation failed:
- auth.profiles.anthropic.mode: "api-key" is not a valid mode. Use "api_key".
- gateway.port: must be an integer, got string "18789"
BEE Belief Validator
When: Before any belief is written to state/vector.db.
Required fields:
| Field | Type | Constraints |
|---|---|---|
content |
string | Non-empty, max 2000 chars |
scope |
enum | private | shared | global |
type |
string | Non-empty |
confidence |
float | 0.0–1.0 inclusive |
source |
string | Must match an existing ticket ID in tickets table |
status |
enum | Always provisional on creation — never active |
Rejected conditions:
sourcereferences a non-existent ticket IDstatusis anything other thanprovisionalon write (PMs cannot create active beliefs)confidenceis above 1.0 or below 0.0scopeisglobal(PMs cannot create global-scope beliefs — only VECTOR can write global scope)
Audit Trail
Table: audit_log in state/vector.db
CREATE TABLE audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
actor TEXT NOT NULL,
action TEXT NOT NULL,
target TEXT,
result TEXT,
detail TEXT,
gate_id TEXT
);
| Column | Type | Description |
|---|---|---|
id |
integer | Auto-incrementing primary key |
timestamp |
ISO 8601 text | UTC timestamp of action |
actor |
text | VECTOR | FORGE | GHOST | ORACLE | SENTINEL | COMPASS | SAGE | SYSTEM |
action |
text | See action types below |
target |
text | null | Ticket ID, file path, belief ID, or PM name |
result |
text | null | passed | failed | error | skipped |
detail |
JSON text | null | Context blob — contents vary by action type |
gate_id |
text | null | Linked gate check ID if action involves a gate |
Action Types
| Action | Actor | Description |
|---|---|---|
spawn_pm |
VECTOR | PM session created |
spawn_worker |
PM | Worker session created |
gate_check |
PM | VECTOR | Gate A/B/C check submitted |
belief_write |
VECTOR | Belief written to beliefs table |
belief_promote |
VECTOR | Belief promoted from provisional to active |
belief_reject |
VECTOR | Belief rejected from pending_shared |
ticket_create |
VECTOR | Ticket created in tickets table |
ticket_close |
VECTOR | Ticket marked complete |
mode_change |
VECTOR | SYSTEM | vector-mode.json updated |
dispatch_violation |
SYSTEM | Dispatch Guard rejected a spawn |
config_validate |
VECTOR | openclaw.json validation run |
Retention
Audit log records are never deleted. Monthly archival (midnight UTC, first of month) compresses records older than 90 days to state/audit_archive_YYYYMM.db. The primary audit_log table retains the trailing 90 days for fast queries.
Example Queries
# Last 20 actions
sqlite3 state/vector.db \
"SELECT timestamp, actor, action, result FROM audit_log ORDER BY id DESC LIMIT 20;"
# All gate failures in the last 7 days
sqlite3 state/vector.db \
"SELECT timestamp, actor, target, detail FROM audit_log
WHERE action = 'gate_check' AND result = 'failed'
AND timestamp > datetime('now', '-7 days');"
# All actions on a specific task
sqlite3 state/vector.db \
"SELECT timestamp, actor, action, result FROM audit_log
WHERE target LIKE '%TASK-20260226-010%'
ORDER BY id ASC;"
# Dispatch violations this month
sqlite3 state/vector.db \
"SELECT timestamp, detail FROM audit_log
WHERE action = 'dispatch_violation'
AND timestamp > datetime('now', 'start of month');"