@team-semicolon/semicolony-cli 4.18.107 → 4.18.108

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.
@@ -0,0 +1,114 @@
1
+ -- 208_meeting_projection_classification_repair.sql
2
+ --
3
+ -- Close a live confidentiality exposure, then make it unrepeatable.
4
+ --
5
+ -- `knowledge_base_hide_restricted` hides a row from sc_app, sc_fleet_board,
6
+ -- semo_ops_bot and semo_ops_hive_cron when
7
+ -- `metadata -> 'classification' ->> 'sensitivity' = 'restricted'`. Measured
8
+ -- 2026-08-18: of 204 `key='meeting'` projections, **74 carried no
9
+ -- `classification` key at all**, and every one of those belongs to a meeting
10
+ -- whose `meetings.visibility` is `restricted`.
11
+ --
12
+ -- The policy therefore did not hide them. `sc_app` could read all 204 — client
13
+ -- pricing, revenue-share terms, contract start dates, design reviews — and every
14
+ -- bot that reads the Knowledge Fabric runs at or below that level. The Slack
15
+ -- actors answer from `kb_search`, so this was reachable in ordinary use.
16
+ --
17
+ -- `record_meeting` is not the cause: called today it still emits
18
+ -- `classification.sensitivity`, verified in a rolled-back transaction. The 74
19
+ -- rows were written or later rewritten by a path that rebuilt `metadata` without
20
+ -- that key. Rather than find and fix every such writer, this migration derives
21
+ -- the value from the table that owns it and then stops the projection from ever
22
+ -- being written without it again.
23
+ --
24
+ -- Part A repairs the existing rows. Part B installs the guard.
25
+ --
26
+ -- The runner owns the transaction boundary, so no BEGIN/COMMIT here, and it
27
+ -- retargets the `semicolony.` qualifier to the active schema.
28
+
29
+ -- ---------------------------------------------------------------- Part A
30
+ -- Derive the missing classification from the meeting the projection names.
31
+ -- `meetings.visibility` is the record of authority; the projection only mirrors
32
+ -- it. All 74 carry a resolvable `meeting_id` (measured: 0 unresolvable).
33
+
34
+ UPDATE semicolony.knowledge_base kb
35
+ SET metadata = kb.metadata || jsonb_build_object(
36
+ 'classification',
37
+ jsonb_build_object(
38
+ 'sensitivity',
39
+ CASE WHEN m.visibility = 'restricted' THEN 'restricted' ELSE 'internal' END
40
+ )
41
+ ),
42
+ updated_at = now()
43
+ FROM semicolony.meetings m
44
+ WHERE kb.key = 'meeting'
45
+ AND m.meeting_id::text = kb.metadata ->> 'meeting_id'
46
+ AND kb.metadata -> 'classification' ->> 'sensitivity' IS DISTINCT FROM
47
+ (CASE WHEN m.visibility = 'restricted' THEN 'restricted' ELSE 'internal' END);
48
+
49
+ DO $$
50
+ DECLARE v_unclassified int;
51
+ BEGIN
52
+ SELECT count(*) INTO v_unclassified
53
+ FROM semicolony.knowledge_base
54
+ WHERE key = 'meeting'
55
+ AND metadata -> 'classification' ->> 'sensitivity' IS NULL;
56
+ IF v_unclassified > 0 THEN
57
+ RAISE EXCEPTION 'still % meeting projection(s) without a classification', v_unclassified;
58
+ END IF;
59
+ END $$;
60
+
61
+ -- ---------------------------------------------------------------- Part B
62
+ -- A meeting projection may not exist without a classification.
63
+ --
64
+ -- Enforced in the database rather than in each writer, because the writer is
65
+ -- exactly what went wrong: the capability emits the key correctly and something
66
+ -- else still produced 74 rows without it. A trigger holds regardless of who
67
+ -- writes, including a direct UPDATE by an operator script.
68
+ --
69
+ -- It derives rather than rejects. Refusing the write would turn a metadata slip
70
+ -- into a lost meeting record, and losing the record is the failure this whole
71
+ -- system was built to stop. Deriving keeps the record and closes the hole.
72
+ --
73
+ -- Fails closed: a projection naming no resolvable meeting gets `restricted`.
74
+
75
+ CREATE OR REPLACE FUNCTION semicolony.enforce_meeting_projection_classification()
76
+ RETURNS trigger
77
+ LANGUAGE plpgsql
78
+ SECURITY DEFINER
79
+ SET search_path = pg_catalog
80
+ AS $function$
81
+ DECLARE
82
+ v_visibility text;
83
+ v_sensitivity text;
84
+ BEGIN
85
+ IF NEW.key IS DISTINCT FROM 'meeting' THEN
86
+ RETURN NEW;
87
+ END IF;
88
+ IF NEW.metadata -> 'classification' ->> 'sensitivity' IS NOT NULL THEN
89
+ RETURN NEW;
90
+ END IF;
91
+
92
+ SELECT m.visibility INTO v_visibility
93
+ FROM semicolony.meetings m
94
+ WHERE m.meeting_id::text = NEW.metadata ->> 'meeting_id';
95
+
96
+ -- Unknown provenance is treated as confidential, not as internal.
97
+ v_sensitivity := CASE
98
+ WHEN v_visibility IS NULL THEN 'restricted'
99
+ WHEN v_visibility = 'restricted' THEN 'restricted'
100
+ ELSE 'internal'
101
+ END;
102
+
103
+ NEW.metadata := COALESCE(NEW.metadata, '{}'::jsonb) || jsonb_build_object(
104
+ 'classification', jsonb_build_object('sensitivity', v_sensitivity)
105
+ );
106
+ RETURN NEW;
107
+ END;
108
+ $function$;
109
+
110
+ DROP TRIGGER IF EXISTS meeting_projection_classification ON semicolony.knowledge_base;
111
+ CREATE TRIGGER meeting_projection_classification
112
+ BEFORE INSERT OR UPDATE ON semicolony.knowledge_base
113
+ FOR EACH ROW
114
+ EXECUTE FUNCTION semicolony.enforce_meeting_projection_classification();
@@ -0,0 +1,79 @@
1
+ -- 209_action_item_project_any_ontology_domain.sql
2
+ --
3
+ -- 액션 아이템의 project_domain 을 ontology 의 모든 엔티티 종류에 개방한다.
4
+ --
5
+ -- 203_action_item_redesign_cutover.sql 은 project_entity 를
6
+ -- project_entity text GENERATED ALWAYS AS ('service') STORED
7
+ -- 로 고정하고 (project_domain, project_entity) 복합 FK 를
8
+ -- ontology(domain, entity_type) 로 걸었다. 상수 컬럼과 복합 FK 의 조합은 곧
9
+ -- "project_domain 은 entity_type='service' 인 도메인만 받는다" 라는 제약이다.
10
+ --
11
+ -- 이 제약은 의도한 것보다 넓게 막고 있었다:
12
+ --
13
+ -- * 팀 차원의 일(결제·계약 행정 등)은 organization 도메인 'semicolon' 에
14
+ -- 귀속되는데 등록 자체가 23503 으로 거부된다. 2026-08-18 실측: 토스페이먼츠
15
+ -- 서류 보완 3건이 이 이유로 등록되지 못했다.
16
+ -- * meeting-generate 는 meeting.target_domain 이 organization 이거나 person
17
+ -- 이면 그 회의의 액션 아이템 전량을 건너뛰고 경고만 남긴다
18
+ -- (packages/semicolony-dashboard/lib/core/meeting-generate.ts).
19
+ -- 'semicolon' 을 대상으로 한 회의는 액션 아이템을 한 건도 남기지 못한다.
20
+ --
21
+ -- ontology 에는 이미 domain 단일 UNIQUE 제약(ontology_domain_key)이 있고, 실제로
22
+ -- 두 개 이상의 entity_type 을 갖는 domain 은 하나도 없다. 즉 domain 하나로 행이
23
+ -- 유일하게 정해지므로, 복합 FK 는 "도메인이 실재하는가" 검증에 아무 것도 보태지
24
+ -- 않고 오직 종류를 'service' 로 고정하는 역할만 한다. 단일 컬럼 FK 로 바꾸면
25
+ -- 실재 검증은 그대로 유지되고 종류 제한만 사라진다.
26
+ --
27
+ -- project_entity 컬럼은 지우지 않는다 — 읽는 쪽이 있고(dashboard ActionItemRow),
28
+ -- "이 도메인이 무엇인가" 는 조회할 때마다 조인하는 것보다 행에 두는 편이 싸다.
29
+ -- 다만 상수가 아니라 ontology 실측값을 트리거로 채운다. 상수였을 때는 이 컬럼이
30
+ -- 정보를 담지 않았고(모든 행이 'service'), 제약을 거는 도구로만 쓰였다.
31
+ --
32
+ -- 게이트웨이의 23503 → 'project_domain is not a registered project' 매핑은 그대로
33
+ -- 유효하다. 등록되지 않은 도메인은 여전히 단일 컬럼 FK 에서 23503 으로 막힌다.
34
+
35
+ BEGIN;
36
+
37
+ -- 1. 종류를 고정하던 복합 FK 를 걷어낸다.
38
+ ALTER TABLE semicolony.action_items
39
+ DROP CONSTRAINT action_items_project_domain_project_entity_fkey;
40
+
41
+ -- 2. 생성 표현식('service' 상수)을 떼어 평범한 컬럼으로 만든다. PG 14+.
42
+ ALTER TABLE semicolony.action_items
43
+ ALTER COLUMN project_entity DROP EXPRESSION;
44
+
45
+ -- 3. 도메인 실재 검증은 단일 컬럼 FK 로 유지한다.
46
+ ALTER TABLE semicolony.action_items
47
+ ADD CONSTRAINT action_items_project_domain_fkey
48
+ FOREIGN KEY (project_domain) REFERENCES semicolony.ontology(domain);
49
+
50
+ -- 4. project_entity 는 ontology 실측값을 따라간다.
51
+ -- 도메인이 ontology 에 없으면 NULL 이 남고, 3번 FK 가 그 행을 23503 으로 막는다.
52
+ CREATE OR REPLACE FUNCTION semicolony.action_item_set_project_entity()
53
+ RETURNS trigger
54
+ LANGUAGE plpgsql
55
+ AS $$
56
+ BEGIN
57
+ SELECT o.entity_type
58
+ INTO NEW.project_entity
59
+ FROM semicolony.ontology o
60
+ WHERE o.domain = NEW.project_domain;
61
+ RETURN NEW;
62
+ END;
63
+ $$;
64
+
65
+ DROP TRIGGER IF EXISTS action_items_set_project_entity ON semicolony.action_items;
66
+ CREATE TRIGGER action_items_set_project_entity
67
+ BEFORE INSERT OR UPDATE OF project_domain ON semicolony.action_items
68
+ FOR EACH ROW
69
+ EXECUTE FUNCTION semicolony.action_item_set_project_entity();
70
+
71
+ -- 5. 기존 행 보정. 지금은 전부 'service' 라 실질 no-op 이지만, 컬럼의 의미가
72
+ -- "상수" 에서 "ontology 실측값" 으로 바뀌었으므로 값을 그 의미에 맞춘다.
73
+ UPDATE semicolony.action_items a
74
+ SET project_entity = o.entity_type
75
+ FROM semicolony.ontology o
76
+ WHERE o.domain = a.project_domain
77
+ AND a.project_entity IS DISTINCT FROM o.entity_type;
78
+
79
+ COMMIT;
@@ -0,0 +1,52 @@
1
+ -- 어드민 ops 사용자가 `restricted` 지식을 읽을 수 있게 한다.
2
+ --
3
+ -- 문제. 대시보드는 사용자와 무관하게 DB 롤 하나(sc_fleet_board)로 접속하고,
4
+ -- 어드민 판정은 Supabase `user_profiles.role='admin'` 이라는 앱 레이어 값이다
5
+ -- (lib/ops/auth.ts evaluateOpsAccess). RLS 는 그 값을 볼 수 없으므로 "어드민만"
6
+ -- 이라는 조건을 정책 술어로 쓸 수 없다. 정책에서 sc_fleet_board 를 빼면 승인된
7
+ -- ops 멤버 전원(2026-08-19 실측 33명)이 고객 단가·계약 조건을 읽게 된다.
8
+ --
9
+ -- 방법. 읽기 표면이 같고 등급 제한만 없는 롤을 하나 더 만들고, 대시보드가
10
+ -- `isAdmin` 일 때만 그 롤의 풀을 쓴다. 판단은 앱이 하되 경계는 DB 가 강제한다 —
11
+ -- 이미 sc_app / sc_fleet_board / sc_ops_records 로 나눠 온 것과 같은 방식이다.
12
+ --
13
+ -- 왜 멤버십 상속이 안전한가. RLS 정책 매칭은 `pg_has_role(current_user, ...,
14
+ -- 'MEMBER')` 이라 sc_fleet_board_admin 은 knowledge_base_hide_restricted 에도
15
+ -- 매칭된다. 그런데 knowledge_base 의 정책은 전부 PERMISSIVE 라 매칭된 정책이
16
+ -- OR 로 합쳐지고, 아래 USING (true) 가 제한을 덮는다(2026-08-19 pg_policy
17
+ -- polpermissive=true 실측). 반대 방향은 성립하지 않는다 — sc_fleet_board 는
18
+ -- 이 새 정책에 매칭되지 않으므로 제한이 그대로 남는다. 훗날 누군가
19
+ -- hide_restricted 를 RESTRICTIVE 로 바꾸면 어드민이 못 읽게 되지 어드민이 아닌
20
+ -- 쪽이 더 읽게 되지는 않는다 — 안전한 방향으로 깨진다.
21
+ --
22
+ -- 비밀번호는 여기서 정하지 않는다. 마이그레이션은 git 에 남고 자격증명은 남으면
23
+ -- 안 된다. 운영자가 별도로 ALTER ROLE ... PASSWORD 로 부여한다.
24
+ DO $$
25
+ BEGIN
26
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'sc_fleet_board_admin') THEN
27
+ CREATE ROLE sc_fleet_board_admin LOGIN;
28
+ END IF;
29
+ END $$;
30
+
31
+ -- 읽기 표면은 sc_fleet_board 와 정확히 같다. 목록을 복제하면 한쪽만 늘어나
32
+ -- 갈라지므로 멤버십으로 상속시킨다.
33
+ GRANT sc_fleet_board TO sc_fleet_board_admin;
34
+
35
+ DROP POLICY IF EXISTS knowledge_base_ops_admin_read ON semo_ops.knowledge_base;
36
+ CREATE POLICY knowledge_base_ops_admin_read ON semo_ops.knowledge_base
37
+ FOR SELECT TO sc_fleet_board_admin USING (true);
38
+
39
+ -- 이 롤은 읽기 전용이다. 상속으로 들어온 worker_fleet_audits INSERT 를 제외하면
40
+ -- 어떤 쓰기 경로도 갖지 않으며, 등급 해제는 SELECT 한 명령에만 적용된다.
41
+ DO $$
42
+ DECLARE v_bad text;
43
+ BEGIN
44
+ SELECT string_agg(DISTINCT privilege_type, ',') INTO v_bad
45
+ FROM information_schema.role_table_grants
46
+ WHERE grantee = 'sc_fleet_board_admin'
47
+ AND table_schema = 'semo_ops' AND table_name = 'knowledge_base'
48
+ AND privilege_type IN ('INSERT','UPDATE','DELETE');
49
+ IF v_bad IS NOT NULL THEN
50
+ RAISE EXCEPTION 'sc_fleet_board_admin must stay read-only on knowledge_base (got %)', v_bad;
51
+ END IF;
52
+ END $$;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semicolony-cli",
3
- "version": "4.18.107",
3
+ "version": "4.18.108",
4
4
  "description": "SemiColony CLI - AI operations and agent orchestration installer",
5
5
  "main": "dist/bundle.js",
6
6
  "bin": {