@stackbone/cli 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/README.md +1 -1
- package/main.js +26442 -9861
- package/migrations/0028_browser_provider_system_secrets.sql +39 -0
- package/migrations/0029_workspace_settings.sql +37 -0
- package/migrations/0030_run_steps_connection_hook.sql +27 -0
- package/migrations/0031_recurring_job_executions.sql +51 -0
- package/migrations/0032_run_steps_external_step_id.sql +48 -0
- package/migrations/0033_runtime_identity.sql +25 -0
- package/migrations/0034_guardrails.sql +92 -0
- package/migrations/0035_run_steps_guardrail.sql +41 -0
- package/migrations/0036_guardrail_attachment_units.sql +61 -0
- package/migrations/0037_runs_entity_kind.sql +45 -0
- package/migrations/0038_evals.sql +272 -0
- package/migrations/0039_eval_case_results_idempotency.sql +22 -0
- package/migrations/0040_eval_case_results_run_idx.sql +27 -0
- package/migrations/0041_eval_criterion_workflow_name.sql +68 -0
- package/migrations/0042_eval_case_results_ended.sql +64 -0
- package/migrations/0043_eval_suite_approval_policy.sql +50 -0
- package/migrations/0044_eval_persona_simulator.sql +88 -0
- package/migrations/0045_eval_case_description.sql +34 -0
- package/package.json +1 -1
- package/runtime-deps/@stackbone/workflow-world/index.js +71 -71
- package/stackbone-cli-0.3.0.tgz +0 -0
- package/stackbone-cli-0.2.8.tgz +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
-- 0028_browser_provider_system_secrets.sql — adopt the browser-provider
|
|
2
|
+
-- credentials an operator created BY HAND (STA-288 / STA-290).
|
|
3
|
+
--
|
|
4
|
+
-- WHY THESE ROWS EXIST AT ALL: until now the documented way to point the browser
|
|
5
|
+
-- tooling at the managed remote browser was to type `BROWSERBASE_API_KEY` and
|
|
6
|
+
-- `BROWSERBASE_PROJECT_ID` straight into the Studio → Secrets screen (see the SDK's
|
|
7
|
+
-- browser-tools reference). Those rows landed the only way that screen can write
|
|
8
|
+
-- them: `is_system = false`. Live boxes therefore carry hand-made, non-system rows
|
|
9
|
+
-- under names that are now RESERVED (`RESERVED_BROWSER_PROVIDER_SECRET_NAMES` in
|
|
10
|
+
-- libs/shared/validators/src/lib/studio/studio-secrets.ts).
|
|
11
|
+
--
|
|
12
|
+
-- WHY RESERVING ALONE IS NOT ENOUGH: reservation blocks creating, overwriting and
|
|
13
|
+
-- deleting a name through the Secrets surface. It does not touch what is already
|
|
14
|
+
-- stored. Without this migration the operator would be left staring at a listed row
|
|
15
|
+
-- they can neither edit nor delete — a dead end. Flipping the flag is what hides the
|
|
16
|
+
-- row from the listing (`listSecrets` filters `is_system = false`), blocks revealing
|
|
17
|
+
-- it, and hands custody to the privileged browser-provider route.
|
|
18
|
+
--
|
|
19
|
+
-- The names are duplicated here on purpose: SQL cannot import the TypeScript
|
|
20
|
+
-- constant, and platform-schema deliberately depends on no other lib. The
|
|
21
|
+
-- browser-provider mode key (`BROWSER_MODE`) is listed too even though nothing wrote
|
|
22
|
+
-- it before this feature — a box cannot have one, and including it keeps the SQL and
|
|
23
|
+
-- the registry readable as the same list. `STACKBONE_BROWSER_CDP_URL` is absent
|
|
24
|
+
-- because the `STACKBONE_` prefix has always been reserved, so a hand-made row under
|
|
25
|
+
-- that name never existed.
|
|
26
|
+
--
|
|
27
|
+
-- ONLY the flag moves — `nonce` and `ciphertext` are untouched, so the value keeps
|
|
28
|
+
-- decrypting with the box's per-agent key and the browser tooling (which reads
|
|
29
|
+
-- through `client.secrets`, a facade that deliberately does not filter system rows)
|
|
30
|
+
-- keeps working across the flip.
|
|
31
|
+
--
|
|
32
|
+
-- IDEMPOTENT: the `is_system = false` guard makes a second run affect zero rows and
|
|
33
|
+
-- stops it churning `updated_at` on rows already adopted. On a box that never had
|
|
34
|
+
-- these secrets the statement is a plain no-op.
|
|
35
|
+
UPDATE stackbone_platform.secrets
|
|
36
|
+
SET is_system = true,
|
|
37
|
+
updated_at = now()
|
|
38
|
+
WHERE name IN ('BROWSER_MODE', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID')
|
|
39
|
+
AND is_system = false;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
-- 0029_workspace_settings.sql — the PLATFORM-owned operational settings of a
|
|
2
|
+
-- workspace (STA-311). One row, one JSON payload, plus the author and the
|
|
3
|
+
-- timestamp of the last change.
|
|
4
|
+
--
|
|
5
|
+
-- Why a table of its own instead of a key inside `agent_config` (0010): that
|
|
6
|
+
-- payload is validated against the JSON Schema the build extracts from the
|
|
7
|
+
-- creator's `config.schema.ts`, and `z.toJSONSchema` emits a CLOSED object by
|
|
8
|
+
-- default (`additionalProperties: false`). The moment a creator declares a config
|
|
9
|
+
-- schema, a platform key living inside their payload would be rejected outright.
|
|
10
|
+
-- These knobs belong to the platform, not to the creator, so they get their own
|
|
11
|
+
-- store with a schema the platform owns.
|
|
12
|
+
--
|
|
13
|
+
-- Why no version history (unlike `agent_config_versions`, 0007): these are
|
|
14
|
+
-- operational settings, not a creator artifact. Last-write-wins with an author
|
|
15
|
+
-- stamp is enough — there is nothing to roll back to.
|
|
16
|
+
--
|
|
17
|
+
-- The payload defaults to `{}`; the DEFAULTS live in the platform schema
|
|
18
|
+
-- (`@stackbone/validators` → `studio-workspace-settings.ts`), never in the DDL,
|
|
19
|
+
-- so the runtime, the SDK and the Studio screen all read one source of truth.
|
|
20
|
+
|
|
21
|
+
-- `updated_at` is NULLABLE and carries NO default on purpose: the seed row below
|
|
22
|
+
-- would otherwise stamp every fresh box with the migration's wall clock, and
|
|
23
|
+
-- "nobody ever configured this workspace" would be indistinguishable from a real
|
|
24
|
+
-- edit. NULL means untouched; a write always sets it explicitly.
|
|
25
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.workspace_settings (
|
|
26
|
+
id int PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
|
27
|
+
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
28
|
+
updated_at timestamptz,
|
|
29
|
+
updated_by text
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
-- Seed the single row so readers never have to branch on "no row yet".
|
|
33
|
+
-- Idempotent: re-running the migration leaves an already-written payload (and
|
|
34
|
+
-- its author stamp) untouched.
|
|
35
|
+
INSERT INTO stackbone_platform.workspace_settings (id, payload)
|
|
36
|
+
VALUES (1, '{}'::jsonb)
|
|
37
|
+
ON CONFLICT (id) DO NOTHING;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- 0030_run_steps_connection_hook.sql — widen the run_steps type CHECK to admit
|
|
2
|
+
-- two new activity step kinds alongside the existing `workflow_step` /
|
|
3
|
+
-- `model_call` rows (0018_run_steps_v2.sql).
|
|
4
|
+
--
|
|
5
|
+
-- `connection` — one row per outbound connector call (`callConnector(...)` /
|
|
6
|
+
-- `connection(id).op(...)`), instrumented in the runtime the same way a direct
|
|
7
|
+
-- `client.ai` call records a `model_call`. Carries the call's input
|
|
8
|
+
-- (connector/operation/args), output and error; no token columns.
|
|
9
|
+
--
|
|
10
|
+
-- `hook` — one row per `requestApproval(...)` durable pause (the WDK hook
|
|
11
|
+
-- that backs HITL). Projected running when the approval is created and resolved
|
|
12
|
+
-- to completed/failed when a human decides or the wait times out. The
|
|
13
|
+
-- `approvals` row stays the source of truth for the inbox; this row only mirrors
|
|
14
|
+
-- the pause onto the run-step timeline so it reads inline in the trace viewer.
|
|
15
|
+
--
|
|
16
|
+
-- The pg_notify trigger (`notify_run_step_event`) already streams ANY type over
|
|
17
|
+
-- SSE, so no trigger change is needed — new-typed rows flow live for free.
|
|
18
|
+
|
|
19
|
+
-- The 0018 inline column CHECK is auto-named `run_steps_type_check` by Postgres.
|
|
20
|
+
-- Drop it (guarded) and recreate it widened as a named constraint so future
|
|
21
|
+
-- migrations can target it by a stable name.
|
|
22
|
+
ALTER TABLE stackbone_platform.run_steps
|
|
23
|
+
DROP CONSTRAINT IF EXISTS run_steps_type_check;
|
|
24
|
+
|
|
25
|
+
ALTER TABLE stackbone_platform.run_steps
|
|
26
|
+
ADD CONSTRAINT run_steps_type_check
|
|
27
|
+
CHECK (type IN ('workflow_step', 'model_call', 'connection', 'hook'));
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
-- 0031_recurring_job_executions.sql — the box's record of WHEN each RECURRING
|
|
2
|
+
-- job last ran, and how it ended (STA-343).
|
|
3
|
+
--
|
|
4
|
+
-- Why a table at all: BullMQ cannot answer "when did this repeatable last run?".
|
|
5
|
+
-- Every recurring job the runtime declares drops its completed ticks by design
|
|
6
|
+
-- (`removeOnComplete: true` in `DEFAULT_RECURRING_JOB_OPTS`), so the finished tick
|
|
7
|
+
-- is gone from Redis the moment it succeeds. The recurring-jobs REGISTRY writes
|
|
8
|
+
-- this row itself, from the wrapper it already puts around every tick handler, so
|
|
9
|
+
-- a job author gets the record for free by declaring an entry.
|
|
10
|
+
--
|
|
11
|
+
-- Writing the PREVIOUS occurrence down also removes work downstream: the
|
|
12
|
+
-- countdown a screen shows is `(now - previous) / (next - previous)`, and BullMQ
|
|
13
|
+
-- only hands out `next` — deriving `previous` from a cron pattern is real work we
|
|
14
|
+
-- would rather not do when the tick already knew the answer.
|
|
15
|
+
--
|
|
16
|
+
-- job_id — the registry's scheduler id (e.g. `decl:digest#0`,
|
|
17
|
+
-- `sub:<link-id>`). PRIMARY KEY: ONE row per job, overwritten on
|
|
18
|
+
-- each tick, so the table is bounded by the number of DECLARED
|
|
19
|
+
-- jobs and never grows with the number of ticks. This is a "last
|
|
20
|
+
-- execution" record, not a history.
|
|
21
|
+
-- family — the scheduler-id prefix that namespaces the job (`decl:`,
|
|
22
|
+
-- `dyn:`, `sub:`, `connector-token-refresh:`). Kept even though
|
|
23
|
+
-- the id starts with it: a reader groups by family, and an id
|
|
24
|
+
-- like `sub:<uuid>` is not self-describing without the registry's
|
|
25
|
+
-- own declaration.
|
|
26
|
+
-- started_at — measured by the wrapper BEFORE the handler runs.
|
|
27
|
+
-- finished_at — measured by the wrapper when the handler settles. Both come
|
|
28
|
+
-- from the BOX's clock (never `now()` here), so a reader can
|
|
29
|
+
-- compare them against BullMQ's `next` without mixing clocks.
|
|
30
|
+
-- status — terminal outcome of the tick: `done` or `failed`. Never
|
|
31
|
+
-- in-flight: the row is written ONCE, when the tick settles, so
|
|
32
|
+
-- a box that dies mid-tick leaves no half-written row.
|
|
33
|
+
-- error — the failure reason on `failed`; NULL on `done`. Always
|
|
34
|
+
-- rewritten (never merged), so a green row can't carry the stale
|
|
35
|
+
-- reason of the failure before it.
|
|
36
|
+
--
|
|
37
|
+
-- NOT seeded: a job that has never run must report NO last execution rather than
|
|
38
|
+
-- a fabricated one, so nothing here defaults to `now()` and no row is planted for
|
|
39
|
+
-- a declared-but-never-run job.
|
|
40
|
+
--
|
|
41
|
+
-- Idempotent (CREATE TABLE IF NOT EXISTS): every box re-applies the migration set
|
|
42
|
+
-- on boot, and an existing box must keep everything it already recorded.
|
|
43
|
+
|
|
44
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.recurring_job_executions (
|
|
45
|
+
job_id text PRIMARY KEY,
|
|
46
|
+
family text NOT NULL,
|
|
47
|
+
started_at timestamptz NOT NULL,
|
|
48
|
+
finished_at timestamptz NOT NULL,
|
|
49
|
+
status text NOT NULL CHECK (status IN ('done', 'failed')),
|
|
50
|
+
error text
|
|
51
|
+
);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
-- 0032_run_steps_external_step_id.sql — turn `run_steps` into the journal of ANY
|
|
2
|
+
-- execution, not just a workflow's. Two changes, one migration:
|
|
3
|
+
--
|
|
4
|
+
-- 1. RENAME the correlation key. `world_step_id` (0018_run_steps_v2.sql) was
|
|
5
|
+
-- named after its only producer at the time: the workflow projector storing
|
|
6
|
+
-- the World `stepId`. A deep-agent chat turn now writes its `tool_call_id`
|
|
7
|
+
-- into the same slot, so the column becomes `external_step_id` — "the
|
|
8
|
+
-- producer's own correlation key", whoever the producer is. Persisting it is
|
|
9
|
+
-- not cosmetic: a tool that paused for human approval resumes in ANOTHER
|
|
10
|
+
-- process, which has no row id in memory and can only find the open step by
|
|
11
|
+
-- this key.
|
|
12
|
+
--
|
|
13
|
+
-- Postgres stores an index's predicate and columns by attribute NUMBER, so
|
|
14
|
+
-- renaming the column rewrites `(run_id, external_step_id) WHERE
|
|
15
|
+
-- external_step_id IS NOT NULL` automatically — the partial unique index
|
|
16
|
+
-- keeps biting with no rebuild. Only its NAME has to be fixed by hand, so it
|
|
17
|
+
-- stops lying too.
|
|
18
|
+
--
|
|
19
|
+
-- 2. WIDEN the type CHECK with two kinds the chat journal needs:
|
|
20
|
+
-- `tool_call` — one tool invocation inside a turn.
|
|
21
|
+
-- `agent` — one subagent delegation (the `task` tool call and the
|
|
22
|
+
-- subagent boundary folded into ONE row; the tools it calls
|
|
23
|
+
-- hang off it via parent_step_id).
|
|
24
|
+
-- The four existing types are reused verbatim. 0030 already made
|
|
25
|
+
-- `run_steps_type_check` a NAMED constraint precisely so a later migration
|
|
26
|
+
-- could target it by name — reuse it.
|
|
27
|
+
--
|
|
28
|
+
-- The pg_notify trigger (`notify_run_step_event`) streams ANY type and reads the
|
|
29
|
+
-- correlation column by name nowhere, so no trigger change is needed — rows of
|
|
30
|
+
-- the new types flow live over SSE for free.
|
|
31
|
+
--
|
|
32
|
+
-- Nothing writes `tool_call` / `agent` rows yet: this migration only opens the
|
|
33
|
+
-- door for the instrumentation that follows.
|
|
34
|
+
|
|
35
|
+
ALTER TABLE stackbone_platform.run_steps
|
|
36
|
+
RENAME COLUMN world_step_id TO external_step_id;
|
|
37
|
+
|
|
38
|
+
ALTER INDEX IF EXISTS stackbone_platform.run_steps_world_step_uq
|
|
39
|
+
RENAME TO run_steps_external_step_uq;
|
|
40
|
+
|
|
41
|
+
ALTER TABLE stackbone_platform.run_steps
|
|
42
|
+
DROP CONSTRAINT IF EXISTS run_steps_type_check;
|
|
43
|
+
|
|
44
|
+
ALTER TABLE stackbone_platform.run_steps
|
|
45
|
+
ADD CONSTRAINT run_steps_type_check
|
|
46
|
+
CHECK (type IN (
|
|
47
|
+
'workflow_step', 'model_call', 'connection', 'hook', 'tool_call', 'agent'
|
|
48
|
+
));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- 0032_runtime_identity.sql — the identity a runtime container derives for
|
|
2
|
+
-- itself on first boot (STA-350).
|
|
3
|
+
--
|
|
4
|
+
-- The image used to demand an installation id and an HMAC secret as environment
|
|
5
|
+
-- variables. Neither is real configuration: the installation id only has to stay
|
|
6
|
+
-- stable across restarts of this container, and the HMAC secret goes unused
|
|
7
|
+
-- until the operator links the deployment to a control plane. Making the
|
|
8
|
+
-- customer invent both before the box will start was pure friction, so the box
|
|
9
|
+
-- now mints them once and remembers them here.
|
|
10
|
+
--
|
|
11
|
+
-- One row, like `workspace_settings` (0029): this table describes THE runtime,
|
|
12
|
+
-- not a collection of anything. The row IS the identity, so there is no seed —
|
|
13
|
+
-- its absence is exactly what "this database has never hosted a runtime" means,
|
|
14
|
+
-- and that absence is the signal the boot warns about before minting a fresh one.
|
|
15
|
+
--
|
|
16
|
+
-- Deliberately NOT encrypted with `STACKBONE_SECRET_KEY`: an operator debugging a
|
|
17
|
+
-- link failure has to be able to read the signing secret out of their own
|
|
18
|
+
-- database, and this database already holds every encrypted secret plus every
|
|
19
|
+
-- run, so storing one more value in it adds no new exposure.
|
|
20
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.runtime_identity (
|
|
21
|
+
id int PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
|
22
|
+
installation_id uuid NOT NULL,
|
|
23
|
+
hmac_secret text NOT NULL,
|
|
24
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
25
|
+
);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
-- 0034_guardrails.sql — the box's own GUARDRAIL policies (STA-359): the rules an
|
|
2
|
+
-- operator configures to stop a turn before it ever reaches the agent.
|
|
3
|
+
--
|
|
4
|
+
-- Numbered 0034: slots 0032 and 0033 were taken by the trunk while this feature
|
|
5
|
+
-- was being built (`0032_run_steps_external_step_id`, `0033_runtime_identity`).
|
|
6
|
+
-- The runner keys `schema_migrations` by the 4-digit prefix, so two files sharing
|
|
7
|
+
-- a number are not merely untidy — the second one to sort is read as a rewrite of
|
|
8
|
+
-- a version already applied and aborts the whole boot. Renumber against the tip
|
|
9
|
+
-- you are actually merging into, not the one you branched from.
|
|
10
|
+
--
|
|
11
|
+
-- Why the policy lives in the DATABASE and not in the agent bundle: a guardrail
|
|
12
|
+
-- is an OPERATOR knob, not a creator artifact. It has to be editable from Studio
|
|
13
|
+
-- and take effect on the NEXT turn, with no rebuild and no restart — which it
|
|
14
|
+
-- cannot do if it is baked into a compiled graph. The runtime therefore reads the
|
|
15
|
+
-- effective policy per turn (behind a short cache), straight off these rows.
|
|
16
|
+
--
|
|
17
|
+
-- guardrail_definitions — one row per named rule.
|
|
18
|
+
-- name — operator-facing, UNIQUE per box: it is what a blocked user
|
|
19
|
+
-- reads in the reason, so two rules may not share it.
|
|
20
|
+
-- check_type — which catalog check runs (`denylist`, `length_cap`). The
|
|
21
|
+
-- catalog itself lives in `@stackbone/guardrails` and is served
|
|
22
|
+
-- over HTTP; this column is deliberately NOT constrained by a
|
|
23
|
+
-- CHECK, because a box running an older build must be able to
|
|
24
|
+
-- STORE a check type it cannot run rather than reject the write
|
|
25
|
+
-- and lose the operator's policy. An unknown type fails CLOSED
|
|
26
|
+
-- at evaluation time (the engine's own rule), which is the safe
|
|
27
|
+
-- place for that decision.
|
|
28
|
+
-- target — which side of the turn it inspects (`input` today).
|
|
29
|
+
-- action — what it does when it fires (`block` today).
|
|
30
|
+
-- params — the check's own settings, validated against the CATALOG at the
|
|
31
|
+
-- handler, never by the DDL. The catalog is the one description
|
|
32
|
+
-- of each check's shape; a CHECK constraint here would be a
|
|
33
|
+
-- second one that drifts.
|
|
34
|
+
-- enabled — a disabled rule NEVER fires. Kept as a flag rather than a
|
|
35
|
+
-- delete so an operator can switch a policy off for one incident
|
|
36
|
+
-- and back on afterwards without retyping it.
|
|
37
|
+
-- fail_open — what to do when the CHECK ITSELF cannot run (bad params, an
|
|
38
|
+
-- invalid pattern). FALSE = fail closed, the safe default for a
|
|
39
|
+
-- guard. Never confused with a real policy violation, which
|
|
40
|
+
-- blocks either way.
|
|
41
|
+
--
|
|
42
|
+
-- guardrail_attachments — which units a rule applies to. Created now, exercised
|
|
43
|
+
-- now only in its workspace-wide shape (`scope = 'workspace'`, `unit_id`
|
|
44
|
+
-- NULL), because the workspace-wide default is IMPLICIT in this slice: every
|
|
45
|
+
-- enabled definition applies. The per-unit resolution (a rule attached to one
|
|
46
|
+
-- agent or one workflow) is a later slice; the table ships now so the
|
|
47
|
+
-- resolver's contract — de-duplication, deterministic order, dangling rows
|
|
48
|
+
-- ignored — is real behaviour with tests rather than a promise.
|
|
49
|
+
-- Deleting a definition CASCADES its attachments: an attachment to a rule
|
|
50
|
+
-- that no longer exists is not a policy, it is a leak.
|
|
51
|
+
--
|
|
52
|
+
-- No defaults for the payload live here — the documented defaults live in the
|
|
53
|
+
-- platform schema (`@stackbone/validators` → `studio-guardrails.ts`), so the
|
|
54
|
+
-- runtime, the facade and the Studio form all read one source of truth. The
|
|
55
|
+
-- column defaults below are structural only (identity, timestamps, and the two
|
|
56
|
+
-- booleans whose absence would otherwise make a row unreadable).
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.guardrail_definitions (
|
|
59
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
60
|
+
name text NOT NULL,
|
|
61
|
+
check_type text NOT NULL,
|
|
62
|
+
target text NOT NULL,
|
|
63
|
+
action text NOT NULL,
|
|
64
|
+
params jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
65
|
+
enabled boolean NOT NULL DEFAULT true,
|
|
66
|
+
fail_open boolean NOT NULL DEFAULT false,
|
|
67
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
68
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
69
|
+
-- The acting user, as the verified JWT / signed actor header reported them.
|
|
70
|
+
-- NULL on the un-authenticated `stackbone dev` datapath.
|
|
71
|
+
created_by text,
|
|
72
|
+
updated_by text
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
-- The name is what a blocked user reads. Two rules sharing it would make the
|
|
76
|
+
-- reason ambiguous, so the uniqueness is enforced by the box, not by the screen.
|
|
77
|
+
CREATE UNIQUE INDEX IF NOT EXISTS guardrail_definitions_name_key
|
|
78
|
+
ON stackbone_platform.guardrail_definitions (name);
|
|
79
|
+
|
|
80
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.guardrail_attachments (
|
|
81
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
82
|
+
definition_id uuid NOT NULL
|
|
83
|
+
REFERENCES stackbone_platform.guardrail_definitions (id) ON DELETE CASCADE,
|
|
84
|
+
-- `workspace` is the only scope in this slice. Widened by the per-unit slice.
|
|
85
|
+
scope text NOT NULL,
|
|
86
|
+
-- The attached unit (an agent name, a workflow name). NULL for `workspace`.
|
|
87
|
+
unit_id text,
|
|
88
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
CREATE INDEX IF NOT EXISTS guardrail_attachments_definition_idx
|
|
92
|
+
ON stackbone_platform.guardrail_attachments (definition_id);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
-- 0035_run_steps_guardrail.sql — widen the run_steps type CHECK to admit the
|
|
2
|
+
-- GUARDRAIL decision step (STA-359), alongside `workflow_step` / `model_call`
|
|
3
|
+
-- (0018), `connection` / `hook` (0030) and `tool_call` / `agent` (0032, from the
|
|
4
|
+
-- trunk — see the note above the constraint below).
|
|
5
|
+
--
|
|
6
|
+
-- `guardrail` — one row per guardrail decision taken at a border. A blocked chat
|
|
7
|
+
-- turn writes exactly one: the name of the rule that fired, the side of the turn
|
|
8
|
+
-- it inspected, and the reason the user was shown. It exists so a blocked turn is
|
|
9
|
+
-- READABLE in the trace viewer — inline with the model calls and tool calls the
|
|
10
|
+
-- turn never got to make — instead of being an invisible non-event that only
|
|
11
|
+
-- shows up as a suspiciously short conversation.
|
|
12
|
+
--
|
|
13
|
+
-- One spelling, everywhere: `guardrail` is the `run_steps.type` value, the
|
|
14
|
+
-- neutral `AgentEvent` variant and the AG-UI `CUSTOM` frame name. The earlier
|
|
15
|
+
-- placeholder `guardrail_triggered` (docs only, never in code) is retired.
|
|
16
|
+
--
|
|
17
|
+
-- The pg_notify trigger (`notify_run_step_event`) already streams ANY type over
|
|
18
|
+
-- SSE, so no trigger change is needed — guardrail rows flow live for free.
|
|
19
|
+
|
|
20
|
+
-- 0030 recreated this as a NAMED constraint precisely so it could be targeted
|
|
21
|
+
-- here. Drop it (guarded — every box re-applies the whole set on boot) and
|
|
22
|
+
-- recreate it widened.
|
|
23
|
+
--
|
|
24
|
+
-- The list below names `tool_call` and `agent`, which NOTHING IN THIS FEATURE
|
|
25
|
+
-- WRITES: they arrived with `0032_run_steps_external_step_id` and belong to the
|
|
26
|
+
-- deep-agent turn journal. Recreating a named CHECK replaces it WHOLESALE rather
|
|
27
|
+
-- than adding to it, so naming only the types this feature cares about would
|
|
28
|
+
-- revoke those two the moment this file runs after 0032. Nothing would flag it:
|
|
29
|
+
-- the two migrations are different files, so there is no merge conflict, and the
|
|
30
|
+
-- damage would surface much later as a 23514 on the first tool call of every chat
|
|
31
|
+
-- turn. The rule for anyone touching this constraint again: carry the whole
|
|
32
|
+
-- accepted set forward, never just your own additions.
|
|
33
|
+
ALTER TABLE stackbone_platform.run_steps
|
|
34
|
+
DROP CONSTRAINT IF EXISTS run_steps_type_check;
|
|
35
|
+
|
|
36
|
+
ALTER TABLE stackbone_platform.run_steps
|
|
37
|
+
ADD CONSTRAINT run_steps_type_check
|
|
38
|
+
CHECK (type IN (
|
|
39
|
+
'workflow_step', 'model_call', 'connection', 'hook',
|
|
40
|
+
'tool_call', 'agent', 'guardrail'
|
|
41
|
+
));
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
-- 0036_guardrail_attachment_units.sql — attach a guardrail WHERE IT IS NEEDED
|
|
2
|
+
-- (STA-365), instead of every enabled rule applying to everything.
|
|
3
|
+
--
|
|
4
|
+
-- 0034 shipped `guardrail_attachments` already carrying `scope` + `unit_id`, but
|
|
5
|
+
-- only ever wrote the workspace-wide shape, because resolution at the time was
|
|
6
|
+
-- implicit: an enabled definition applied everywhere. This migration turns that
|
|
7
|
+
-- table into the real thing.
|
|
8
|
+
--
|
|
9
|
+
-- `scope` now takes three values:
|
|
10
|
+
--
|
|
11
|
+
-- `workspace` — the DEFAULT SET (`unit_id` NULL). It covers every agent and
|
|
12
|
+
-- every workflow, including ones added after the rule was written. This is
|
|
13
|
+
-- what makes a newly created agent protected with nobody remembering to do
|
|
14
|
+
-- anything.
|
|
15
|
+
-- `agent` / `workflow` — ONE named unit (`unit_id` = its name), so a high-risk
|
|
16
|
+
-- agent can be stricter than the rest without duplicating the policy.
|
|
17
|
+
--
|
|
18
|
+
-- Deliberately NOT a CHECK constraint on `scope`, for the same reason `check_type`
|
|
19
|
+
-- has none: a box running an older build must be able to STORE a scope it cannot
|
|
20
|
+
-- resolve rather than reject the write and lose the operator's policy. An
|
|
21
|
+
-- unrecognised scope is IGNORED at resolution time (the resolver's own rule),
|
|
22
|
+
-- which is the safe place for that decision — a stale or unreadable attachment
|
|
23
|
+
-- must never take a chat turn down.
|
|
24
|
+
--
|
|
25
|
+
-- Two things this file does add:
|
|
26
|
+
--
|
|
27
|
+
-- 1. UNIQUENESS per (rule, place). Attaching the same rule to the same unit twice
|
|
28
|
+
-- would run the same check twice on one turn — the resolver de-duplicates by
|
|
29
|
+
-- definition id anyway, but a duplicate row is a lie about the policy and it
|
|
30
|
+
-- would show up twice in the blast-radius summary the screen paints before an
|
|
31
|
+
-- edit or a delete. `COALESCE(unit_id, '')` is what makes the workspace shape
|
|
32
|
+
-- unique too: plain SQL NULLs are all distinct from each other, so without it
|
|
33
|
+
-- one rule could hold ten identical workspace attachments.
|
|
34
|
+
--
|
|
35
|
+
-- 2. A BACKFILL. Every definition stored under 0034 relied on the implicit
|
|
36
|
+
-- workspace-wide default. Once resolution is attachment-driven, a definition
|
|
37
|
+
-- with no attachment rows applies NOWHERE — so an upgrade that skipped this
|
|
38
|
+
-- step would silently switch off every guardrail an operator had already
|
|
39
|
+
-- configured. Giving each of them the workspace attachment they implicitly had
|
|
40
|
+
-- keeps behaviour identical across the upgrade.
|
|
41
|
+
|
|
42
|
+
-- The unique index must exist before the backfill, so a re-run cannot double the
|
|
43
|
+
-- backfilled rows.
|
|
44
|
+
CREATE UNIQUE INDEX IF NOT EXISTS guardrail_attachments_unit_key
|
|
45
|
+
ON stackbone_platform.guardrail_attachments (definition_id, scope, COALESCE(unit_id, ''));
|
|
46
|
+
|
|
47
|
+
-- Index the lookup the resolver's read actually makes: every attachment for the
|
|
48
|
+
-- box, joined back onto its definition. `definition_id` is already indexed by
|
|
49
|
+
-- 0034; this one serves the per-unit filter a large box would otherwise scan.
|
|
50
|
+
CREATE INDEX IF NOT EXISTS guardrail_attachments_unit_idx
|
|
51
|
+
ON stackbone_platform.guardrail_attachments (scope, unit_id);
|
|
52
|
+
|
|
53
|
+
INSERT INTO stackbone_platform.guardrail_attachments (definition_id, scope, unit_id)
|
|
54
|
+
SELECT d.id, 'workspace', NULL
|
|
55
|
+
FROM stackbone_platform.guardrail_definitions d
|
|
56
|
+
WHERE NOT EXISTS (
|
|
57
|
+
SELECT 1
|
|
58
|
+
FROM stackbone_platform.guardrail_attachments a
|
|
59
|
+
WHERE a.definition_id = d.id
|
|
60
|
+
)
|
|
61
|
+
ON CONFLICT DO NOTHING;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
-- 0037_runs_entity_kind.sql — record WHICH KIND of entity produced a run, so two
|
|
2
|
+
-- entries that share a name stop contaminating each other (STA-426).
|
|
3
|
+
--
|
|
4
|
+
-- 0020 denormalised the entity NAME onto the run and its comment claimed that
|
|
5
|
+
-- "combined with the existing `trigger` the UI knows the kind". That is not true:
|
|
6
|
+
-- `trigger` records WHY a run started (manual, cron, chat, workflow), not WHAT the
|
|
7
|
+
-- entity is — a workflow run started by cron carries `cron`, and a workflow that
|
|
8
|
+
-- opens an agent turn carries `chat`. So the name was doing identity work on its
|
|
9
|
+
-- own, and a deep agent named `sync` plus a workflow named `sync` landed in one row
|
|
10
|
+
-- set: every screen that filters runs by name showed the two mixed together.
|
|
11
|
+
--
|
|
12
|
+
-- The guardrails side of the codebase already settled this vocabulary — a guardrail
|
|
13
|
+
-- attachment is scoped to an `agent` or a `workflow` unit precisely because the KIND
|
|
14
|
+
-- is part of an entry's identity, never just the name. `entity_kind` brings the runs
|
|
15
|
+
-- table in line with it.
|
|
16
|
+
--
|
|
17
|
+
-- Deliberately NOT a CHECK constraint, for the same reason 0036's `scope` has none:
|
|
18
|
+
-- a box running an older build must be able to STORE a kind it cannot resolve rather
|
|
19
|
+
-- than reject the write and lose the run. An unrecognised kind simply never matches a
|
|
20
|
+
-- filter, which is the safe failure.
|
|
21
|
+
--
|
|
22
|
+
-- NO BACKFILL. Rows written before this migration keep a NULL kind, and the two reads
|
|
23
|
+
-- that filter by kind treat that gap differently, on purpose.
|
|
24
|
+
--
|
|
25
|
+
-- The runs LIST admits a NULL kind under a matching name, so nothing that works today
|
|
26
|
+
-- stops working. Under a name two entries share, such a row shows up on both: nothing
|
|
27
|
+
-- on it says which one it was, and a human reading the list can see that for himself.
|
|
28
|
+
--
|
|
29
|
+
-- The AGGREGATE read admits it only for a name that is unambiguous in the request.
|
|
30
|
+
-- Excluding legacy rows outright reported a workspace that had been running for a week
|
|
31
|
+
-- as idle right after every upgrade, while the list one click away showed them — two
|
|
32
|
+
-- surfaces of one feature contradicting each other. But admitting them under a shared
|
|
33
|
+
-- name would count one run twice and invent activity, so there they stay out of both.
|
|
34
|
+
|
|
35
|
+
ALTER TABLE stackbone_platform.runs
|
|
36
|
+
ADD COLUMN IF NOT EXISTS entity_kind text;
|
|
37
|
+
|
|
38
|
+
-- Backs the (name, kind) exact-match filter and the distinct (name, kind) list that
|
|
39
|
+
-- feeds the Runs screen's dropdown. A NEW index rather than a widened
|
|
40
|
+
-- `runs_entity_name_idx`: dropping and recreating 0020's index under its own name
|
|
41
|
+
-- would silently revoke whatever a later migration or an operator added to it, and
|
|
42
|
+
-- the name-only filter still uses it.
|
|
43
|
+
CREATE INDEX IF NOT EXISTS runs_entity_name_kind_idx
|
|
44
|
+
ON stackbone_platform.runs (entity_name, entity_kind)
|
|
45
|
+
WHERE entity_name IS NOT NULL;
|