@stackbone/cli 0.1.0-alpha.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 +14 -0
- package/README.md +50 -0
- package/main.js +8660 -0
- package/migrations/0001_runs.sql +150 -0
- package/migrations/0002_approvals.sql +110 -0
- package/migrations/0003_runs_is_playground_index.sql +15 -0
- package/migrations/0004_playground_fixtures.sql +40 -0
- package/migrations/0005_stackbone_viewer.sql +47 -0
- package/migrations/0006_secrets.sql +37 -0
- package/migrations/0007_agent_config_versions.sql +57 -0
- package/migrations/0008_approvals_idempotency_nulls_not_distinct.sql +19 -0
- package/package.json +34 -0
- package/stackbone-cli-0.1.0-alpha.0.tgz +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
-- 0001_runs.sql — schema and tables for the run-level surface of Studio.
|
|
2
|
+
-- Spec: docs/arquitectura/specs/stackbone-agent-protocol-v1.md §6 (runs, run_steps).
|
|
3
|
+
-- ADR: docs/arquitectura/decisiones/2026-05-03-stackbone-studio-datapath-y-scope-mvp.md §D2/D7/D9.
|
|
4
|
+
|
|
5
|
+
CREATE SCHEMA IF NOT EXISTS stackbone_platform;
|
|
6
|
+
|
|
7
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.runs (
|
|
8
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
9
|
+
-- Nullable in local (single-agent emulator), required in cloud.
|
|
10
|
+
agent_id text,
|
|
11
|
+
workspace_id text,
|
|
12
|
+
trigger text NOT NULL DEFAULT 'manual'
|
|
13
|
+
CHECK (trigger IN ('manual', 'webhook', 'cron', 'event')),
|
|
14
|
+
status text NOT NULL DEFAULT 'running'
|
|
15
|
+
CHECK (status IN ('running', 'done', 'failed', 'interrupted')),
|
|
16
|
+
trace_id text,
|
|
17
|
+
is_playground boolean NOT NULL DEFAULT false,
|
|
18
|
+
input jsonb,
|
|
19
|
+
output jsonb,
|
|
20
|
+
error jsonb,
|
|
21
|
+
cost_estimated_usd numeric,
|
|
22
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
23
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
24
|
+
started_at timestamptz,
|
|
25
|
+
finished_at timestamptz,
|
|
26
|
+
duration_ms integer
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE INDEX IF NOT EXISTS runs_workspace_started_idx
|
|
30
|
+
ON stackbone_platform.runs (workspace_id, started_at DESC NULLS LAST);
|
|
31
|
+
|
|
32
|
+
-- Drives the cursor pagination from §5 of the protocol spec: order by
|
|
33
|
+
-- created_at desc with id as the tie-break for runs created within the same
|
|
34
|
+
-- millisecond.
|
|
35
|
+
CREATE INDEX IF NOT EXISTS runs_created_at_id_idx
|
|
36
|
+
ON stackbone_platform.runs (created_at DESC, id DESC);
|
|
37
|
+
|
|
38
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.run_steps (
|
|
39
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
40
|
+
run_id uuid NOT NULL
|
|
41
|
+
REFERENCES stackbone_platform.runs(id) ON DELETE CASCADE,
|
|
42
|
+
parent_step_id uuid REFERENCES stackbone_platform.run_steps(id) ON DELETE CASCADE,
|
|
43
|
+
type text NOT NULL
|
|
44
|
+
CHECK (type IN (
|
|
45
|
+
'agent', 'llm_call', 'db_query', 'http_fetch',
|
|
46
|
+
'queue_publish', 'hitl_pause', 'tool_call',
|
|
47
|
+
'rag_query', 'storage_op'
|
|
48
|
+
)),
|
|
49
|
+
name text,
|
|
50
|
+
status text NOT NULL DEFAULT 'running'
|
|
51
|
+
CHECK (status IN ('ok', 'error', 'running')),
|
|
52
|
+
payload jsonb,
|
|
53
|
+
error jsonb,
|
|
54
|
+
started_at timestamptz NOT NULL DEFAULT now(),
|
|
55
|
+
finished_at timestamptz,
|
|
56
|
+
duration_ms integer
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
CREATE INDEX IF NOT EXISTS run_steps_run_started_idx
|
|
60
|
+
ON stackbone_platform.run_steps (run_id, started_at);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS run_steps_parent_idx
|
|
62
|
+
ON stackbone_platform.run_steps (parent_step_id);
|
|
63
|
+
|
|
64
|
+
-- AFTER INSERT/UPDATE triggers fan-out into pg_notify('stackbone_platform_events',
|
|
65
|
+
-- ...). The `apps/api` SSE handler (D4) and the local emulator's bus (V2/V3)
|
|
66
|
+
-- LISTEN on this channel and forward each frame as the SSE payload defined in
|
|
67
|
+
-- the protocol spec §10.2.
|
|
68
|
+
CREATE OR REPLACE FUNCTION stackbone_platform.notify_run_event() RETURNS trigger
|
|
69
|
+
LANGUAGE plpgsql AS $fn$
|
|
70
|
+
DECLARE
|
|
71
|
+
payload jsonb;
|
|
72
|
+
BEGIN
|
|
73
|
+
IF TG_OP = 'INSERT' THEN
|
|
74
|
+
payload := jsonb_build_object(
|
|
75
|
+
'type', 'run.created',
|
|
76
|
+
'run_id', NEW.id,
|
|
77
|
+
'trigger', NEW.trigger,
|
|
78
|
+
'created_at', NEW.created_at
|
|
79
|
+
);
|
|
80
|
+
ELSIF TG_OP = 'UPDATE' THEN
|
|
81
|
+
IF (OLD.started_at IS NULL) AND (NEW.started_at IS NOT NULL) THEN
|
|
82
|
+
payload := jsonb_build_object(
|
|
83
|
+
'type', 'run.started',
|
|
84
|
+
'run_id', NEW.id,
|
|
85
|
+
'started_at', NEW.started_at
|
|
86
|
+
);
|
|
87
|
+
ELSIF (OLD.finished_at IS NULL) AND (NEW.finished_at IS NOT NULL) THEN
|
|
88
|
+
payload := jsonb_build_object(
|
|
89
|
+
'type', 'run.finished',
|
|
90
|
+
'run_id', NEW.id,
|
|
91
|
+
'status', NEW.status,
|
|
92
|
+
'finished_at', NEW.finished_at,
|
|
93
|
+
'duration_ms', NEW.duration_ms
|
|
94
|
+
);
|
|
95
|
+
ELSE
|
|
96
|
+
RETURN NULL;
|
|
97
|
+
END IF;
|
|
98
|
+
ELSE
|
|
99
|
+
RETURN NULL;
|
|
100
|
+
END IF;
|
|
101
|
+
|
|
102
|
+
PERFORM pg_notify('stackbone_platform_events', payload::text);
|
|
103
|
+
RETURN NULL;
|
|
104
|
+
END;
|
|
105
|
+
$fn$;
|
|
106
|
+
|
|
107
|
+
DROP TRIGGER IF EXISTS runs_notify_aiu ON stackbone_platform.runs;
|
|
108
|
+
CREATE TRIGGER runs_notify_aiu
|
|
109
|
+
AFTER INSERT OR UPDATE ON stackbone_platform.runs
|
|
110
|
+
FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_run_event();
|
|
111
|
+
|
|
112
|
+
CREATE OR REPLACE FUNCTION stackbone_platform.notify_run_step_event() RETURNS trigger
|
|
113
|
+
LANGUAGE plpgsql AS $fn$
|
|
114
|
+
DECLARE
|
|
115
|
+
payload jsonb;
|
|
116
|
+
BEGIN
|
|
117
|
+
IF TG_OP = 'INSERT' THEN
|
|
118
|
+
payload := jsonb_build_object(
|
|
119
|
+
'type', 'run.step.created',
|
|
120
|
+
'run_id', NEW.run_id,
|
|
121
|
+
'step_id', NEW.id,
|
|
122
|
+
'parent_step_id', NEW.parent_step_id,
|
|
123
|
+
'step_type', NEW.type,
|
|
124
|
+
'started_at', NEW.started_at
|
|
125
|
+
);
|
|
126
|
+
ELSIF TG_OP = 'UPDATE' THEN
|
|
127
|
+
IF (OLD.finished_at IS NULL) AND (NEW.finished_at IS NOT NULL) THEN
|
|
128
|
+
payload := jsonb_build_object(
|
|
129
|
+
'type', 'run.step.finished',
|
|
130
|
+
'run_id', NEW.run_id,
|
|
131
|
+
'step_id', NEW.id,
|
|
132
|
+
'status', NEW.status,
|
|
133
|
+
'duration_ms', NEW.duration_ms
|
|
134
|
+
);
|
|
135
|
+
ELSE
|
|
136
|
+
RETURN NULL;
|
|
137
|
+
END IF;
|
|
138
|
+
ELSE
|
|
139
|
+
RETURN NULL;
|
|
140
|
+
END IF;
|
|
141
|
+
|
|
142
|
+
PERFORM pg_notify('stackbone_platform_events', payload::text);
|
|
143
|
+
RETURN NULL;
|
|
144
|
+
END;
|
|
145
|
+
$fn$;
|
|
146
|
+
|
|
147
|
+
DROP TRIGGER IF EXISTS run_steps_notify_aiu ON stackbone_platform.run_steps;
|
|
148
|
+
CREATE TRIGGER run_steps_notify_aiu
|
|
149
|
+
AFTER INSERT OR UPDATE ON stackbone_platform.run_steps
|
|
150
|
+
FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_run_step_event();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
-- 0002_approvals.sql — HITL approvals queue and audit trail.
|
|
2
|
+
-- Spec: docs/arquitectura/specs/stackbone-agent-protocol-v1.md §6.6/§6.7.
|
|
3
|
+
-- Component: docs/arquitectura/componentes/08-hitl-inbox.md.
|
|
4
|
+
-- ADR: docs/arquitectura/decisiones/2026-05-03-stackbone-studio-datapath-y-scope-mvp.md §D2.
|
|
5
|
+
|
|
6
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.approvals (
|
|
7
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
8
|
+
-- Nullable in local (single-agent emulator), required in cloud.
|
|
9
|
+
agent_id text,
|
|
10
|
+
workspace_id text,
|
|
11
|
+
topic text NOT NULL,
|
|
12
|
+
status text NOT NULL DEFAULT 'pending'
|
|
13
|
+
CHECK (status IN (
|
|
14
|
+
'pending', 'approved', 'rejected',
|
|
15
|
+
'timed_out', 'cancelled'
|
|
16
|
+
)),
|
|
17
|
+
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
18
|
+
schema jsonb,
|
|
19
|
+
run_id uuid REFERENCES stackbone_platform.runs(id) ON DELETE SET NULL,
|
|
20
|
+
requested_by_step_id uuid REFERENCES stackbone_platform.run_steps(id) ON DELETE SET NULL,
|
|
21
|
+
callback_url text,
|
|
22
|
+
idempotency_key text,
|
|
23
|
+
fallback text CHECK (fallback IS NULL OR fallback IN (
|
|
24
|
+
'approve', 'reject', 'timeout'
|
|
25
|
+
)),
|
|
26
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
27
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
28
|
+
timeout_at timestamptz,
|
|
29
|
+
decided_at timestamptz
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
-- Idempotency: same (workspace, topic, idempotency_key) tuple cannot create
|
|
33
|
+
-- two approvals. Partial index keeps a NULL idempotency_key from blocking
|
|
34
|
+
-- everything; the 99% of HITL pauses that don't carry a key still work.
|
|
35
|
+
CREATE UNIQUE INDEX IF NOT EXISTS approvals_idempotency_uq
|
|
36
|
+
ON stackbone_platform.approvals (workspace_id, topic, idempotency_key)
|
|
37
|
+
WHERE idempotency_key IS NOT NULL;
|
|
38
|
+
|
|
39
|
+
CREATE INDEX IF NOT EXISTS approvals_workspace_status_idx
|
|
40
|
+
ON stackbone_platform.approvals (workspace_id, status, created_at DESC);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS approvals_run_idx
|
|
43
|
+
ON stackbone_platform.approvals (run_id);
|
|
44
|
+
|
|
45
|
+
CREATE INDEX IF NOT EXISTS approvals_pending_timeout_idx
|
|
46
|
+
ON stackbone_platform.approvals (timeout_at)
|
|
47
|
+
WHERE status = 'pending' AND timeout_at IS NOT NULL;
|
|
48
|
+
|
|
49
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.approval_decisions (
|
|
50
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
51
|
+
approval_id uuid NOT NULL
|
|
52
|
+
REFERENCES stackbone_platform.approvals(id) ON DELETE CASCADE,
|
|
53
|
+
actor_user_id text,
|
|
54
|
+
actor_email text,
|
|
55
|
+
decision text NOT NULL CHECK (decision IN ('approve', 'reject')),
|
|
56
|
+
edited_payload jsonb,
|
|
57
|
+
comment text,
|
|
58
|
+
decided_at timestamptz NOT NULL DEFAULT now()
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
CREATE INDEX IF NOT EXISTS approval_decisions_approval_idx
|
|
62
|
+
ON stackbone_platform.approval_decisions (approval_id, decided_at);
|
|
63
|
+
|
|
64
|
+
-- Triggers fire on the same `stackbone_platform_events` channel as the runs
|
|
65
|
+
-- triggers, so the SSE handler in `apps/api` only LISTENs once.
|
|
66
|
+
CREATE OR REPLACE FUNCTION stackbone_platform.notify_approval_event() RETURNS trigger
|
|
67
|
+
LANGUAGE plpgsql AS $fn$
|
|
68
|
+
DECLARE
|
|
69
|
+
payload jsonb;
|
|
70
|
+
BEGIN
|
|
71
|
+
IF TG_OP = 'INSERT' THEN
|
|
72
|
+
payload := jsonb_build_object(
|
|
73
|
+
'type', 'approval.created',
|
|
74
|
+
'approval_id', NEW.id,
|
|
75
|
+
'run_id', NEW.run_id,
|
|
76
|
+
'topic', NEW.topic,
|
|
77
|
+
'created_at', NEW.created_at,
|
|
78
|
+
'timeout_at', NEW.timeout_at
|
|
79
|
+
);
|
|
80
|
+
ELSIF TG_OP = 'UPDATE' THEN
|
|
81
|
+
IF (OLD.decided_at IS NULL) AND (NEW.decided_at IS NOT NULL)
|
|
82
|
+
AND NEW.status IN ('approved', 'rejected') THEN
|
|
83
|
+
payload := jsonb_build_object(
|
|
84
|
+
'type', 'approval.decided',
|
|
85
|
+
'approval_id', NEW.id,
|
|
86
|
+
'status', NEW.status,
|
|
87
|
+
'decided_at', NEW.decided_at
|
|
88
|
+
);
|
|
89
|
+
ELSIF (OLD.status = 'pending') AND (NEW.status = 'timed_out') THEN
|
|
90
|
+
payload := jsonb_build_object(
|
|
91
|
+
'type', 'approval.timed_out',
|
|
92
|
+
'approval_id', NEW.id,
|
|
93
|
+
'fallback', NEW.fallback
|
|
94
|
+
);
|
|
95
|
+
ELSE
|
|
96
|
+
RETURN NULL;
|
|
97
|
+
END IF;
|
|
98
|
+
ELSE
|
|
99
|
+
RETURN NULL;
|
|
100
|
+
END IF;
|
|
101
|
+
|
|
102
|
+
PERFORM pg_notify('stackbone_platform_events', payload::text);
|
|
103
|
+
RETURN NULL;
|
|
104
|
+
END;
|
|
105
|
+
$fn$;
|
|
106
|
+
|
|
107
|
+
DROP TRIGGER IF EXISTS approvals_notify_aiu ON stackbone_platform.approvals;
|
|
108
|
+
CREATE TRIGGER approvals_notify_aiu
|
|
109
|
+
AFTER INSERT OR UPDATE ON stackbone_platform.approvals
|
|
110
|
+
FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_approval_event();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- 0003_runs_is_playground_index.sql — billing-friendly partial index over runs.
|
|
2
|
+
-- Spec: docs/tasks/Studio V5 - Playground e Invoke.md (V5#01).
|
|
3
|
+
-- ADR: docs/arquitectura/decisiones/2026-05-03-stackbone-studio-datapath-y-scope-mvp.md §D5/D9.
|
|
4
|
+
--
|
|
5
|
+
-- The `is_playground` column itself already lives in 0001_runs.sql (F2 baked it
|
|
6
|
+
-- in defensively so playground writes had a place to land before V5 shipped).
|
|
7
|
+
-- This migration adds the partial index that the billing pipeline and the
|
|
8
|
+
-- workspace-level aggregations (V1+ Workspace Health, LLM Usage) need to scan
|
|
9
|
+
-- only the runs that count toward billing — i.e. `is_playground = false`.
|
|
10
|
+
-- Keeping it partial keeps the index small even when a workspace stress-tests
|
|
11
|
+
-- the agent from the Playground.
|
|
12
|
+
|
|
13
|
+
CREATE INDEX IF NOT EXISTS runs_billable_workspace_started_idx
|
|
14
|
+
ON stackbone_platform.runs (workspace_id, started_at DESC NULLS LAST)
|
|
15
|
+
WHERE is_playground = false;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- 0004_playground_fixtures.sql — saved Playground inputs for V5 (Studio).
|
|
2
|
+
-- Spec: docs/arquitectura/specs/stackbone-agent-protocol-v1.md §6.10.
|
|
3
|
+
-- Task: docs/tasks/Studio V5 - Playground e Invoke.md (V5#03).
|
|
4
|
+
--
|
|
5
|
+
-- The table holds JSON inputs the creator pinned for repeated invocations
|
|
6
|
+
-- from the Studio Playground. Cloud-only — local fixtures live in
|
|
7
|
+
-- `~/.stackbone/dev/fixtures/<agentId>.jsonl` for filesystem parity with the
|
|
8
|
+
-- rest of the local datapath. Workspace-shared fixtures and version history
|
|
9
|
+
-- land in V1+ (today MVP only stores the owner's saved inputs).
|
|
10
|
+
--
|
|
11
|
+
-- The `(agent_id, name)` unique constraint backs the `409 fixture_name_conflict`
|
|
12
|
+
-- response defined in the protocol spec — the partial index keeps the
|
|
13
|
+
-- agentless rows (single-agent local emulator parity) from blocking each
|
|
14
|
+
-- other on `name`.
|
|
15
|
+
|
|
16
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.playground_fixtures (
|
|
17
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
18
|
+
-- Nullable in local (single-agent emulator). In cloud the saga sets it
|
|
19
|
+
-- to the install's agent_id so different agents in the same workspace
|
|
20
|
+
-- can reuse fixture names.
|
|
21
|
+
agent_id text,
|
|
22
|
+
name text NOT NULL,
|
|
23
|
+
input_json jsonb NOT NULL,
|
|
24
|
+
description text,
|
|
25
|
+
-- The owner's better-auth user id at save time. Kept text to match the
|
|
26
|
+
-- existing `workspaces.owner_user_id` column type.
|
|
27
|
+
created_by text,
|
|
28
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE UNIQUE INDEX IF NOT EXISTS playground_fixtures_agent_name_uq
|
|
32
|
+
ON stackbone_platform.playground_fixtures (agent_id, name)
|
|
33
|
+
WHERE agent_id IS NOT NULL;
|
|
34
|
+
|
|
35
|
+
CREATE UNIQUE INDEX IF NOT EXISTS playground_fixtures_name_uq
|
|
36
|
+
ON stackbone_platform.playground_fixtures (name)
|
|
37
|
+
WHERE agent_id IS NULL;
|
|
38
|
+
|
|
39
|
+
CREATE INDEX IF NOT EXISTS playground_fixtures_created_at_idx
|
|
40
|
+
ON stackbone_platform.playground_fixtures (created_at DESC, id DESC);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
-- 0005_stackbone_viewer.sql — read-only role for the Studio DB Explorer.
|
|
2
|
+
-- Spec: docs/arquitectura/componentes/11-stackbone-studio.md §"DB Explorer".
|
|
3
|
+
-- Task: docs/tasks/Studio V6 - DB Explorer read-only.md (V6#01).
|
|
4
|
+
-- ADR: docs/arquitectura/decisiones/2026-05-03-stackbone-studio-datapath-y-scope-mvp.md §D6.
|
|
5
|
+
--
|
|
6
|
+
-- The role is `NOLOGIN` because callers (apps/api, apps/cli emulator) reach it
|
|
7
|
+
-- via `SET ROLE stackbone_viewer` on a session opened with the regular owner
|
|
8
|
+
-- credentials, not by logging in as `stackbone_viewer` directly. This keeps the
|
|
9
|
+
-- Neon HTTP driver one-shot model (D3) untouched: the connection string still
|
|
10
|
+
-- belongs to the owner, the privilege drop happens per-statement.
|
|
11
|
+
--
|
|
12
|
+
-- DEFAULT PRIVILEGES are scoped to the current migration runner (`current_user`)
|
|
13
|
+
-- so any table the runner creates afterwards in `public`, `stackbone_platform`,
|
|
14
|
+
-- and the future `pgmq` / `rag_*` schemas auto-grants SELECT to the viewer
|
|
15
|
+
-- without re-issuing GRANTs from a follow-up migration.
|
|
16
|
+
|
|
17
|
+
DO $$
|
|
18
|
+
BEGIN
|
|
19
|
+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'stackbone_viewer') THEN
|
|
20
|
+
CREATE ROLE stackbone_viewer NOLOGIN;
|
|
21
|
+
END IF;
|
|
22
|
+
END
|
|
23
|
+
$$;
|
|
24
|
+
|
|
25
|
+
-- Existing schemas at the time this migration runs.
|
|
26
|
+
GRANT USAGE ON SCHEMA public TO stackbone_viewer;
|
|
27
|
+
GRANT USAGE ON SCHEMA stackbone_platform TO stackbone_viewer;
|
|
28
|
+
|
|
29
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA public TO stackbone_viewer;
|
|
30
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA stackbone_platform TO stackbone_viewer;
|
|
31
|
+
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO stackbone_viewer;
|
|
32
|
+
GRANT SELECT ON ALL SEQUENCES IN SCHEMA stackbone_platform TO stackbone_viewer;
|
|
33
|
+
|
|
34
|
+
-- Default privileges so any *future* table created by the migration runner in
|
|
35
|
+
-- these schemas auto-grants SELECT to `stackbone_viewer`. The Studio sidebar
|
|
36
|
+
-- promises visibility into `pgmq.*`, `_storage_objects`, `rag_*` and the
|
|
37
|
+
-- creator's own schemas — without these defaults, every new table created by
|
|
38
|
+
-- a downstream migration (or by the agent itself via Drizzle) would silently
|
|
39
|
+
-- be invisible to the DB Explorer.
|
|
40
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
41
|
+
GRANT SELECT ON TABLES TO stackbone_viewer;
|
|
42
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
43
|
+
GRANT SELECT ON SEQUENCES TO stackbone_viewer;
|
|
44
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA stackbone_platform
|
|
45
|
+
GRANT SELECT ON TABLES TO stackbone_viewer;
|
|
46
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA stackbone_platform
|
|
47
|
+
GRANT SELECT ON SEQUENCES TO stackbone_viewer;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
-- 0006_secrets.sql — local mirror of `installation_secret` for the
|
|
2
|
+
-- `stackbone dev` emulator (Studio V10).
|
|
3
|
+
-- Spec: docs/arquitectura/componentes/11-stackbone-studio.md §"Secrets".
|
|
4
|
+
-- Task: docs/tasks/Studio V10 - Secrets.md (V10#02).
|
|
5
|
+
-- ADR: docs/arquitectura/decisiones/2026-05-03-stackbone-studio-datapath-y-scope-mvp.md
|
|
6
|
+
-- (decision V10#01 in the task itself: cloud secrets stay in
|
|
7
|
+
-- `installation_secret` of the control plane; local emulator stores
|
|
8
|
+
-- the same wire shape here so the Studio UI sees identical data.)
|
|
9
|
+
--
|
|
10
|
+
-- The emulator runs without a control plane. To keep the Studio Secrets
|
|
11
|
+
-- screen wire-compatible (one shape from `@stackbone/validators`) we mirror
|
|
12
|
+
-- the cloud table inside `stackbone_platform.secrets`. Encryption uses the
|
|
13
|
+
-- documented `dev-only-encryption-key` (see STUDIO_LOCAL_DEV_ENCRYPTION_KEY
|
|
14
|
+
-- in libs/validators) and never leaves the developer machine.
|
|
15
|
+
--
|
|
16
|
+
-- `is_system` carries the same semantics as in cloud — the saga injects
|
|
17
|
+
-- nothing here so the column stays at its default `false`, but we keep it
|
|
18
|
+
-- for parity in case future system-level injectors land in the emulator.
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.secrets (
|
|
21
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
22
|
+
name text NOT NULL,
|
|
23
|
+
version text NOT NULL DEFAULT 'v1' CHECK (version IN ('v1')),
|
|
24
|
+
nonce bytea NOT NULL,
|
|
25
|
+
ciphertext bytea NOT NULL,
|
|
26
|
+
is_system boolean NOT NULL DEFAULT false,
|
|
27
|
+
description text,
|
|
28
|
+
-- created_by_email is a free-text field in local: there is no `user`
|
|
29
|
+
-- table in the emulator's Postgres. The cloud surface backfills the
|
|
30
|
+
-- equivalent via better-auth user lookup.
|
|
31
|
+
created_by_email text,
|
|
32
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
33
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE UNIQUE INDEX IF NOT EXISTS secrets_name_idx
|
|
37
|
+
ON stackbone_platform.secrets (name);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
-- 0007_agent_config_versions.sql — versioned history of `AGENT_CONFIG`
|
|
2
|
+
-- payloads for Studio's Config dinámica screen (V11).
|
|
3
|
+
-- Spec: docs/arquitectura/componentes/11-stackbone-studio.md §"Mapa de pantallas / C.Config dinámica".
|
|
4
|
+
-- Task: docs/tasks/Studio V11 - Config dinámica.md (V11#01).
|
|
5
|
+
-- ADR: docs/arquitectura/decisiones/2026-05-03-stackbone-studio-datapath-y-scope-mvp.md §D2/D7.
|
|
6
|
+
--
|
|
7
|
+
-- The current config is the row with the highest `version`. Older rows are
|
|
8
|
+
-- kept for the rollback action and the lateral history list. Hot-reload is
|
|
9
|
+
-- driven off `pg_notify('stackbone_platform_events', …)` from an INSERT trigger
|
|
10
|
+
-- — the local emulator orchestrator (V11#04) and `apps/api` SSE handlers
|
|
11
|
+
-- LISTEN on the same channel as the rest of the platform events.
|
|
12
|
+
--
|
|
13
|
+
-- `created_by_email` carries free text on the local emulator (no `user`
|
|
14
|
+
-- table inside the agent's Postgres) and the cloud control plane backfills
|
|
15
|
+
-- it via better-auth lookup. This mirrors `stackbone_platform.secrets` so the
|
|
16
|
+
-- audit shape stays uniform across vault screens.
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.agent_config_versions (
|
|
19
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
20
|
+
-- Monotonically increasing per install. The application layer assigns
|
|
21
|
+
-- `MAX(version) + 1` inside the same transaction as the INSERT so the
|
|
22
|
+
-- column doubles as a stable cursor for the lateral history list.
|
|
23
|
+
version integer NOT NULL CHECK (version >= 1),
|
|
24
|
+
value jsonb NOT NULL,
|
|
25
|
+
-- When a row originates from a rollback we record the source version
|
|
26
|
+
-- so the UI can render "rolled back from v7" without joining twice.
|
|
27
|
+
rolled_back_from integer,
|
|
28
|
+
created_by_email text,
|
|
29
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE UNIQUE INDEX IF NOT EXISTS agent_config_versions_version_idx
|
|
33
|
+
ON stackbone_platform.agent_config_versions (version);
|
|
34
|
+
|
|
35
|
+
CREATE INDEX IF NOT EXISTS agent_config_versions_created_at_idx
|
|
36
|
+
ON stackbone_platform.agent_config_versions (created_at DESC, version DESC);
|
|
37
|
+
|
|
38
|
+
CREATE OR REPLACE FUNCTION stackbone_platform.notify_agent_config_event() RETURNS trigger
|
|
39
|
+
LANGUAGE plpgsql AS $fn$
|
|
40
|
+
DECLARE
|
|
41
|
+
payload jsonb;
|
|
42
|
+
BEGIN
|
|
43
|
+
payload := jsonb_build_object(
|
|
44
|
+
'type', 'agent_config.changed',
|
|
45
|
+
'version', NEW.version,
|
|
46
|
+
'rolled_back_from', NEW.rolled_back_from,
|
|
47
|
+
'created_at', NEW.created_at
|
|
48
|
+
);
|
|
49
|
+
PERFORM pg_notify('stackbone_platform_events', payload::text);
|
|
50
|
+
RETURN NULL;
|
|
51
|
+
END;
|
|
52
|
+
$fn$;
|
|
53
|
+
|
|
54
|
+
DROP TRIGGER IF EXISTS agent_config_versions_notify_aiu ON stackbone_platform.agent_config_versions;
|
|
55
|
+
CREATE TRIGGER agent_config_versions_notify_aiu
|
|
56
|
+
AFTER INSERT ON stackbone_platform.agent_config_versions
|
|
57
|
+
FOR EACH ROW EXECUTE FUNCTION stackbone_platform.notify_agent_config_event();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- 0008_approvals_idempotency_nulls_not_distinct.sql — fix the partial unique
|
|
2
|
+
-- index on `approvals` so the `ON CONFLICT (workspace_id, topic,
|
|
3
|
+
-- idempotency_key)` upsert in `apps/cli` reuses an existing row when
|
|
4
|
+
-- `workspace_id IS NULL` (the local emulator never populates it).
|
|
5
|
+
--
|
|
6
|
+
-- Postgres treats NULL as distinct in unique indexes by default, so two
|
|
7
|
+
-- inserts with the same (NULL, topic, idempotencyKey) tuple were both
|
|
8
|
+
-- accepted instead of round-tripping the original approvalId. `NULLS NOT
|
|
9
|
+
-- DISTINCT` (Postgres 15+) collapses NULLs into a single comparable value,
|
|
10
|
+
-- which preserves the cloud semantics (workspace_id NOT NULL) and unblocks
|
|
11
|
+
-- the local emulator path.
|
|
12
|
+
--
|
|
13
|
+
-- Spec: docs/arquitectura/specs/stackbone-agent-protocol-v1.md §6.6.
|
|
14
|
+
|
|
15
|
+
DROP INDEX IF EXISTS stackbone_platform.approvals_idempotency_uq;
|
|
16
|
+
|
|
17
|
+
CREATE UNIQUE INDEX IF NOT EXISTS approvals_idempotency_uq
|
|
18
|
+
ON stackbone_platform.approvals (workspace_id, topic, idempotency_key) NULLS NOT DISTINCT
|
|
19
|
+
WHERE idempotency_key IS NOT NULL;
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stackbone/cli",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"license": "UNLICENSED",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"stackbone": "./main.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20.18.0"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@aws-sdk/client-s3": "^3.1038.0",
|
|
17
|
+
"@aws-sdk/s3-request-presigner": "^3.1038.0",
|
|
18
|
+
"@clack/prompts": "^1.2.0",
|
|
19
|
+
"@hono/node-server": "^2.0.0",
|
|
20
|
+
"ajv": "^8.20.0",
|
|
21
|
+
"citty": "^0.2.2",
|
|
22
|
+
"drizzle-kit": "^0.31.10",
|
|
23
|
+
"drizzle-orm": "^0.45.2",
|
|
24
|
+
"hono": "^4.12.15",
|
|
25
|
+
"open": "^11.0.0",
|
|
26
|
+
"pg": "^8.20.0",
|
|
27
|
+
"pg-query-emscripten": "^5.1.0",
|
|
28
|
+
"pino": "^10.3.1",
|
|
29
|
+
"pino-pretty": "^13.1.3",
|
|
30
|
+
"postgres": "^3.4.9",
|
|
31
|
+
"yaml": "^2.8.3",
|
|
32
|
+
"zod": "^4.3.6"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
Binary file
|