@team-semicolon/semicolony-cli 4.18.106 → 4.18.107
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/dist/bundle.js +416 -416
- package/migrations/203_ops_meetings_board_grant.sql +45 -0
- package/migrations/206_action_scope_narrow_dashboard.sql +29 -0
- package/migrations/207_action_grants_fleet_board.sql +48 -0
- package/migrations/208_action_grants_fleet_board_guard_repair.sql +45 -0
- package/package.json +1 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
-- 203_ops_meetings_board_grant.sql
|
|
2
|
+
--
|
|
3
|
+
-- Let the SEMO Ops control room read the team meeting record.
|
|
4
|
+
--
|
|
5
|
+
-- `/ops` reads `semo_ops` through the least-privilege `sc_fleet_board` role on a
|
|
6
|
+
-- separate pool, and that role holds SELECT on fourteen tables — `meetings` not
|
|
7
|
+
-- among them. Without this grant the meetings view fails with
|
|
8
|
+
-- `permission denied for table meetings`, which is how the action-item and
|
|
9
|
+
-- ontology cards failed in 196: a release gap, not a code bug.
|
|
10
|
+
--
|
|
11
|
+
-- SELECT only. The control room observes; every write goes through
|
|
12
|
+
-- `record_meeting`, and the writer roles must not overlap with the board.
|
|
13
|
+
--
|
|
14
|
+
-- Unlike `knowledge_base`, `semo_ops.meetings` has RLS disabled, so there is no
|
|
15
|
+
-- policy to extend and no silent-zero-rows failure mode here. The grant is the
|
|
16
|
+
-- whole permission story, which also means **restriction is not enforced by the
|
|
17
|
+
-- database for this table**: a `restricted` meeting is visible to any granted
|
|
18
|
+
-- role, and the reading query must filter `visibility` itself. That is stated in
|
|
19
|
+
-- semo-ops `docs/dashboard/code-and-authority-inventory-2026-08-13.md` and
|
|
20
|
+
-- KB `semo-ops/process/meetings-visibility-enforcement`.
|
|
21
|
+
--
|
|
22
|
+
-- `action_items` is already granted (196), so meeting → action-item lookups need
|
|
23
|
+
-- no further permission.
|
|
24
|
+
--
|
|
25
|
+
-- Safety: additive, SELECT only. The runner owns the transaction boundary, so no
|
|
26
|
+
-- BEGIN/COMMIT here, and it retargets the `semicolony.` qualifier to the active
|
|
27
|
+
-- schema.
|
|
28
|
+
|
|
29
|
+
GRANT SELECT ON TABLE semicolony.meetings TO sc_fleet_board;
|
|
30
|
+
|
|
31
|
+
DO $$
|
|
32
|
+
DECLARE v_cols int;
|
|
33
|
+
BEGIN
|
|
34
|
+
-- A table-level grant must cover every column; a partial grant would drop
|
|
35
|
+
-- fields from the view with no error at grant time.
|
|
36
|
+
SELECT count(*) INTO v_cols
|
|
37
|
+
FROM information_schema.role_column_grants
|
|
38
|
+
WHERE grantee = 'sc_fleet_board'
|
|
39
|
+
AND table_schema = current_schema()
|
|
40
|
+
AND table_name = 'meetings'
|
|
41
|
+
AND privilege_type = 'SELECT';
|
|
42
|
+
IF v_cols = 0 THEN
|
|
43
|
+
RAISE EXCEPTION 'sc_fleet_board did not receive SELECT on meetings';
|
|
44
|
+
END IF;
|
|
45
|
+
END $$;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- The semo-ops-dashboard credential gained action:read and action:write from
|
|
2
|
+
-- 205's blanket scope amendment, but the dashboard never calls the Gateway's
|
|
3
|
+
-- action routes -- it reads and writes semo_ops.action_items through direct
|
|
4
|
+
-- queries in lib/core/action-items.ts. Measured 2026-08-14: zero occurrences
|
|
5
|
+
-- of /action/list|create|update|get anywhere in packages/semicolony-dashboard.
|
|
6
|
+
-- Take the unused scopes back.
|
|
7
|
+
UPDATE semicolony.gateway_credentials
|
|
8
|
+
SET scopes = array_remove(array_remove(scopes, 'action:read'), 'action:write')
|
|
9
|
+
WHERE status = 'active'
|
|
10
|
+
AND metadata->>'device_id' = 'semo-ops-dashboard';
|
|
11
|
+
|
|
12
|
+
DO $$
|
|
13
|
+
DECLARE
|
|
14
|
+
remaining text[];
|
|
15
|
+
BEGIN
|
|
16
|
+
SELECT scopes INTO remaining
|
|
17
|
+
FROM semicolony.gateway_credentials
|
|
18
|
+
WHERE status = 'active' AND metadata->>'device_id' = 'semo-ops-dashboard';
|
|
19
|
+
|
|
20
|
+
IF remaining IS NULL THEN
|
|
21
|
+
RAISE EXCEPTION 'semo-ops-dashboard credential not found or not active';
|
|
22
|
+
END IF;
|
|
23
|
+
IF 'workspace:assets-read' <> ALL(remaining) THEN
|
|
24
|
+
RAISE EXCEPTION 'workspace:assets-read was lost: %', remaining;
|
|
25
|
+
END IF;
|
|
26
|
+
IF 'action:read' = ANY(remaining) OR 'action:write' = ANY(remaining) THEN
|
|
27
|
+
RAISE EXCEPTION 'action scopes still present: %', remaining;
|
|
28
|
+
END IF;
|
|
29
|
+
END $$;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
-- The dashboard's action-item surface moved off appdb.semicolony onto
|
|
2
|
+
-- semo_ops (see KB semicolon decision
|
|
3
|
+
-- semo-ops-dashboard-control-room-data-routing-2026-08-06, and the 42P01
|
|
4
|
+
-- outage measured 2026-08-16). Its pool connects as sc_fleet_board, which
|
|
5
|
+
-- held SELECT only. Writes still reach action_items exclusively through
|
|
6
|
+
-- SECURITY DEFINER functions -- this grants EXECUTE on the three the
|
|
7
|
+
-- dashboard calls (lib/core/action-items.ts: createActionItem,
|
|
8
|
+
-- assignActionItem, operatorUpdateActionItem -- cancelActionItem delegates
|
|
9
|
+
-- to operatorUpdateActionItem rather than calling its own function) and
|
|
10
|
+
-- adds no direct DML. The other five surfaces that moved to the fleet pool
|
|
11
|
+
-- in the same cutover (the /api/action-items roster query, /api/goals,
|
|
12
|
+
-- /api/person/[domain], lib/plugins/service/kpi.ts, lib/tasks-db.ts) are
|
|
13
|
+
-- read-only SELECTs against action_items/team_members/bot_commitments and
|
|
14
|
+
-- call no function, so they need no grant here. No read path selects from
|
|
15
|
+
-- action_item_events, so this migration does not grant SELECT on it either.
|
|
16
|
+
GRANT EXECUTE ON FUNCTION semicolony.create_action_item(
|
|
17
|
+
p_description text, p_project_domain text, p_deadline date,
|
|
18
|
+
p_assignee_kind text, p_assignee_member text, p_assignee_agent text,
|
|
19
|
+
p_assignee_device text, p_priority text, p_source_kind text,
|
|
20
|
+
p_source_ref text, p_related_url text, p_external_contact text,
|
|
21
|
+
p_metadata jsonb, p_origin_surface text, p_origin_actor_kind text,
|
|
22
|
+
p_origin_member text, p_origin_agent text, p_origin_device text,
|
|
23
|
+
p_origin_harness text, p_origin_session_key text
|
|
24
|
+
) TO sc_fleet_board;
|
|
25
|
+
|
|
26
|
+
GRANT EXECUTE ON FUNCTION semicolony.assign_action_item(
|
|
27
|
+
p_action_item_id uuid, p_assignee_kind text, p_assignee_member text,
|
|
28
|
+
p_assignee_agent text, p_assignee_device text, p_actor_kind text,
|
|
29
|
+
p_actor_member text, p_actor_agent text
|
|
30
|
+
) TO sc_fleet_board;
|
|
31
|
+
|
|
32
|
+
GRANT EXECUTE ON FUNCTION semicolony.operator_update_action_item(
|
|
33
|
+
p_action_item_id uuid, p_status text, p_deadline date, p_note text,
|
|
34
|
+
p_metadata jsonb, p_actor_kind text, p_actor_member text,
|
|
35
|
+
p_actor_agent text
|
|
36
|
+
) TO sc_fleet_board;
|
|
37
|
+
|
|
38
|
+
DO $$
|
|
39
|
+
BEGIN
|
|
40
|
+
IF EXISTS (
|
|
41
|
+
SELECT 1 FROM information_schema.table_privileges
|
|
42
|
+
WHERE grantee = 'sc_fleet_board' AND table_schema = 'semicolony'
|
|
43
|
+
AND table_name = 'action_items'
|
|
44
|
+
AND privilege_type IN ('INSERT','UPDATE','DELETE')
|
|
45
|
+
) THEN
|
|
46
|
+
RAISE EXCEPTION 'sc_fleet_board must never hold direct DML on action_items';
|
|
47
|
+
END IF;
|
|
48
|
+
END $$;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
-- 207_action_grants_fleet_board.sql shipped a safety-net guard that can never
|
|
2
|
+
-- fire in the environment it targets:
|
|
3
|
+
--
|
|
4
|
+
-- WHERE grantee = 'sc_fleet_board' AND table_schema = 'semicolony' ...
|
|
5
|
+
--
|
|
6
|
+
-- `table_schema = 'semicolony'` is a string LITERAL compared against a column,
|
|
7
|
+
-- not a schema-qualified identifier. `retargetSchemaSql`
|
|
8
|
+
-- (packages/cli/src/commands/db.ts:35-44) rewrites `semicolony.` only when it
|
|
9
|
+
-- qualifies an identifier (`semicolony.<name>`, matched by
|
|
10
|
+
-- `(?<![\w./])semicolony\.`) -- a bare string literal with no trailing dot,
|
|
11
|
+
-- like `'semicolony'` in a WHERE clause, is left untouched by design (see
|
|
12
|
+
-- that function's own docstring, which names this exact pattern as excluded).
|
|
13
|
+
-- This migration always runs with SEMICOLONY_DB_SCHEMA=semo_ops against
|
|
14
|
+
-- team_knowledge_fabric, which has no `semicolony` schema at all -- so the
|
|
15
|
+
-- guard's WHERE clause matched zero rows regardless of grant state, and the
|
|
16
|
+
-- RAISE EXCEPTION was dead code. The invariant itself was never violated
|
|
17
|
+
-- (verified separately, see task-3-report.md); only the safety net was inert.
|
|
18
|
+
--
|
|
19
|
+
-- Fix: drop the schema filter rather than try to make it retarget-aware.
|
|
20
|
+
-- `current_schema()` was considered and rejected -- it happens to resolve to
|
|
21
|
+
-- `semo_ops` for the role this migration currently runs as (that role's
|
|
22
|
+
-- search_path defaults to `semo_ops, public, pg_catalog` in this database),
|
|
23
|
+
-- but that is a property of the connecting role's search_path, not of
|
|
24
|
+
-- SEMICOLONY_DB_SCHEMA, and would silently go back to matching nothing under
|
|
25
|
+
-- a role without that search_path -- the same failure mode this migration
|
|
26
|
+
-- exists to close. `action_items` is schema-unique in this database (checked
|
|
27
|
+
-- against information_schema.tables across all schemas: exactly one row,
|
|
28
|
+
-- semo_ops.action_items) so filtering on table_name alone is unambiguous
|
|
29
|
+
-- today, and if a second action_items ever appears anywhere and picks up
|
|
30
|
+
-- direct DML for sc_fleet_board, this guard should fire for that too -- a
|
|
31
|
+
-- schema-agnostic check is the more conservative invariant, not a weaker one.
|
|
32
|
+
--
|
|
33
|
+
-- Record the trap for future authors: a bare string literal inside a WHERE
|
|
34
|
+
-- clause is not an identifier and `retargetSchemaSql` will never rewrite it,
|
|
35
|
+
-- by design, no matter how much it looks like a schema reference.
|
|
36
|
+
DO $$
|
|
37
|
+
BEGIN
|
|
38
|
+
IF EXISTS (
|
|
39
|
+
SELECT 1 FROM information_schema.table_privileges
|
|
40
|
+
WHERE grantee = 'sc_fleet_board' AND table_name = 'action_items'
|
|
41
|
+
AND privilege_type IN ('INSERT','UPDATE','DELETE')
|
|
42
|
+
) THEN
|
|
43
|
+
RAISE EXCEPTION 'sc_fleet_board must never hold direct DML on action_items';
|
|
44
|
+
END IF;
|
|
45
|
+
END $$;
|