feat(interceptors): §9.4 service interceptors — thin KV allow/deny slice
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>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Project Blueprint: Lightweight Event-Based Serverless Cloud
|
||||
|
||||
**Status**: v1.1 shipped (SDK + services) · v1.2 *Hierarchies* track **complete** · v1.2 *Workflows* track **shipped** (M1–M6: DAG execution, conditional branching, nested sub-workflows, `workflow::start` SDK, dashboard DAG + run-history); §9.4 service interceptors + v1.3 cluster mode are next
|
||||
**Status**: v1.1 shipped (SDK + services) · v1.2 *Hierarchies* track **complete** · v1.2 *Workflows* track **shipped** (M1–M6: DAG execution, conditional branching, nested sub-workflows, `workflow::start` SDK, dashboard DAG + run-history); a §9.4 service-interceptor KV allow/deny slice has shipped (migration 0073); the rest of §9.4 + v1.3 cluster mode are next
|
||||
**Last Updated**: 2026-07-12 (reconciled to shipped code — CLAUDE.md is the live source of truth)
|
||||
**Audience**: Solo developer (DIY self-hosted)
|
||||
|
||||
@@ -171,8 +171,8 @@ rhai_executor --script $SCRIPT_PATH --request "$REQUEST_JSON"
|
||||
|
||||
### 3.4 PostgreSQL Database
|
||||
**Schema (MVP sketch — NOT authoritative):** the block below is the original MVP shape. The **authoritative
|
||||
schema is the migration set** in `crates/manager-core/migrations/` (through `0072` as of v1.2, incl. the
|
||||
Workflows tables), which has
|
||||
schema is the migration set** in `crates/manager-core/migrations/` (through `0073` as of v1.2, incl. the
|
||||
Workflows tables + the §9.4 interceptor markers), which has
|
||||
since added apps/domains, RBAC (`admin_users`/`app_members`/`api_keys`), the v1.1 data-plane services
|
||||
(KV/docs/files/queues/…), and the v1.2 groups/collections/templates/projects tables. Treat this as
|
||||
illustration only.
|
||||
@@ -1697,6 +1697,16 @@ CREATE INDEX idx_execution_parent ON execution_logs(parent_execution_id);
|
||||
|
||||
### 9.4 Service Interceptors & Middleware (v1.2+)
|
||||
|
||||
> **Status — thin KV allow/deny slice SHIPPED** (migration `0073_interceptors.sql`). A `[[interceptors]]`
|
||||
> manifest block (app OR group) binds a script to run BEFORE `kv::set` / `kv::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). Registration is a marker
|
||||
> `(owner, service, op) → script`, resolved **nearest-owner-wins** on the calling app's chain (an app overrides
|
||||
> a group's), mirroring extension points (§5.5); the interceptor script itself is resolved + run through the
|
||||
> `invoke()` re-entry path (shared depth bound, AST cache). **Deferred (the rest of the spec below):** the
|
||||
> `data` transform return, services other than `kv`, `after_*` hooks, interceptor chaining + circular-dependency
|
||||
> guard, and the timeout policy. The remaining subsections describe that full design.
|
||||
|
||||
**Concept**: A script can act as middleware to intercept and validate/transform service operations before they execute.
|
||||
|
||||
**Use Cases:**
|
||||
|
||||
Reference in New Issue
Block a user