Smallest honest vertical slice of §9.4: a `[[interceptors]]` block (app OR
group) binds a script to run BEFORE `kv::set`/`delete`; it reads the operation
context (`ctx.request.body`: service, action, collection, key, value, caller
ids) and returns `#{ allowed, reason }` — `allowed == false` denies the op (the
write never runs, the caller gets a runtime error).
Reuses two existing mechanisms rather than inventing new ones:
- Registration mirrors extension points (§5.5): a marker table
`0073_interceptors.sql` (owner-polymorphic app_id/group_id XOR, keyed
(service, op) → script), `interceptor_repo` (insert/delete/list + the
nearest-owner-wins `resolve_before` chain walk), reconciled through the
declarative apply exactly like `vars` (create/update/delete, prunable).
- Execution reuses the `invoke()` re-entry path: the new `InterceptorService`
(shared trait + Postgres-backed impl) only RESOLVES the script name (keeping
executor-core Postgres-free); the executor's `sdk::interceptor::run_before`
resolves that name and runs it via `run_resolved_blocking` (extracted from
`invoke_blocking` — shared depth bound + AST cache). An un-hooked write pays
one indexed `Ok(None)` resolve; no interceptor ⇒ zero overhead.
Nearest-owner-wins so an app overrides a group's interceptor, and a group
interceptor is inherited by every descendant app — the chain walk is the
isolation boundary (a sibling subtree never matches). `validate_bundle_for`
restricts the MVP to `service = "kv"`, `op ∈ {set, delete}`, one marker per
(service, op).
Deferred (documented in §9.4): the `data` transform return, services other
than kv, `after_*` hooks, chaining + circular-dependency guard, the timeout
policy, and a `pic interceptors ls` read surface (needs a server route).
Pinned by `tests/interceptors.rs` (deny blocks the write; allow passes;
group→app inheritance), schema snapshot re-blessed. 154/154 journeys pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
43 lines
2.3 KiB
SQL
43 lines
2.3 KiB
SQL
-- §9.4 Service Interceptors (v1.2) — before-op allow/deny hooks.
|
|
--
|
|
-- A marker `(owner, service, op) -> interceptor_script` declares that a script
|
|
-- runs BEFORE a data-plane operation and may deny it. MVP scope: `service='kv'`,
|
|
-- `op IN ('set','delete')`, allow/deny only (no data transform, no chaining,
|
|
-- no after-hooks). The interceptor is itself a script owned by the same node
|
|
-- (or an ancestor group); it is resolved + run through the existing `invoke()`
|
|
-- re-entry path, so this table holds only the MARKER — pure declaration,
|
|
-- structurally like an `extension_points` row (0051): config, not code, so
|
|
-- ON DELETE CASCADE.
|
|
--
|
|
-- Ownership is polymorphic (mirrors extension_points/vars/secrets/scripts):
|
|
-- exactly one of (app_id, group_id) is set. Resolution walks the calling app's
|
|
-- chain (app, then nearest ancestor group) and picks the nearest declaration
|
|
-- for a (service, op) — nearest-owner-wins, so an app overrides a group's
|
|
-- interceptor, the deliberate inverse of a sealed import.
|
|
|
|
CREATE TABLE interceptors (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
group_id UUID REFERENCES groups(id) ON DELETE CASCADE,
|
|
app_id UUID REFERENCES apps(id) ON DELETE CASCADE,
|
|
CONSTRAINT interceptors_owner_exactly_one
|
|
CHECK ((group_id IS NULL) <> (app_id IS NULL)),
|
|
-- The intercepted operation. MVP: service='kv', op IN ('set','delete').
|
|
service TEXT NOT NULL,
|
|
op TEXT NOT NULL,
|
|
-- Name of the interceptor script (resolved on the owner's chain at run time).
|
|
interceptor_script TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- One marker per (owner, service, op). Partial because the owner is split
|
|
-- across two nullable columns.
|
|
CREATE UNIQUE INDEX interceptors_group_uidx
|
|
ON interceptors (group_id, service, op) WHERE group_id IS NOT NULL;
|
|
CREATE UNIQUE INDEX interceptors_app_uidx
|
|
ON interceptors (app_id, service, op) WHERE app_id IS NOT NULL;
|
|
|
|
-- Lookup indexes for the resolver's chain join + list-by-owner.
|
|
CREATE INDEX interceptors_group_id_idx ON interceptors (group_id) WHERE group_id IS NOT NULL;
|
|
CREATE INDEX interceptors_app_id_idx ON interceptors (app_id) WHERE app_id IS NOT NULL;
|