1pub const CONTRACT_SQL: &str = r#"-- 0001_contract.sql — the gwk contract schema, greenfield.
9--
10-- This file is CONTRACT: shapes, state sets, legal edges, and invariants any
11-- conforming storage backend must uphold. Engine-specific mechanics (queues,
12-- notification channels, lock strategies, privilege hardening) belong to the
13-- backend/deployment layer and are deliberately absent here.
14--
15-- Conventions:
16-- * every identifier snake_case; ids are opaque text
17-- * state values are exactly the wire strings of gwk-domain's FSM enums
18-- * u64 counters (seq, fence_token, byte_size) are numeric(20,0) IN STORAGE,
19-- CHECK-bounded to [0, 2^64-1] (18446744073709551615) — numeric(20,0)
20-- alone admits ±(10^20-1), overshooting u64 in both directions. A signed
21-- bigint tops out at 2^63-1, half the u64 wire range; the decimal-string
22-- rule is a WIRE-format rule (JSON), not a storage rule
23-- * u32 version counters (aggregate_version, each state row's version) are
24-- bigint IN STORAGE, CHECK-bounded to [1, 2^32-1] (4294967295) — a signed
25-- integer tops out at 2^31-1, half the u32 wire range
26-- * timestamps are timestamptz
27
28BEGIN;
29
30CREATE SCHEMA IF NOT EXISTS gwk;
31
32-- ============================================================
33-- The event log
34-- ============================================================
35
36-- seq is ASSIGNED BY THE KERNEL APPEND ACTOR, in COMMIT order, and is the
37-- global read/replay cursor: unique, strictly increasing, NOT gapless.
38--
39-- Deliberately NOT BIGSERIAL/IDENTITY: sequence numbers allocate at INSERT
40-- time but transactions COMMIT in a different order, so an allocation-ordered
41-- column is not proof of commit order — a reader paging by it can observe
42-- seq N+1 before an uncommitted seq N exists, then never see N. A single
43-- append actor assigning seq at commit closes that hole by construction.
44CREATE TABLE gwk.event (
45 seq numeric(20,0) PRIMARY KEY
46 CHECK (seq >= 0 AND seq <= 18446744073709551615),
47 event_id text NOT NULL UNIQUE,
48 project_id text NOT NULL,
49 aggregate_type text NOT NULL,
50 aggregate_id text NOT NULL,
51 aggregate_version bigint NOT NULL CHECK (aggregate_version BETWEEN 1 AND 4294967295),
52 event_type text NOT NULL,
53 -- bigint, not integer: the envelope declares `schema_version: u32`, and a
54 -- signed integer tops out at 2^31-1 — half that range. The narrow column made
55 -- the contract claim a width the storage could not hold, on the one field a
56 -- future decoder dispatches on. Same treatment as aggregate_version above.
57 schema_version bigint NOT NULL CHECK (schema_version BETWEEN 1 AND 4294967295),
58 occurred_at timestamptz NOT NULL,
59 appended_at timestamptz NOT NULL,
60 actor jsonb NOT NULL,
61 origin jsonb NOT NULL,
62 causation_id text,
63 correlation_id text,
64 idempotency_key text,
65 payload jsonb NOT NULL,
66 payload_ref jsonb,
67 -- one event per aggregate version: the contiguity + CAS anchor
68 UNIQUE (aggregate_type, aggregate_id, aggregate_version)
69);
70
71-- retry-stable appends: the same keyed write cannot land twice per aggregate.
72-- This is the index the REPLAY lookup reads — a retry of the same command on
73-- the same aggregate is answered from here with the original events.
74CREATE UNIQUE INDEX event_idempotency
75 ON gwk.event (aggregate_type, aggregate_id, idempotency_key)
76 WHERE idempotency_key IS NOT NULL;
77
78-- Idempotency is GLOBAL per (project_id, idempotency_key), not per aggregate:
79-- one key names one request, and reusing it for a different one is a caller bug
80-- that must be refused rather than silently landed twice. The per-aggregate
81-- index above cannot see that case — the two writes differ in aggregate_id, so
82-- it admits both. This is the guard; the kernel's own pre-check turns the
83-- violation into a typed `idempotency_conflict` before it reaches the index.
84CREATE UNIQUE INDEX event_idempotency_project
85 ON gwk.event (project_id, idempotency_key)
86 WHERE idempotency_key IS NOT NULL;
87
88-- Append-only is a CONTRACT property, so it is enforced here as a trigger any
89-- backend inherits. Privilege-level enforcement (revoking UPDATE/DELETE from
90-- runtime roles) is a deployment MECHANISM layered on top by the kernel's
91-- provisioning, not part of the contract.
92CREATE FUNCTION gwk.forbid_event_mutation() RETURNS trigger
93LANGUAGE plpgsql AS $$
94BEGIN
95 RAISE EXCEPTION 'gwk.event is append-only (% refused)', TG_OP;
96END $$;
97
98CREATE TRIGGER event_append_only
99 BEFORE UPDATE OR DELETE ON gwk.event
100 FOR EACH ROW EXECUTE FUNCTION gwk.forbid_event_mutation();
101
102-- Row-level triggers never fire on TRUNCATE, so append-only needs a
103-- statement-level trigger too — without it one statement erases the log.
104CREATE TRIGGER event_no_truncate
105 BEFORE TRUNCATE ON gwk.event
106 FOR EACH STATEMENT EXECUTE FUNCTION gwk.forbid_event_mutation();
107
108-- ENABLE ALWAYS on every guard trigger in this file: ordinary triggers are
109-- suppressed under SET session_replication_role = 'replica', which would
110-- otherwise switch every invariant off in one statement.
111ALTER TABLE gwk.event ENABLE ALWAYS TRIGGER event_append_only;
112ALTER TABLE gwk.event ENABLE ALWAYS TRIGGER event_no_truncate;
113
114-- ============================================================
115-- FSM edge data + the transition guard
116-- ============================================================
117
118CREATE TABLE gwk.transition (
119 entity text NOT NULL,
120 from_state text NOT NULL,
121 to_state text NOT NULL,
122 PRIMARY KEY (entity, from_state, to_state)
123);
124
125INSERT INTO gwk.transition (entity, from_state, to_state) VALUES
126 ('task', 'submitted', 'working'),
127 ('task', 'submitted', 'canceled'),
128 ('task', 'working', 'input_required'),
129 ('task', 'working', 'completed'),
130 ('task', 'working', 'failed'),
131 ('task', 'working', 'canceled'),
132 ('task', 'input_required', 'working'),
133 ('task', 'input_required', 'canceled'),
134
135 ('attempt', 'queued', 'leased'),
136 ('attempt', 'queued', 'canceled'),
137 ('attempt', 'leased', 'starting'),
138 ('attempt', 'leased', 'canceled'),
139 ('attempt', 'starting', 'running'),
140 ('attempt', 'starting', 'failed'),
141 ('attempt', 'starting', 'unknown'),
142 ('attempt', 'starting', 'canceling'),
143 ('attempt', 'running', 'blocked'),
144 ('attempt', 'running', 'canceling'),
145 ('attempt', 'running', 'failed'),
146 ('attempt', 'running', 'unknown'),
147 ('attempt', 'running', 'succeeded'),
148 ('attempt', 'blocked', 'running'),
149 ('attempt', 'blocked', 'canceling'),
150 ('attempt', 'blocked', 'failed'),
151 ('attempt', 'blocked', 'unknown'),
152 ('attempt', 'canceling', 'canceled'),
153 ('attempt', 'canceling', 'unknown'),
154
155 ('message', 'accepted', 'delivered'),
156 ('message', 'accepted', 'dead_letter'),
157 ('message', 'delivered', 'acknowledged'),
158 ('message', 'delivered', 'dead_letter'),
159 ('message', 'acknowledged', 'applied'),
160 ('message', 'acknowledged', 'rejected'),
161 ('message', 'acknowledged', 'dead_letter'),
162
163 ('command', 'issued', 'targeted'),
164 ('command', 'targeted', 'signaled'),
165 ('command', 'signaled', 'verification_complete');
166
167-- The edge table is the data the transition guard trusts: a plain INSERT here
168-- would legalise a previously forbidden edge for every subsequent transition.
169-- Seeded above, then frozen — these triggers refuse every later write.
170CREATE FUNCTION gwk.forbid_transition_mutation() RETURNS trigger
171LANGUAGE plpgsql AS $$
172BEGIN
173 RAISE EXCEPTION 'gwk.transition is immutable seed data (% refused)', TG_OP;
174END $$;
175
176CREATE TRIGGER transition_immutable
177 BEFORE INSERT OR UPDATE OR DELETE ON gwk.transition
178 FOR EACH ROW EXECUTE FUNCTION gwk.forbid_transition_mutation();
179
180CREATE TRIGGER transition_no_truncate
181 BEFORE TRUNCATE ON gwk.transition
182 FOR EACH STATEMENT EXECUTE FUNCTION gwk.forbid_transition_mutation();
183
184ALTER TABLE gwk.transition ENABLE ALWAYS TRIGGER transition_immutable;
185ALTER TABLE gwk.transition ENABLE ALWAYS TRIGGER transition_no_truncate;
186
187-- The row-level guard behind every FSM table: edge legality against
188-- gwk.transition, and CAS discipline — EVERY update advances version by
189-- exactly 1 (terminal immutability follows from terminals having no edges).
190-- NOT enforced here: the liveness-producer flip rule (which actor may drive
191-- running <-> blocked) lives in the domain crate's transition apply path —
192-- every writer must route through it.
193CREATE FUNCTION gwk.assert_transition() RETURNS trigger
194LANGUAGE plpgsql AS $$
195BEGIN
196 IF NEW.state IS DISTINCT FROM OLD.state
197 AND NOT EXISTS (
198 SELECT 1 FROM gwk.transition t
199 WHERE t.entity = TG_ARGV[0]
200 AND t.from_state = OLD.state
201 AND t.to_state = NEW.state
202 ) THEN
203 RAISE EXCEPTION 'illegal % transition: % -> %', TG_ARGV[0], OLD.state, NEW.state;
204 END IF;
205 IF NEW.version IS DISTINCT FROM OLD.version + 1 THEN
206 RAISE EXCEPTION '% version must advance by exactly 1 (got % -> %)',
207 TG_ARGV[0], OLD.version, NEW.version;
208 END IF;
209 RETURN NEW;
210END $$;
211
212-- Companion guards on every FSM table. A row must be BORN at its machine's
213-- initial state at version 1: a row inserted mid-spine — or born terminal,
214-- e.g. a command inserted directly at verification_complete — satisfies every
215-- CHECK yet skips the whole edge guard. And rows are never deleted:
216-- DELETE-then-INSERT would walk around the edge table the same way.
217CREATE FUNCTION gwk.assert_initial_state() RETURNS trigger
218LANGUAGE plpgsql AS $$
219BEGIN
220 IF NEW.state IS DISTINCT FROM TG_ARGV[1] OR NEW.version IS DISTINCT FROM 1 THEN
221 RAISE EXCEPTION '% rows must be born in state % at version 1 (got %, version %)',
222 TG_ARGV[0], TG_ARGV[1], NEW.state, NEW.version;
223 END IF;
224 RETURN NEW;
225END $$;
226
227CREATE FUNCTION gwk.forbid_state_row_delete() RETURNS trigger
228LANGUAGE plpgsql AS $$
229BEGIN
230 RAISE EXCEPTION '% rows cannot be deleted', TG_ARGV[0];
231END $$;
232
233-- ============================================================
234-- Vertical-slice entity tables
235-- ============================================================
236
237CREATE TABLE gwk.lease (
238 id text PRIMARY KEY,
239 version bigint NOT NULL DEFAULT 1 CHECK (version BETWEEN 1 AND 4294967295),
240 state text NOT NULL DEFAULT 'held'
241 CHECK (state IN ('held', 'released', 'expired')),
242 mode text NOT NULL DEFAULT 'exclusive'
243 CHECK (mode IN ('exclusive', 'shared')),
244 holder text,
245 scope text,
246 repo text,
247 path text,
248 branch text,
249 base_sha text,
250 fence_token numeric(20,0)
251 CHECK (fence_token >= 0 AND fence_token <= 18446744073709551615),
252 heartbeat_at timestamptz,
253 expires_at timestamptz,
254 dirty boolean NOT NULL DEFAULT false,
255 unpushed boolean NOT NULL DEFAULT false,
256 disposition text,
257 created_at timestamptz NOT NULL DEFAULT now(),
258 updated_at timestamptz NOT NULL DEFAULT now()
259);
260
261-- gwk.lease carries the fence token — the deposed-writer defense: a lease taken
262-- over hands the new holder a strictly higher fence, and any write stamped with
263-- a stale (lower) fence must be rejected downstream. A fence that could rewind
264-- would re-arm a deposed writer, so it is monotonic non-decreasing here and,
265-- once set, never cleared; and,
266-- as on the FSM tables, every UPDATE is a versioned CAS: version advances by
267-- exactly 1. This binds ALL lease writes — a heartbeat touch or a time-driven
268-- expiry write bumps version too. It is a write-discipline invariant (optimistic
269-- concurrency), NOT a state-transition rule, so it does not contradict the next
270-- line: the lease is NOT one of the four public FSMs (fsm.rs: expiry is
271-- time-driven, release holder-driven), so it gets no edge guard, and which
272-- held/released/expired edges are legal — expiry vs release semantics — is a
273-- backend concern deliberately not modeled here. This contract enforces exactly
274-- two things on the lease: fence monotonicity (above) and the version CAS.
275CREATE FUNCTION gwk.assert_lease_guard() RETURNS trigger
276LANGUAGE plpgsql AS $$
277BEGIN
278 IF NEW.version IS DISTINCT FROM OLD.version + 1 THEN
279 RAISE EXCEPTION 'lease version must advance by exactly 1 (got % -> %)',
280 OLD.version, NEW.version;
281 END IF;
282 -- A fence, once set, is monotonic non-decreasing AND may never be cleared.
283 -- Guarding only `NEW < OLD` left a two-step launder: 100 -> NULL (NEW null,
284 -- skips the check) then NULL -> 50 (OLD null, skips it) rewinds the fence and
285 -- re-arms a deposed writer. Refuse clearing or lowering a set fence; a fresh
286 -- fence (OLD null -> NEW value) is still allowed.
287 IF OLD.fence_token IS NOT NULL
288 AND (NEW.fence_token IS NULL OR NEW.fence_token < OLD.fence_token) THEN
289 RAISE EXCEPTION 'lease fence_token must not decrease or be cleared (got % -> %)',
290 OLD.fence_token, NEW.fence_token;
291 END IF;
292 RETURN NEW;
293END $$;
294
295CREATE TRIGGER lease_guard
296 BEFORE UPDATE ON gwk.lease
297 FOR EACH ROW EXECUTE FUNCTION gwk.assert_lease_guard();
298
299ALTER TABLE gwk.lease ENABLE ALWAYS TRIGGER lease_guard;
300
301CREATE TABLE gwk.task (
302 id text PRIMARY KEY,
303 version bigint NOT NULL DEFAULT 1 CHECK (version BETWEEN 1 AND 4294967295),
304 state text NOT NULL DEFAULT 'submitted'
305 CHECK (state IN ('submitted', 'working', 'input_required',
306 'completed', 'failed', 'canceled')),
307 kind text,
308 title text,
309 spec_ref text,
310 project text,
311 priority integer,
312 tracker_ref text,
313 created_at timestamptz NOT NULL DEFAULT now(),
314 updated_at timestamptz NOT NULL DEFAULT now()
315);
316
317CREATE TRIGGER task_transition
318 BEFORE UPDATE ON gwk.task
319 FOR EACH ROW EXECUTE FUNCTION gwk.assert_transition('task');
320
321CREATE TRIGGER task_born_initial
322 BEFORE INSERT ON gwk.task
323 FOR EACH ROW EXECUTE FUNCTION gwk.assert_initial_state('task', 'submitted');
324
325CREATE TRIGGER task_no_delete
326 BEFORE DELETE ON gwk.task
327 FOR EACH ROW EXECUTE FUNCTION gwk.forbid_state_row_delete('task');
328
329-- Row-level triggers never fire on TRUNCATE, so the delete guard needs a
330-- statement-level partner — else TRUNCATE wipes the table and every id is free
331-- to be re-inserted at the initial state, the exact DELETE-then-INSERT
332-- walk-around task_no_delete exists to stop, in one statement.
333CREATE TRIGGER task_no_truncate
334 BEFORE TRUNCATE ON gwk.task
335 FOR EACH STATEMENT EXECUTE FUNCTION gwk.forbid_state_row_delete('task');
336
337ALTER TABLE gwk.task ENABLE ALWAYS TRIGGER task_transition;
338ALTER TABLE gwk.task ENABLE ALWAYS TRIGGER task_born_initial;
339ALTER TABLE gwk.task ENABLE ALWAYS TRIGGER task_no_delete;
340ALTER TABLE gwk.task ENABLE ALWAYS TRIGGER task_no_truncate;
341
342CREATE TABLE gwk.attempt (
343 id text PRIMARY KEY,
344 version bigint NOT NULL DEFAULT 1 CHECK (version BETWEEN 1 AND 4294967295),
345 state text NOT NULL DEFAULT 'queued'
346 CHECK (state IN ('queued', 'leased', 'starting',
347 'running', 'blocked', 'canceling',
348 'canceled', 'failed', 'unknown',
349 'succeeded')),
350 task_id text NOT NULL REFERENCES gwk.task(id),
351 engine text NOT NULL,
352 capability text,
353 role text,
354 model_lane text,
355 permission_profile text,
356 worktree_lease_id text REFERENCES gwk.lease(id),
357 base_sha text,
358 budget jsonb,
359 result_schema_ref text,
360 provider_session_ref text,
361 runtime_ref text,
362 runtime_started_at timestamptz,
363 exit_code integer,
364 provider_terminal_event text,
365 result_valid boolean,
366 evidence_manifest_ref text,
367 gate_result text,
368 created_at timestamptz NOT NULL DEFAULT now(),
369 updated_at timestamptz NOT NULL DEFAULT now()
370);
371
372CREATE TRIGGER attempt_transition
373 BEFORE UPDATE ON gwk.attempt
374 FOR EACH ROW EXECUTE FUNCTION gwk.assert_transition('attempt');
375
376CREATE TRIGGER attempt_born_initial
377 BEFORE INSERT ON gwk.attempt
378 FOR EACH ROW EXECUTE FUNCTION gwk.assert_initial_state('attempt', 'queued');
379
380CREATE TRIGGER attempt_no_delete
381 BEFORE DELETE ON gwk.attempt
382 FOR EACH ROW EXECUTE FUNCTION gwk.forbid_state_row_delete('attempt');
383
384CREATE TRIGGER attempt_no_truncate
385 BEFORE TRUNCATE ON gwk.attempt
386 FOR EACH STATEMENT EXECUTE FUNCTION gwk.forbid_state_row_delete('attempt');
387
388ALTER TABLE gwk.attempt ENABLE ALWAYS TRIGGER attempt_transition;
389ALTER TABLE gwk.attempt ENABLE ALWAYS TRIGGER attempt_born_initial;
390ALTER TABLE gwk.attempt ENABLE ALWAYS TRIGGER attempt_no_delete;
391ALTER TABLE gwk.attempt ENABLE ALWAYS TRIGGER attempt_no_truncate;
392
393CREATE TABLE gwk.engine_session (
394 id text PRIMARY KEY,
395 attempt_id text NOT NULL REFERENCES gwk.attempt(id),
396 engine text NOT NULL,
397 provider_session_ref text,
398 started_at timestamptz NOT NULL,
399 ended_at timestamptz
400);
401
402CREATE TABLE gwk.message (
403 id text PRIMARY KEY,
404 version bigint NOT NULL DEFAULT 1 CHECK (version BETWEEN 1 AND 4294967295),
405 state text NOT NULL DEFAULT 'accepted'
406 CHECK (state IN ('accepted', 'delivered', 'acknowledged',
407 'applied', 'rejected', 'dead_letter')),
408 idempotency_key text NOT NULL UNIQUE,
409 correlation_id text,
410 reply_to text REFERENCES gwk.message(id),
411 sender text,
412 recipient text,
413 channel text,
414 kind text,
415 payload jsonb,
416 deadline timestamptz,
417 delivery_attempts integer NOT NULL DEFAULT 0 CHECK (delivery_attempts >= 0),
418 dead_letter_reason text,
419 -- channel name -> opaque per-channel delivery reference
420 delivery_refs jsonb,
421 created_at timestamptz NOT NULL DEFAULT now(),
422 updated_at timestamptz NOT NULL DEFAULT now()
423);
424
425CREATE TRIGGER message_transition
426 BEFORE UPDATE ON gwk.message
427 FOR EACH ROW EXECUTE FUNCTION gwk.assert_transition('message');
428
429CREATE TRIGGER message_born_initial
430 BEFORE INSERT ON gwk.message
431 FOR EACH ROW EXECUTE FUNCTION gwk.assert_initial_state('message', 'accepted');
432
433CREATE TRIGGER message_no_delete
434 BEFORE DELETE ON gwk.message
435 FOR EACH ROW EXECUTE FUNCTION gwk.forbid_state_row_delete('message');
436
437CREATE TRIGGER message_no_truncate
438 BEFORE TRUNCATE ON gwk.message
439 FOR EACH STATEMENT EXECUTE FUNCTION gwk.forbid_state_row_delete('message');
440
441ALTER TABLE gwk.message ENABLE ALWAYS TRIGGER message_transition;
442ALTER TABLE gwk.message ENABLE ALWAYS TRIGGER message_born_initial;
443ALTER TABLE gwk.message ENABLE ALWAYS TRIGGER message_no_delete;
444ALTER TABLE gwk.message ENABLE ALWAYS TRIGGER message_no_truncate;
445
446CREATE TABLE gwk.command (
447 id text PRIMARY KEY,
448 version bigint NOT NULL DEFAULT 1 CHECK (version BETWEEN 1 AND 4294967295),
449 state text NOT NULL DEFAULT 'issued'
450 CHECK (state IN ('issued', 'targeted', 'signaled',
451 'verification_complete')),
452 kind text NOT NULL,
453 target text,
454 actor jsonb,
455 idempotency_key text UNIQUE,
456 -- The verified RESULT, distinct from the progress spine: present exactly
457 -- when the command is verification_complete, written in the same
458 -- transaction as that terminal transition — this CHECK is what makes a
459 -- split write unrepresentable.
460 outcome text CHECK (outcome IN ('clean', 'partial', 'unknown')),
461 created_at timestamptz NOT NULL DEFAULT now(),
462 updated_at timestamptz NOT NULL DEFAULT now(),
463 CONSTRAINT outcome_iff_verification_complete
464 CHECK ((state = 'verification_complete') = (outcome IS NOT NULL))
465);
466
467CREATE TRIGGER command_transition
468 BEFORE UPDATE ON gwk.command
469 FOR EACH ROW EXECUTE FUNCTION gwk.assert_transition('command');
470
471CREATE TRIGGER command_born_initial
472 BEFORE INSERT ON gwk.command
473 FOR EACH ROW EXECUTE FUNCTION gwk.assert_initial_state('command', 'issued');
474
475CREATE TRIGGER command_no_delete
476 BEFORE DELETE ON gwk.command
477 FOR EACH ROW EXECUTE FUNCTION gwk.forbid_state_row_delete('command');
478
479CREATE TRIGGER command_no_truncate
480 BEFORE TRUNCATE ON gwk.command
481 FOR EACH STATEMENT EXECUTE FUNCTION gwk.forbid_state_row_delete('command');
482
483ALTER TABLE gwk.command ENABLE ALWAYS TRIGGER command_transition;
484ALTER TABLE gwk.command ENABLE ALWAYS TRIGGER command_born_initial;
485ALTER TABLE gwk.command ENABLE ALWAYS TRIGGER command_no_delete;
486ALTER TABLE gwk.command ENABLE ALWAYS TRIGGER command_no_truncate;
487
488CREATE TABLE gwk.gate (
489 id text PRIMARY KEY,
490 version bigint NOT NULL DEFAULT 1 CHECK (version BETWEEN 1 AND 4294967295),
491 attempt_id text REFERENCES gwk.attempt(id),
492 phase_ref text,
493 -- OPEN kind (verify/review/security/eval/cert/...): new gate kinds are
494 -- additive, never a schema change
495 kind text,
496 verdict text NOT NULL DEFAULT 'pending'
497 CHECK (verdict IN ('pending', 'pass', 'fail', 'partial')),
498 evidence_ref text,
499 created_at timestamptz NOT NULL DEFAULT now(),
500 updated_at timestamptz NOT NULL DEFAULT now()
501);
502
503CREATE TABLE gwk.authority_grant (
504 id text PRIMARY KEY,
505 grantee jsonb NOT NULL,
506 action_class text NOT NULL,
507 scope text,
508 granted_at timestamptz NOT NULL DEFAULT now(),
509 expires_at timestamptz,
510 -- Set together when the grant is withdrawn. Without them a revoke could only
511 -- be expressed by backdating `expires_at`, which makes "revoked for cause"
512 -- and "expired on schedule" the same row — and `gw authority list` reads this
513 -- table as the audit trail, so that difference is the point of it.
514 revoked_at timestamptz,
515 revoke_reason text,
516 receipt_id text
517);
518
519-- Append-only attestation ledger (no version/updated_at: receipts are facts).
520CREATE TABLE gwk.receipt (
521 id text PRIMARY KEY,
522 actor jsonb NOT NULL,
523 action text NOT NULL,
524 subject_type text NOT NULL,
525 subject_id text NOT NULL,
526 from_state text,
527 to_state text,
528 observed_basis text,
529 ts timestamptz NOT NULL DEFAULT now()
530);
531
532CREATE FUNCTION gwk.forbid_receipt_mutation() RETURNS trigger
533LANGUAGE plpgsql AS $$
534BEGIN
535 RAISE EXCEPTION 'gwk.receipt is append-only (% refused)', TG_OP;
536END $$;
537
538CREATE TRIGGER receipt_append_only
539 BEFORE UPDATE OR DELETE ON gwk.receipt
540 FOR EACH ROW EXECUTE FUNCTION gwk.forbid_receipt_mutation();
541
542-- Statement-level TRUNCATE cover, as on gwk.event.
543CREATE TRIGGER receipt_no_truncate
544 BEFORE TRUNCATE ON gwk.receipt
545 FOR EACH STATEMENT EXECUTE FUNCTION gwk.forbid_receipt_mutation();
546
547ALTER TABLE gwk.receipt ENABLE ALWAYS TRIGGER receipt_append_only;
548ALTER TABLE gwk.receipt ENABLE ALWAYS TRIGGER receipt_no_truncate;
549
550CREATE TABLE gwk.evidence (
551 id text PRIMARY KEY,
552 -- OPEN kind (transcript/diff/log/...); no format column
553 kind text NOT NULL,
554 ref text NOT NULL,
555 digest text,
556 byte_size numeric(20,0)
557 CHECK (byte_size >= 0 AND byte_size <= 18446744073709551615),
558 created_at timestamptz NOT NULL DEFAULT now()
559);
560
561CREATE TABLE gwk.attention_item (
562 id text PRIMARY KEY,
563 kind text NOT NULL,
564 summary text NOT NULL,
565 subject_ref text,
566 raised_by jsonb,
567 raised_at timestamptz NOT NULL DEFAULT now(),
568 resolved_at timestamptz,
569 resolution text
570);
571
572-- Attention deduplicates by (kind, subject_ref) WHILE UNRESOLVED: a page fires
573-- once per real problem, not once per retry of the command that hit it. The
574-- predicate is what makes the same pair raisable again after someone resolves
575-- it — a recurring problem is a new item, not a reopened one.
576--
577-- NULL `subject_ref` stays distinct (the PostgreSQL default): an item about
578-- nothing in particular is not the same item as another about nothing in
579-- particular. The kernel's own page path always names a subject, so this only
580-- affects an explicit RaiseAttention that omits one.
581CREATE UNIQUE INDEX attention_item_unresolved_dedup
582 ON gwk.attention_item (kind, subject_ref)
583 WHERE resolved_at IS NULL;
584
585-- The only projection a replay cannot rebuild. `gwk.receipt` is the other one,
586-- and it is append-only; this table is not, because resolving an item is an
587-- UPDATE. But the kernel's own page path writes the row directly inside the
588-- refused command's transaction with no event behind it, so a deleted item is
589-- gone from every copy at once — and `derived_records`, which is what a
590-- checkpoint and a scratch rebuild hash, deliberately leaves this table out.
591-- Nothing else would notice. The refusal's receipt survives either way, so what
592-- these guards protect is the operator's queue rather than the audit trail.
593CREATE TRIGGER attention_item_no_delete
594 BEFORE DELETE ON gwk.attention_item
595 FOR EACH ROW EXECUTE FUNCTION gwk.forbid_state_row_delete('attention_item');
596
597CREATE TRIGGER attention_item_no_truncate
598 BEFORE TRUNCATE ON gwk.attention_item
599 FOR EACH STATEMENT EXECUTE FUNCTION gwk.forbid_state_row_delete('attention_item');
600
601ALTER TABLE gwk.attention_item ENABLE ALWAYS TRIGGER attention_item_no_delete;
602ALTER TABLE gwk.attention_item ENABLE ALWAYS TRIGGER attention_item_no_truncate;
603
604-- `released_at`/`disposition` are what make a released worktree distinguishable
605-- from a live one in the projection. Without them the release is recoverable
606-- only by replaying the log, and `projection get worktree <id>` — a first-class
607-- read in the CLI surface — could not answer "is anyone still in this tree?".
608-- The lease beside it carries the same pair for the same reason.
609CREATE TABLE gwk.worktree (
610 id text PRIMARY KEY,
611 repo text NOT NULL,
612 path text NOT NULL,
613 branch text NOT NULL,
614 base_sha text,
615 lease_id text REFERENCES gwk.lease(id),
616 dirty boolean NOT NULL DEFAULT false,
617 unpushed boolean NOT NULL DEFAULT false,
618 released_at timestamptz,
619 disposition text,
620 created_at timestamptz NOT NULL DEFAULT now()
621);
622
623-- A dispatch node is bookkeeping over the spawn tree, NOT one of the four
624-- public state machines: `state` is an OPEN bounded lifecycle label with no
625-- seeded edge set, so `assert_transition` (which trusts gwk.transition) does
626-- not apply. What DOES apply is the CAS discipline every versioned row owes,
627-- because concurrent writers race the same node.
628CREATE TABLE gwk.dispatch_node (
629 id text PRIMARY KEY,
630 version bigint NOT NULL DEFAULT 1 CHECK (version BETWEEN 1 AND 4294967295),
631 parent_id text REFERENCES gwk.dispatch_node(id),
632 attempt_id text REFERENCES gwk.attempt(id),
633 kind text NOT NULL,
634 state text NOT NULL DEFAULT 'registered',
635 label text,
636 created_at timestamptz NOT NULL DEFAULT now(),
637 updated_at timestamptz NOT NULL DEFAULT now()
638);
639
640-- Version CAS without edge legality — the guard an open-lifecycle versioned
641-- table needs. Deliberately NOT gwk.assert_transition: that one refuses any
642-- state change lacking a gwk.transition row, and dispatch_node has no seeded
643-- edges by design, so every transition would be refused.
644CREATE FUNCTION gwk.assert_version_cas() RETURNS trigger
645LANGUAGE plpgsql AS $$
646BEGIN
647 IF NEW.version IS DISTINCT FROM OLD.version + 1 THEN
648 RAISE EXCEPTION '% version must advance by exactly 1 (got % -> %)',
649 TG_ARGV[0], OLD.version, NEW.version;
650 END IF;
651 RETURN NEW;
652END $$;
653
654CREATE TRIGGER dispatch_node_cas
655 BEFORE UPDATE ON gwk.dispatch_node
656 FOR EACH ROW EXECUTE FUNCTION gwk.assert_version_cas('dispatch_node');
657CREATE TRIGGER dispatch_node_no_delete
658 BEFORE DELETE ON gwk.dispatch_node
659 FOR EACH ROW EXECUTE FUNCTION gwk.forbid_state_row_delete('dispatch_node');
660
661ALTER TABLE gwk.dispatch_node ENABLE ALWAYS TRIGGER dispatch_node_cas;
662ALTER TABLE gwk.dispatch_node ENABLE ALWAYS TRIGGER dispatch_node_no_delete;
663
664-- The orchestrator's crash-recovery snapshot, latest-per-orchestrator.
665--
666-- Distinct from `gwk-domain::checkpoint::Checkpoint`, which is the kernel's own
667-- projection-hash checkpoint: this one is the orchestrator's impression of its
668-- own in-flight work at crash time. Recovery re-reads the live rows; this is
669-- the crash-time impression, not truth.
670--
671-- `seq` is the monotonic per-orchestrator counter and is guarded below. It is a
672-- resume cursor, so a rewind would let recovery restart from state that has
673-- already been superseded.
674CREATE TABLE gwk.orchestrator_checkpoint (
675 orchestrator_id text PRIMARY KEY,
676 seq numeric(20,0) NOT NULL
677 CHECK (seq >= 0 AND seq <= 18446744073709551615),
678 native_session_ref text,
679 active_goal text,
680 active_step_ref text,
681 latest_command_ref text,
682 open_attempts jsonb,
683 leases jsonb,
684 pending_approvals jsonb,
685 budget_cursor jsonb,
686 updated_at timestamptz NOT NULL DEFAULT now()
687);
688
689CREATE FUNCTION gwk.assert_checkpoint_seq_advances() RETURNS trigger
690LANGUAGE plpgsql AS $$
691BEGIN
692 IF NEW.seq <= OLD.seq THEN
693 RAISE EXCEPTION 'orchestrator_checkpoint seq must advance (got % -> %)',
694 OLD.seq, NEW.seq;
695 END IF;
696 RETURN NEW;
697END $$;
698
699CREATE TRIGGER orchestrator_checkpoint_seq
700 BEFORE UPDATE ON gwk.orchestrator_checkpoint
701 FOR EACH ROW EXECUTE FUNCTION gwk.assert_checkpoint_seq_advances();
702
703ALTER TABLE gwk.orchestrator_checkpoint ENABLE ALWAYS TRIGGER orchestrator_checkpoint_seq;
704
705-- Records that entered the log through ingestion rather than through an
706-- aggregate's lifecycle: memory, knowledge, graph snapshots, eval verdicts,
707-- insights, cost, health, session, config, checkpoints, rounds, findings.
708--
709-- `kind` is the one CLOSED classification column in this file. Everywhere else
710-- an open bounded string is preferred so a new label is additive; here the
711-- closed set IS the property — the absence of an import path is load-bearing,
712-- and an open column would let `import` in as data. The CHECK
713-- lists the twelve accepted kinds so the refusal happens in the database and
714-- not only in the process that wrote the row.
715--
716-- No version, no state, no updated_at: an ingested record is a fact that
717-- arrived, like gwk.receipt. `event_seq` points back at the log entry that
718-- produced it, which is the only thing that can prove the row — ingestion
719-- writes no gwk.transition.
720CREATE TABLE gwk.ingested_record (
721 id text PRIMARY KEY,
722 kind text NOT NULL
723 CHECK (kind IN ('memory', 'knowledge', 'graph_snapshot',
724 'eval_verdict', 'insight', 'cost', 'health',
725 'session', 'config', 'checkpoint', 'round',
726 'finding')),
727 -- Bounded inline content, or a descriptor of the blob beside it. The 64 KiB
728 -- bound belongs to the append path (it bounds the whole event payload); a
729 -- column CHECK here would be a second, drifting copy of it.
730 payload jsonb NOT NULL,
731 payload_ref jsonb,
732 ingested_by jsonb,
733 event_seq numeric(20,0) NOT NULL
734 CHECK (event_seq >= 0 AND event_seq <= 18446744073709551615),
735 ingested_at timestamptz NOT NULL DEFAULT now()
736);
737
738CREATE FUNCTION gwk.forbid_ingested_record_mutation() RETURNS trigger
739LANGUAGE plpgsql AS $$
740BEGIN
741 RAISE EXCEPTION 'gwk.ingested_record is append-only (% refused)', TG_OP;
742END $$;
743
744CREATE TRIGGER ingested_record_append_only
745 BEFORE UPDATE OR DELETE ON gwk.ingested_record
746 FOR EACH ROW EXECUTE FUNCTION gwk.forbid_ingested_record_mutation();
747
748-- Statement-level TRUNCATE cover, as on gwk.event and gwk.receipt.
749CREATE TRIGGER ingested_record_no_truncate
750 BEFORE TRUNCATE ON gwk.ingested_record
751 FOR EACH STATEMENT EXECUTE FUNCTION gwk.forbid_ingested_record_mutation();
752
753ALTER TABLE gwk.ingested_record ENABLE ALWAYS TRIGGER ingested_record_append_only;
754ALTER TABLE gwk.ingested_record ENABLE ALWAYS TRIGGER ingested_record_no_truncate;
755
756COMMIT;
757"#;
758
759pub const CONTRACT_SQL_SHA256: &str =
766 "158da24f2d5b24dd82128db463512b5a7672f1849817779fdea4a11676e5ec30";