@team-semicolon/semicolony-cli 4.18.127 → 4.18.128
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,5 @@
|
|
|
1
|
+
-- /ops/actions now reads per-item history as sc_fleet_board. Migration 203
|
|
2
|
+
-- deliberately omitted events before that reader existed; 226 retained it.
|
|
3
|
+
-- Add only the missing read privilege. Mutations still use guarded functions.
|
|
4
|
+
-- Qualified schema identifiers are retargeted by the production migration runner.
|
|
5
|
+
GRANT SELECT ON TABLE semicolony.action_item_events TO sc_fleet_board;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
-- 229_worker_asset_default_policy.sql
|
|
2
|
+
--
|
|
3
|
+
-- Materialize the approved person-bound asset policy as the exact project
|
|
4
|
+
-- grants already consumed by the Worker Gateway. This deliberately adds no
|
|
5
|
+
-- wildcard authorization path and does not alter Worker-agent actors.
|
|
6
|
+
--
|
|
7
|
+
-- Decision:
|
|
8
|
+
-- semicolon/decision/reus-worker-default-all-project-asset-read-write-2026-09-14
|
|
9
|
+
--
|
|
10
|
+
-- The migration runner owns the transaction boundary.
|
|
11
|
+
|
|
12
|
+
CREATE TABLE IF NOT EXISTS semicolony.worker_asset_access_policies (
|
|
13
|
+
policy_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
14
|
+
member_id uuid NOT NULL
|
|
15
|
+
REFERENCES semicolony.team_members(member_id) ON DELETE RESTRICT,
|
|
16
|
+
selector text NOT NULL CHECK (selector = 'all-team-projects'),
|
|
17
|
+
access_mode text NOT NULL CHECK (access_mode = 'read-write'),
|
|
18
|
+
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked')),
|
|
19
|
+
granted_by text NOT NULL CHECK (NULLIF(btrim(granted_by), '') IS NOT NULL),
|
|
20
|
+
reason text NOT NULL CHECK (NULLIF(btrim(reason), '') IS NOT NULL),
|
|
21
|
+
decision_ref text NOT NULL CHECK (NULLIF(btrim(decision_ref), '') IS NOT NULL),
|
|
22
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
23
|
+
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
24
|
+
last_reconciled_at timestamptz,
|
|
25
|
+
UNIQUE (member_id, selector)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
COMMENT ON TABLE semicolony.worker_asset_access_policies IS
|
|
29
|
+
'Audited person-bound policies materialized as exact Worker workspace asset grants.';
|
|
30
|
+
|
|
31
|
+
CREATE OR REPLACE FUNCTION semicolony.reconcile_worker_asset_access_policy(uuid)
|
|
32
|
+
RETURNS integer
|
|
33
|
+
LANGUAGE plpgsql
|
|
34
|
+
SECURITY DEFINER
|
|
35
|
+
SET search_path = pg_catalog
|
|
36
|
+
AS $function$
|
|
37
|
+
DECLARE
|
|
38
|
+
p_policy_id alias for $1;
|
|
39
|
+
v_policy record;
|
|
40
|
+
v_credential record;
|
|
41
|
+
v_previous_scopes text[];
|
|
42
|
+
v_next_scopes text[];
|
|
43
|
+
v_changed integer;
|
|
44
|
+
v_total integer := 0;
|
|
45
|
+
BEGIN
|
|
46
|
+
SELECT p.policy_id, p.granted_by, p.reason, p.decision_ref, tm.domain AS member_domain
|
|
47
|
+
INTO v_policy
|
|
48
|
+
FROM semicolony.worker_asset_access_policies AS p
|
|
49
|
+
JOIN semicolony.team_members AS tm ON tm.member_id = p.member_id
|
|
50
|
+
WHERE p.policy_id = p_policy_id
|
|
51
|
+
AND p.status = 'active'
|
|
52
|
+
AND p.selector = 'all-team-projects'
|
|
53
|
+
AND p.access_mode = 'read-write'
|
|
54
|
+
AND tm.status = 'active';
|
|
55
|
+
IF NOT FOUND THEN
|
|
56
|
+
RETURN 0;
|
|
57
|
+
END IF;
|
|
58
|
+
|
|
59
|
+
FOR v_credential IN
|
|
60
|
+
SELECT gc.id, gc.scopes
|
|
61
|
+
FROM semicolony.gateway_credentials AS gc
|
|
62
|
+
WHERE gc.status = 'active'
|
|
63
|
+
AND gc.revoked_at IS NULL
|
|
64
|
+
AND (gc.expires_at IS NULL OR gc.expires_at > clock_timestamp())
|
|
65
|
+
AND gc.metadata ->> 'credential_kind' = 'worker'
|
|
66
|
+
AND gc.metadata ->> 'worker_id' = v_policy.member_domain
|
|
67
|
+
FOR UPDATE
|
|
68
|
+
LOOP
|
|
69
|
+
INSERT INTO semicolony.workspace_projection_grants AS existing (
|
|
70
|
+
gateway_credential_id,
|
|
71
|
+
project_id,
|
|
72
|
+
capability,
|
|
73
|
+
environments,
|
|
74
|
+
status,
|
|
75
|
+
expires_at,
|
|
76
|
+
metadata
|
|
77
|
+
)
|
|
78
|
+
SELECT
|
|
79
|
+
v_credential.id,
|
|
80
|
+
project.domain,
|
|
81
|
+
capability.name,
|
|
82
|
+
ARRAY[]::text[],
|
|
83
|
+
'active',
|
|
84
|
+
NULL,
|
|
85
|
+
jsonb_build_object(
|
|
86
|
+
'source', 'worker-asset-access-policy',
|
|
87
|
+
'policy_id', v_policy.policy_id,
|
|
88
|
+
'decision_ref', v_policy.decision_ref,
|
|
89
|
+
'granted_by', v_policy.granted_by,
|
|
90
|
+
'reason', v_policy.reason
|
|
91
|
+
)
|
|
92
|
+
FROM semicolony.ontology AS project
|
|
93
|
+
CROSS JOIN (VALUES ('assets-read'), ('assets-write')) AS capability(name)
|
|
94
|
+
WHERE project.entity_type IN ('service', 'project')
|
|
95
|
+
OR project.domain = 'semicolon'
|
|
96
|
+
ON CONFLICT (gateway_credential_id, project_id, capability) DO UPDATE
|
|
97
|
+
SET status = 'active',
|
|
98
|
+
expires_at = NULL,
|
|
99
|
+
metadata = CASE
|
|
100
|
+
WHEN existing.metadata ->> 'source' = 'worker-asset-access-policy'
|
|
101
|
+
THEN EXCLUDED.metadata
|
|
102
|
+
ELSE existing.metadata
|
|
103
|
+
END
|
|
104
|
+
WHERE existing.status IS DISTINCT FROM 'active'
|
|
105
|
+
OR existing.expires_at IS NOT NULL
|
|
106
|
+
OR (
|
|
107
|
+
existing.metadata ->> 'source' = 'worker-asset-access-policy'
|
|
108
|
+
AND existing.metadata IS DISTINCT FROM EXCLUDED.metadata
|
|
109
|
+
);
|
|
110
|
+
GET DIAGNOSTICS v_changed = ROW_COUNT;
|
|
111
|
+
v_total := v_total + v_changed;
|
|
112
|
+
|
|
113
|
+
v_previous_scopes := v_credential.scopes;
|
|
114
|
+
v_next_scopes := v_previous_scopes;
|
|
115
|
+
IF NOT ('workspace:assets-read' = ANY(v_next_scopes)) THEN
|
|
116
|
+
v_next_scopes := array_append(v_next_scopes, 'workspace:assets-read');
|
|
117
|
+
END IF;
|
|
118
|
+
IF NOT ('workspace:assets-write' = ANY(v_next_scopes)) THEN
|
|
119
|
+
v_next_scopes := array_append(v_next_scopes, 'workspace:assets-write');
|
|
120
|
+
END IF;
|
|
121
|
+
|
|
122
|
+
IF v_next_scopes IS DISTINCT FROM v_previous_scopes THEN
|
|
123
|
+
INSERT INTO semicolony.gateway_credential_scope_amendments (
|
|
124
|
+
credential_id,
|
|
125
|
+
previous_scopes,
|
|
126
|
+
next_scopes,
|
|
127
|
+
reason,
|
|
128
|
+
amended_by
|
|
129
|
+
) VALUES (
|
|
130
|
+
v_credential.id,
|
|
131
|
+
v_previous_scopes,
|
|
132
|
+
v_next_scopes,
|
|
133
|
+
v_policy.reason,
|
|
134
|
+
v_policy.granted_by
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
UPDATE semicolony.gateway_credentials
|
|
138
|
+
SET scopes = v_next_scopes
|
|
139
|
+
WHERE id = v_credential.id;
|
|
140
|
+
END IF;
|
|
141
|
+
END LOOP;
|
|
142
|
+
|
|
143
|
+
UPDATE semicolony.worker_asset_access_policies
|
|
144
|
+
SET last_reconciled_at = clock_timestamp(),
|
|
145
|
+
updated_at = clock_timestamp()
|
|
146
|
+
WHERE policy_id = p_policy_id;
|
|
147
|
+
|
|
148
|
+
RETURN v_total;
|
|
149
|
+
END;
|
|
150
|
+
$function$;
|
|
151
|
+
|
|
152
|
+
CREATE OR REPLACE FUNCTION semicolony.reconcile_worker_asset_policy_for_ontology()
|
|
153
|
+
RETURNS trigger
|
|
154
|
+
LANGUAGE plpgsql
|
|
155
|
+
SECURITY DEFINER
|
|
156
|
+
SET search_path = pg_catalog
|
|
157
|
+
AS $function$
|
|
158
|
+
DECLARE
|
|
159
|
+
v_policy_id uuid;
|
|
160
|
+
BEGIN
|
|
161
|
+
IF NEW.entity_type IN ('service', 'project') OR NEW.domain = 'semicolon' THEN
|
|
162
|
+
FOR v_policy_id IN
|
|
163
|
+
SELECT policy_id
|
|
164
|
+
FROM semicolony.worker_asset_access_policies
|
|
165
|
+
WHERE status = 'active'
|
|
166
|
+
LOOP
|
|
167
|
+
PERFORM semicolony.reconcile_worker_asset_access_policy(v_policy_id);
|
|
168
|
+
END LOOP;
|
|
169
|
+
END IF;
|
|
170
|
+
RETURN NEW;
|
|
171
|
+
END;
|
|
172
|
+
$function$;
|
|
173
|
+
|
|
174
|
+
CREATE OR REPLACE FUNCTION semicolony.reconcile_worker_asset_policy_for_credential()
|
|
175
|
+
RETURNS trigger
|
|
176
|
+
LANGUAGE plpgsql
|
|
177
|
+
SECURITY DEFINER
|
|
178
|
+
SET search_path = pg_catalog
|
|
179
|
+
AS $function$
|
|
180
|
+
DECLARE
|
|
181
|
+
v_policy_id uuid;
|
|
182
|
+
BEGIN
|
|
183
|
+
IF NEW.status = 'active'
|
|
184
|
+
AND NEW.revoked_at IS NULL
|
|
185
|
+
AND (NEW.expires_at IS NULL OR NEW.expires_at > clock_timestamp())
|
|
186
|
+
AND NEW.metadata ->> 'credential_kind' = 'worker' THEN
|
|
187
|
+
FOR v_policy_id IN
|
|
188
|
+
SELECT p.policy_id
|
|
189
|
+
FROM semicolony.worker_asset_access_policies AS p
|
|
190
|
+
JOIN semicolony.team_members AS tm ON tm.member_id = p.member_id
|
|
191
|
+
WHERE p.status = 'active'
|
|
192
|
+
AND tm.status = 'active'
|
|
193
|
+
AND tm.domain = NEW.metadata ->> 'worker_id'
|
|
194
|
+
LOOP
|
|
195
|
+
PERFORM semicolony.reconcile_worker_asset_access_policy(v_policy_id);
|
|
196
|
+
END LOOP;
|
|
197
|
+
END IF;
|
|
198
|
+
RETURN NEW;
|
|
199
|
+
END;
|
|
200
|
+
$function$;
|
|
201
|
+
|
|
202
|
+
DROP TRIGGER IF EXISTS trg_worker_asset_policy_ontology ON semicolony.ontology;
|
|
203
|
+
CREATE TRIGGER trg_worker_asset_policy_ontology
|
|
204
|
+
AFTER INSERT OR UPDATE OF domain, entity_type ON semicolony.ontology
|
|
205
|
+
FOR EACH ROW
|
|
206
|
+
EXECUTE FUNCTION semicolony.reconcile_worker_asset_policy_for_ontology();
|
|
207
|
+
|
|
208
|
+
DROP TRIGGER IF EXISTS trg_worker_asset_policy_credential ON semicolony.gateway_credentials;
|
|
209
|
+
CREATE CONSTRAINT TRIGGER trg_worker_asset_policy_credential
|
|
210
|
+
AFTER INSERT ON semicolony.gateway_credentials
|
|
211
|
+
DEFERRABLE INITIALLY DEFERRED
|
|
212
|
+
FOR EACH ROW
|
|
213
|
+
EXECUTE FUNCTION semicolony.reconcile_worker_asset_policy_for_credential();
|
|
214
|
+
|
|
215
|
+
DO $reus_member_guard$
|
|
216
|
+
BEGIN
|
|
217
|
+
IF NOT EXISTS (
|
|
218
|
+
SELECT 1
|
|
219
|
+
FROM semicolony.team_members AS tm
|
|
220
|
+
WHERE tm.domain = 'reus'
|
|
221
|
+
AND tm.status = 'active'
|
|
222
|
+
) THEN
|
|
223
|
+
RAISE EXCEPTION 'active reus team member is required for worker asset policy'
|
|
224
|
+
USING ERRCODE = '22023';
|
|
225
|
+
END IF;
|
|
226
|
+
END;
|
|
227
|
+
$reus_member_guard$;
|
|
228
|
+
|
|
229
|
+
INSERT INTO semicolony.worker_asset_access_policies (
|
|
230
|
+
member_id,
|
|
231
|
+
selector,
|
|
232
|
+
access_mode,
|
|
233
|
+
status,
|
|
234
|
+
granted_by,
|
|
235
|
+
reason,
|
|
236
|
+
decision_ref
|
|
237
|
+
)
|
|
238
|
+
SELECT
|
|
239
|
+
tm.member_id,
|
|
240
|
+
'all-team-projects',
|
|
241
|
+
'read-write',
|
|
242
|
+
'active',
|
|
243
|
+
'reus',
|
|
244
|
+
'Approved default asset read/write access for all current and future team projects',
|
|
245
|
+
'semicolon/decision/reus-worker-default-all-project-asset-read-write-2026-09-14'
|
|
246
|
+
FROM semicolony.team_members AS tm
|
|
247
|
+
WHERE tm.domain = 'reus'
|
|
248
|
+
AND tm.status = 'active'
|
|
249
|
+
ON CONFLICT (member_id, selector) DO UPDATE
|
|
250
|
+
SET access_mode = EXCLUDED.access_mode,
|
|
251
|
+
status = EXCLUDED.status,
|
|
252
|
+
granted_by = EXCLUDED.granted_by,
|
|
253
|
+
reason = EXCLUDED.reason,
|
|
254
|
+
decision_ref = EXCLUDED.decision_ref,
|
|
255
|
+
updated_at = clock_timestamp();
|
|
256
|
+
|
|
257
|
+
DO $initial_reconciliation$
|
|
258
|
+
DECLARE
|
|
259
|
+
v_policy_id uuid;
|
|
260
|
+
BEGIN
|
|
261
|
+
SELECT p.policy_id
|
|
262
|
+
INTO STRICT v_policy_id
|
|
263
|
+
FROM semicolony.worker_asset_access_policies AS p
|
|
264
|
+
JOIN semicolony.team_members AS tm ON tm.member_id = p.member_id
|
|
265
|
+
WHERE tm.domain = 'reus'
|
|
266
|
+
AND p.selector = 'all-team-projects'
|
|
267
|
+
AND p.status = 'active';
|
|
268
|
+
PERFORM semicolony.reconcile_worker_asset_access_policy(v_policy_id);
|
|
269
|
+
END;
|
|
270
|
+
$initial_reconciliation$;
|
|
271
|
+
|
|
272
|
+
REVOKE ALL ON TABLE semicolony.worker_asset_access_policies
|
|
273
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
274
|
+
REVOKE ALL ON FUNCTION semicolony.reconcile_worker_asset_access_policy(uuid)
|
|
275
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
276
|
+
REVOKE ALL ON FUNCTION semicolony.reconcile_worker_asset_policy_for_ontology()
|
|
277
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
278
|
+
REVOKE ALL ON FUNCTION semicolony.reconcile_worker_asset_policy_for_credential()
|
|
279
|
+
FROM PUBLIC, sc_app, sc_provider;
|
package/migrations/README.md
CHANGED
|
@@ -30,6 +30,7 @@ SEMICOLONY CLI의 PostgreSQL 마이그레이션 파일들.
|
|
|
30
30
|
| `196_ops_control_room_board_grants.sql` | `sc_fleet_board` 에 `action_items`·`ontology`·`knowledge_base` SELECT 부여 + knowledge_base SELECT 정책 롤 확장 — /ops 홈의 `permission denied` 카드 수정 |
|
|
31
31
|
| `218_feedback_loop_state.sql` | 피드백 해소 루프 상태를 KB slug 키로 DB 에 둔다 (ledger.json 대체) |
|
|
32
32
|
| `219_feedback_action_item_on_demand.sql` | `record_feedback` 이 액션아이템을 만들지 않고, `duplicate` 를 재발 카운터로 판정한다 |
|
|
33
|
+
| `229_worker_asset_default_policy.sql` | `reus` 팀원의 현재·미래 Human Worker 자격증명에 모든 팀 프로젝트 자산 read/write 권한을 exact grant로 감사 가능하게 자동 반영 |
|
|
33
34
|
|
|
34
35
|
## 007: Flat Skill Naming
|
|
35
36
|
|