feat(hooks): introduce agent lifecycle hooks foundation with fail-closed blocking semantics

Phase 1 of agent-hooks-system establishes:
- Hook types, matchers, and CEL evaluation engine for event-driven extensibility
- Edition-gated command handlers (Lite disabled) with dedup_key-indexed audit log
- Dual-DB store layer (PostgreSQL + SQLite) with transaction boundary enforcement
- Blocking-event semantics: hook execution failures propagate to agent loop, graceful retry
- Tenant-isolated audit trail with per-hook execution context tracing
- Integration tests verifying store correctness and hook dispatch atomicity

Lays groundwork for post-v3.0 phase-02 (per-tenant onboarding hooks).
This commit is contained in:
viettranx committed 2026-04-15 14:25:59 +07:00
1 parent 86d564967f
commit 3fe5ae0b50
34 files changed
+4866 -27

No files matched your search

+86
View File
@@ -1568,3 +1568,89 @@ CREATE INDEX IF NOT EXISTS idx_vault_links_to ON vault_links(to_doc_id);
CREATE INDEX IF NOT EXISTS idx_vault_links_source
ON vault_links(json_extract(metadata, '$.source'))
WHERE json_extract(metadata, '$.source') IS NOT NULL;
-- ============================================================
-- Table: agent_hooks (migration 000052)
-- SQLite translation: JSONB→TEXT, TIMESTAMPTZ→TEXT, UUID→TEXT,
-- BYTEA→BLOB, DATE→TEXT (ISO8601), CHECK for enums.
-- ============================================================
CREATE TABLE IF NOT EXISTS agent_hooks (
id TEXT NOT NULL PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT '0193a5b0-7000-7000-8000-000000000001',
agent_id TEXT REFERENCES agents(id) ON DELETE CASCADE,
scope TEXT NOT NULL CHECK (scope IN ('global', 'tenant', 'agent')),
event TEXT NOT NULL,
handler_type TEXT NOT NULL CHECK (handler_type IN ('command', 'http', 'prompt')),
config TEXT NOT NULL DEFAULT '{}',
matcher TEXT,
if_expr TEXT,
timeout_ms INTEGER NOT NULL DEFAULT 5000,
on_timeout TEXT NOT NULL DEFAULT 'block' CHECK (on_timeout IN ('block', 'allow')),
priority INTEGER NOT NULL DEFAULT 0,
enabled INTEGER NOT NULL DEFAULT 1,
version INTEGER NOT NULL DEFAULT 1,
source TEXT NOT NULL DEFAULT 'ui' CHECK (source IN ('ui', 'api', 'seed')),
metadata TEXT NOT NULL DEFAULT '{}',
created_by TEXT,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_hooks_global
ON agent_hooks (event, handler_type)
WHERE scope = 'global';
CREATE UNIQUE INDEX IF NOT EXISTS uq_hooks_tenant
ON agent_hooks (tenant_id, event, handler_type)
WHERE scope = 'tenant';
CREATE UNIQUE INDEX IF NOT EXISTS uq_hooks_agent
ON agent_hooks (tenant_id, agent_id, event, handler_type)
WHERE scope = 'agent';
CREATE INDEX IF NOT EXISTS idx_hooks_lookup
ON agent_hooks (tenant_id, agent_id, event)
WHERE enabled = 1;
-- ============================================================
-- Table: hook_executions (append-only audit log, migration 000052)
-- ============================================================
CREATE TABLE IF NOT EXISTS hook_executions (
id TEXT NOT NULL PRIMARY KEY,
hook_id TEXT REFERENCES agent_hooks(id) ON DELETE SET NULL,
session_id TEXT,
event TEXT NOT NULL,
input_hash TEXT,
decision TEXT NOT NULL CHECK (decision IN ('allow', 'block', 'error', 'timeout')),
duration_ms INTEGER NOT NULL DEFAULT 0,
retry INTEGER NOT NULL DEFAULT 0,
dedup_key TEXT,
error TEXT,
error_detail BLOB,
metadata TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_hook_executions_session
ON hook_executions (session_id, created_at);
CREATE UNIQUE INDEX IF NOT EXISTS uq_hook_executions_dedup
ON hook_executions (dedup_key)
WHERE dedup_key IS NOT NULL;
-- ============================================================
-- Table: tenant_hook_budget (migration 000052)
-- month_start stored as TEXT ISO8601 date (YYYY-MM-DD).
-- ============================================================
CREATE TABLE IF NOT EXISTS tenant_hook_budget (
tenant_id TEXT NOT NULL PRIMARY KEY,
month_start TEXT NOT NULL,
budget_total INTEGER NOT NULL DEFAULT 0,
remaining INTEGER NOT NULL DEFAULT 0,
last_warned_at TEXT,
metadata TEXT NOT NULL DEFAULT '{}',
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);