@team-semicolon/semicolony-cli 4.18.51 → 4.18.52

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,529 @@
1
+ -- @schema-retarget: off
2
+ -- SEMO Ops skill release, capability, proposal, and project audit contract.
3
+ --
4
+ -- This migration is additive and contains no production backfill. The migration
5
+ -- runner supplies the transaction boundary; do not add BEGIN/COMMIT here.
6
+
7
+ CREATE TABLE IF NOT EXISTS semicolony.skill_capability_leases (
8
+ lease_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
9
+ lease_family_id UUID NOT NULL,
10
+ lease_version INTEGER NOT NULL CHECK (lease_version > 0),
11
+ credential_id UUID NOT NULL
12
+ REFERENCES semicolony.gateway_credentials (id),
13
+ principal_id TEXT NOT NULL,
14
+ worker_id TEXT NOT NULL,
15
+ device_id TEXT NOT NULL,
16
+ task_id TEXT NOT NULL,
17
+ profile TEXT NOT NULL
18
+ CHECK (profile IN ('read', 'make', 'operate')),
19
+ operations TEXT[] NOT NULL,
20
+ resources TEXT[] NOT NULL,
21
+ channel_scope TEXT,
22
+ project_scope TEXT,
23
+ release_scope UUID,
24
+ not_before TIMESTAMPTZ NOT NULL,
25
+ expires_at TIMESTAMPTZ NOT NULL,
26
+ approval_reference TEXT,
27
+ issued_by_principal TEXT NOT NULL,
28
+ created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
29
+ UNIQUE (lease_family_id, lease_version),
30
+ CHECK (NULLIF(btrim(principal_id), '') IS NOT NULL),
31
+ CHECK (NULLIF(btrim(worker_id), '') IS NOT NULL),
32
+ CHECK (NULLIF(btrim(device_id), '') IS NOT NULL),
33
+ CHECK (NULLIF(btrim(task_id), '') IS NOT NULL),
34
+ CHECK (NULLIF(btrim(issued_by_principal), '') IS NOT NULL),
35
+ CHECK (cardinality(operations) > 0),
36
+ CHECK (NOT ('*' = ANY(operations))),
37
+ CHECK (array_position(operations, NULL) IS NULL),
38
+ CHECK (
39
+ (
40
+ profile = 'read'
41
+ AND operations <@ ARRAY[
42
+ 'skills.health.read',
43
+ 'skills.release.read'
44
+ ]::TEXT[]
45
+ )
46
+ OR
47
+ (
48
+ profile = 'make'
49
+ AND operations <@ ARRAY[
50
+ 'skills.health.read',
51
+ 'skills.release.read',
52
+ 'skills.proposal.write',
53
+ 'skills.projection.reconcile',
54
+ 'skills.project.write',
55
+ 'skills.release.stage'
56
+ ]::TEXT[]
57
+ )
58
+ OR
59
+ (
60
+ profile = 'operate'
61
+ AND operations <@ ARRAY[
62
+ 'skills.health.read',
63
+ 'skills.release.read',
64
+ 'skills.proposal.write',
65
+ 'skills.projection.reconcile',
66
+ 'skills.project.write',
67
+ 'skills.release.stage',
68
+ 'skills.project.publish',
69
+ 'skills.release.activate',
70
+ 'skills.audience.manage'
71
+ ]::TEXT[]
72
+ )
73
+ ),
74
+ CHECK (cardinality(resources) > 0),
75
+ CHECK (NOT ('*' = ANY(resources))),
76
+ CHECK (array_position(resources, NULL) IS NULL),
77
+ CHECK (not_before < expires_at),
78
+ CHECK (expires_at <= not_before + INTERVAL '60 minutes'),
79
+ CHECK (
80
+ profile = 'read'
81
+ OR NULLIF(btrim(approval_reference), '') IS NOT NULL
82
+ )
83
+ );
84
+
85
+ COMMENT ON TABLE semicolony.skill_capability_leases IS
86
+ 'Immutable task-scoped Knowledge Fabric skill capability lease versions. Worker credentials are coarse route gates only.';
87
+
88
+ CREATE INDEX IF NOT EXISTS idx_skill_capability_leases_binding
89
+ ON semicolony.skill_capability_leases
90
+ (worker_id, device_id, task_id, principal_id, lease_family_id, lease_version DESC);
91
+
92
+ CREATE INDEX IF NOT EXISTS idx_skill_capability_leases_validity
93
+ ON semicolony.skill_capability_leases (not_before, expires_at);
94
+
95
+ CREATE TABLE IF NOT EXISTS semicolony.skill_capability_lease_revocations (
96
+ revocation_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
97
+ lease_family_id UUID NOT NULL,
98
+ revoked_through_version INTEGER NOT NULL CHECK (revoked_through_version > 0),
99
+ reason TEXT NOT NULL CHECK (NULLIF(btrim(reason), '') IS NOT NULL),
100
+ revoked_by_principal TEXT NOT NULL
101
+ CHECK (NULLIF(btrim(revoked_by_principal), '') IS NOT NULL),
102
+ revoked_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
103
+ UNIQUE (lease_family_id, revoked_through_version),
104
+ FOREIGN KEY (lease_family_id, revoked_through_version)
105
+ REFERENCES semicolony.skill_capability_leases (lease_family_id, lease_version)
106
+ );
107
+
108
+ COMMENT ON TABLE semicolony.skill_capability_lease_revocations IS
109
+ 'Append-only revocations for a lease family through an exact issued version.';
110
+
111
+ CREATE TABLE IF NOT EXISTS semicolony.skill_releases (
112
+ release_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
113
+ channel TEXT NOT NULL CHECK (NULLIF(btrim(channel), '') IS NOT NULL),
114
+ skill_name TEXT NOT NULL
115
+ CHECK (skill_name ~ '^[a-z0-9]+(?:-[a-z0-9]+)*$'),
116
+ source_repository TEXT NOT NULL
117
+ CHECK (NULLIF(btrim(source_repository), '') IS NOT NULL),
118
+ git_commit_sha TEXT NOT NULL
119
+ CHECK (git_commit_sha ~ '^[0-9a-f]{40}$'),
120
+ git_tree_oid TEXT NOT NULL
121
+ CHECK (git_tree_oid ~ '^[0-9a-f]{40}$'),
122
+ tree_sha256 TEXT NOT NULL
123
+ CHECK (tree_sha256 ~ '^[0-9a-f]{64}$'),
124
+ manifest_sha256 TEXT NOT NULL
125
+ CHECK (manifest_sha256 ~ '^[0-9a-f]{64}$'),
126
+ file_count INTEGER NOT NULL CHECK (file_count > 0),
127
+ total_bytes BIGINT NOT NULL CHECK (total_bytes >= 0),
128
+ audiences TEXT[] NOT NULL DEFAULT ARRAY['worker']::TEXT[],
129
+ created_by_principal TEXT NOT NULL
130
+ CHECK (NULLIF(btrim(created_by_principal), '') IS NOT NULL),
131
+ created_under_lease_id UUID NOT NULL
132
+ REFERENCES semicolony.skill_capability_leases (lease_id),
133
+ staged_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
134
+ UNIQUE (release_id, channel, skill_name),
135
+ UNIQUE (release_id, channel, skill_name, tree_sha256),
136
+ UNIQUE (source_repository, git_commit_sha, channel, skill_name),
137
+ UNIQUE (source_repository, git_commit_sha, channel, skill_name, tree_sha256),
138
+ CHECK (cardinality(audiences) > 0),
139
+ CHECK (array_position(audiences, NULL) IS NULL),
140
+ CHECK (audiences <@ ARRAY['worker', 'semi', 'colony']::TEXT[])
141
+ );
142
+
143
+ COMMENT ON TABLE semicolony.skill_releases IS
144
+ 'Immutable complete common-skill release headers. Runtime authority is selected only by activation rows.';
145
+
146
+ CREATE INDEX IF NOT EXISTS idx_skill_releases_channel_skill
147
+ ON semicolony.skill_releases (channel, skill_name, staged_at DESC);
148
+
149
+ CREATE TABLE IF NOT EXISTS semicolony.skill_release_files (
150
+ release_id UUID NOT NULL
151
+ REFERENCES semicolony.skill_releases (release_id),
152
+ relative_path TEXT NOT NULL,
153
+ file_mode INTEGER NOT NULL CHECK (file_mode IN (420, 493)),
154
+ size BIGINT NOT NULL CHECK (size >= 0),
155
+ content_sha256 TEXT NOT NULL
156
+ CHECK (content_sha256 ~ '^[0-9a-f]{64}$'),
157
+ content BYTEA NOT NULL,
158
+ PRIMARY KEY (release_id, relative_path),
159
+ CHECK (relative_path <> ''),
160
+ CHECK (relative_path !~ '^/'),
161
+ CHECK (relative_path !~ E'\\\\'),
162
+ CHECK (relative_path !~ '(^|/)\.{1,2}(/|$)'),
163
+ CHECK (relative_path !~ '//'),
164
+ CHECK (relative_path !~ '/$'),
165
+ CHECK (octet_length(content) = size)
166
+ );
167
+
168
+ COMMENT ON TABLE semicolony.skill_release_files IS
169
+ 'Immutable bytes and provenance needed to reconstruct and verify a complete common-skill tree.';
170
+
171
+ CREATE TABLE IF NOT EXISTS semicolony.skill_release_activations (
172
+ activation_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
173
+ channel TEXT NOT NULL CHECK (NULLIF(btrim(channel), '') IS NOT NULL),
174
+ skill_name TEXT NOT NULL
175
+ CHECK (skill_name ~ '^[a-z0-9]+(?:-[a-z0-9]+)*$'),
176
+ release_id UUID NOT NULL,
177
+ previous_release_id UUID,
178
+ audiences TEXT[] NOT NULL,
179
+ lease_id UUID NOT NULL
180
+ REFERENCES semicolony.skill_capability_leases (lease_id),
181
+ approval_reference TEXT NOT NULL,
182
+ idempotency_key TEXT NOT NULL UNIQUE,
183
+ activated_by_principal TEXT NOT NULL
184
+ CHECK (NULLIF(btrim(activated_by_principal), '') IS NOT NULL),
185
+ activated_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
186
+ FOREIGN KEY (release_id, channel, skill_name)
187
+ REFERENCES semicolony.skill_releases (release_id, channel, skill_name),
188
+ FOREIGN KEY (previous_release_id, channel, skill_name)
189
+ REFERENCES semicolony.skill_releases (release_id, channel, skill_name),
190
+ CHECK (NULLIF(btrim(approval_reference), '') IS NOT NULL),
191
+ CHECK (NULLIF(btrim(idempotency_key), '') IS NOT NULL),
192
+ CHECK (cardinality(audiences) > 0),
193
+ CHECK (array_position(audiences, NULL) IS NULL),
194
+ CHECK (audiences <@ ARRAY['worker', 'semi', 'colony']::TEXT[]),
195
+ CHECK (previous_release_id IS NULL OR previous_release_id <> release_id)
196
+ );
197
+
198
+ COMMENT ON TABLE semicolony.skill_release_activations IS
199
+ 'Append-only activation ledger. Rollback appends a row that targets an earlier immutable release.';
200
+
201
+ CREATE INDEX IF NOT EXISTS idx_skill_release_activations_current
202
+ ON semicolony.skill_release_activations
203
+ (channel, skill_name, activated_at DESC, activation_id DESC);
204
+
205
+ CREATE OR REPLACE FUNCTION semicolony.validate_skill_release_activation()
206
+ RETURNS TRIGGER
207
+ LANGUAGE plpgsql
208
+ AS $$
209
+ DECLARE
210
+ staged_lease_id UUID;
211
+ staged_audiences TEXT[];
212
+ staged_file_count INTEGER;
213
+ staged_total_bytes BIGINT;
214
+ actual_file_count BIGINT;
215
+ actual_total_bytes BIGINT;
216
+ has_skill_md BOOLEAN;
217
+ current_release_id UUID;
218
+ BEGIN
219
+ PERFORM pg_advisory_xact_lock(
220
+ hashtextextended(NEW.channel || ':' || NEW.skill_name, 0)
221
+ );
222
+
223
+ SELECT
224
+ release.created_under_lease_id,
225
+ release.audiences,
226
+ release.file_count,
227
+ release.total_bytes
228
+ INTO
229
+ staged_lease_id,
230
+ staged_audiences,
231
+ staged_file_count,
232
+ staged_total_bytes
233
+ FROM semicolony.skill_releases AS release
234
+ WHERE release.release_id = NEW.release_id
235
+ AND release.channel = NEW.channel
236
+ AND release.skill_name = NEW.skill_name;
237
+
238
+ IF staged_lease_id IS NULL THEN
239
+ RAISE EXCEPTION 'activation_release_identity_mismatch'
240
+ USING ERRCODE = '23503';
241
+ END IF;
242
+
243
+ IF NEW.lease_id = staged_lease_id THEN
244
+ RAISE EXCEPTION 'activation_lease_must_differ_from_stage_lease'
245
+ USING ERRCODE = '23514';
246
+ END IF;
247
+
248
+ IF NEW.audiences IS DISTINCT FROM staged_audiences THEN
249
+ RAISE EXCEPTION 'activation_audiences_must_match_release'
250
+ USING ERRCODE = '23514';
251
+ END IF;
252
+
253
+ SELECT
254
+ COUNT(*),
255
+ COALESCE(SUM(file.size), 0),
256
+ COALESCE(BOOL_OR(file.relative_path = 'SKILL.md'), false)
257
+ INTO
258
+ actual_file_count,
259
+ actual_total_bytes,
260
+ has_skill_md
261
+ FROM semicolony.skill_release_files AS file
262
+ WHERE file.release_id = NEW.release_id;
263
+
264
+ IF actual_file_count <> staged_file_count
265
+ OR actual_total_bytes <> staged_total_bytes
266
+ OR NOT has_skill_md
267
+ THEN
268
+ RAISE EXCEPTION 'activation_release_tree_is_not_sealed'
269
+ USING ERRCODE = '23514';
270
+ END IF;
271
+
272
+ SELECT activation.release_id
273
+ INTO current_release_id
274
+ FROM semicolony.skill_release_activations AS activation
275
+ WHERE activation.channel = NEW.channel
276
+ AND activation.skill_name = NEW.skill_name
277
+ ORDER BY activation.activated_at DESC, activation.activation_id DESC
278
+ LIMIT 1;
279
+
280
+ IF NEW.previous_release_id IS DISTINCT FROM current_release_id THEN
281
+ RAISE EXCEPTION 'activation_previous_release_must_match_current'
282
+ USING ERRCODE = '23514';
283
+ END IF;
284
+
285
+ RETURN NEW;
286
+ END;
287
+ $$;
288
+
289
+ DROP TRIGGER IF EXISTS validate_skill_release_activation
290
+ ON semicolony.skill_release_activations;
291
+ CREATE TRIGGER validate_skill_release_activation
292
+ BEFORE INSERT ON semicolony.skill_release_activations
293
+ FOR EACH ROW EXECUTE FUNCTION semicolony.validate_skill_release_activation();
294
+
295
+ CREATE TABLE IF NOT EXISTS semicolony.skill_change_proposals (
296
+ proposal_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
297
+ proposal_family_id UUID NOT NULL,
298
+ proposal_version INTEGER NOT NULL CHECK (proposal_version > 0),
299
+ previous_proposal_id UUID
300
+ REFERENCES semicolony.skill_change_proposals (proposal_id),
301
+ event_type TEXT NOT NULL
302
+ CHECK (event_type IN ('proposed', 'accepted', 'rejected')),
303
+ scope TEXT NOT NULL
304
+ CHECK (scope = 'semo-ops-common'),
305
+ base_channel TEXT NOT NULL DEFAULT 'semo-ops-common'
306
+ CHECK (base_channel = 'semo-ops-common'),
307
+ skill_name TEXT NOT NULL
308
+ CHECK (skill_name ~ '^[a-z0-9]+(?:-[a-z0-9]+)*$'),
309
+ base_release_id UUID NOT NULL
310
+ REFERENCES semicolony.skill_releases (release_id),
311
+ base_tree_sha256 TEXT NOT NULL
312
+ CHECK (base_tree_sha256 ~ '^[0-9a-f]{64}$'),
313
+ observed_tree_sha256 TEXT NOT NULL
314
+ CHECK (observed_tree_sha256 ~ '^[0-9a-f]{64}$'),
315
+ bundle_sha256 TEXT NOT NULL
316
+ CHECK (bundle_sha256 ~ '^[0-9a-f]{64}$'),
317
+ bundle BYTEA NOT NULL CHECK (octet_length(bundle) > 0),
318
+ harness TEXT NOT NULL
319
+ CHECK (harness IN ('codex', 'claude')),
320
+ observed_path TEXT NOT NULL
321
+ CHECK (NULLIF(btrim(observed_path), '') IS NOT NULL),
322
+ worker_id TEXT NOT NULL
323
+ CHECK (NULLIF(btrim(worker_id), '') IS NOT NULL),
324
+ device_id TEXT NOT NULL
325
+ CHECK (NULLIF(btrim(device_id), '') IS NOT NULL),
326
+ task_id TEXT NOT NULL
327
+ CHECK (NULLIF(btrim(task_id), '') IS NOT NULL),
328
+ principal_id TEXT NOT NULL
329
+ CHECK (NULLIF(btrim(principal_id), '') IS NOT NULL),
330
+ lease_id UUID NOT NULL
331
+ REFERENCES semicolony.skill_capability_leases (lease_id),
332
+ decision_reference TEXT,
333
+ recorded_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
334
+ UNIQUE (proposal_family_id, proposal_version),
335
+ FOREIGN KEY (base_release_id, base_channel, skill_name, base_tree_sha256)
336
+ REFERENCES semicolony.skill_releases
337
+ (release_id, channel, skill_name, tree_sha256),
338
+ CHECK (
339
+ (proposal_version = 1 AND event_type = 'proposed' AND previous_proposal_id IS NULL)
340
+ OR
341
+ (proposal_version > 1 AND event_type IN ('accepted', 'rejected') AND previous_proposal_id IS NOT NULL)
342
+ ),
343
+ CHECK (
344
+ event_type = 'proposed'
345
+ OR NULLIF(btrim(decision_reference), '') IS NOT NULL
346
+ )
347
+ );
348
+
349
+ COMMENT ON TABLE semicolony.skill_change_proposals IS
350
+ 'Immutable proposal event rows preserving managed projection drift before any authorized reconcile.';
351
+
352
+ CREATE TABLE IF NOT EXISTS semicolony.project_skill_releases (
353
+ project_release_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
354
+ project_id TEXT NOT NULL
355
+ CHECK (NULLIF(btrim(project_id), '') IS NOT NULL),
356
+ source_repository TEXT NOT NULL
357
+ CHECK (NULLIF(btrim(source_repository), '') IS NOT NULL),
358
+ skill_name TEXT NOT NULL
359
+ CHECK (skill_name ~ '^[a-z0-9]+(?:-[a-z0-9]+)*$'),
360
+ git_commit_sha TEXT NOT NULL
361
+ CHECK (git_commit_sha ~ '^[0-9a-f]{40}$'),
362
+ source_tree_sha256 TEXT NOT NULL
363
+ CHECK (source_tree_sha256 ~ '^[0-9a-f]{64}$'),
364
+ agents_tree_sha256 TEXT NOT NULL
365
+ CHECK (agents_tree_sha256 ~ '^[0-9a-f]{64}$'),
366
+ claude_tree_sha256 TEXT NOT NULL
367
+ CHECK (claude_tree_sha256 ~ '^[0-9a-f]{64}$'),
368
+ generator_contract TEXT NOT NULL
369
+ CHECK (NULLIF(btrim(generator_contract), '') IS NOT NULL),
370
+ approval_state TEXT NOT NULL DEFAULT 'approved'
371
+ CHECK (approval_state = 'approved'),
372
+ approval_reference TEXT NOT NULL,
373
+ lease_id UUID NOT NULL
374
+ REFERENCES semicolony.skill_capability_leases (lease_id),
375
+ recorded_by_principal TEXT NOT NULL
376
+ CHECK (NULLIF(btrim(recorded_by_principal), '') IS NOT NULL),
377
+ recorded_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
378
+ UNIQUE (project_id, skill_name, git_commit_sha, source_tree_sha256),
379
+ CHECK (NULLIF(btrim(approval_reference), '') IS NOT NULL),
380
+ CHECK (agents_tree_sha256 = source_tree_sha256),
381
+ CHECK (claude_tree_sha256 = source_tree_sha256)
382
+ );
383
+
384
+ COMMENT ON TABLE semicolony.project_skill_releases IS
385
+ 'Immutable project skill release approval audit. Project Git is content SoT; no project skill body is stored here.';
386
+
387
+ CREATE OR REPLACE VIEW semicolony.v_current_skill_capability_leases AS
388
+ WITH latest AS (
389
+ SELECT DISTINCT ON (lease_family_id)
390
+ lease.*
391
+ FROM semicolony.skill_capability_leases AS lease
392
+ ORDER BY lease_family_id, lease_version DESC
393
+ )
394
+ SELECT
395
+ latest.*,
396
+ COALESCE(
397
+ revocation.revoked_through_version >= latest.lease_version,
398
+ false
399
+ ) AS is_revoked,
400
+ revocation.revoked_through_version,
401
+ revocation.revoked_at
402
+ FROM latest
403
+ LEFT JOIN LATERAL (
404
+ SELECT
405
+ MAX(revoked_through_version) AS revoked_through_version,
406
+ MAX(revoked_at) AS revoked_at
407
+ FROM semicolony.skill_capability_lease_revocations
408
+ WHERE lease_family_id = latest.lease_family_id
409
+ ) AS revocation ON true;
410
+
411
+ CREATE OR REPLACE VIEW semicolony.v_current_skill_release_activations AS
412
+ SELECT DISTINCT ON (channel, skill_name)
413
+ activation.*
414
+ FROM semicolony.skill_release_activations AS activation
415
+ ORDER BY channel, skill_name, activated_at DESC, activation_id DESC;
416
+
417
+ CREATE OR REPLACE FUNCTION semicolony.reject_skill_governance_mutation()
418
+ RETURNS TRIGGER
419
+ LANGUAGE plpgsql
420
+ AS $$
421
+ BEGIN
422
+ RAISE EXCEPTION 'skill_governance_rows_are_append_only'
423
+ USING ERRCODE = '55000';
424
+ RETURN NULL;
425
+ END;
426
+ $$;
427
+
428
+ DROP TRIGGER IF EXISTS protect_skill_releases ON semicolony.skill_releases;
429
+ CREATE TRIGGER protect_skill_releases
430
+ BEFORE UPDATE OR DELETE ON semicolony.skill_releases
431
+ FOR EACH ROW EXECUTE FUNCTION semicolony.reject_skill_governance_mutation();
432
+
433
+ DROP TRIGGER IF EXISTS protect_skill_release_files ON semicolony.skill_release_files;
434
+ CREATE TRIGGER protect_skill_release_files
435
+ BEFORE UPDATE OR DELETE ON semicolony.skill_release_files
436
+ FOR EACH ROW EXECUTE FUNCTION semicolony.reject_skill_governance_mutation();
437
+
438
+ DROP TRIGGER IF EXISTS protect_skill_release_activations ON semicolony.skill_release_activations;
439
+ CREATE TRIGGER protect_skill_release_activations
440
+ BEFORE UPDATE OR DELETE ON semicolony.skill_release_activations
441
+ FOR EACH ROW EXECUTE FUNCTION semicolony.reject_skill_governance_mutation();
442
+
443
+ DROP TRIGGER IF EXISTS protect_skill_capability_leases ON semicolony.skill_capability_leases;
444
+ CREATE TRIGGER protect_skill_capability_leases
445
+ BEFORE UPDATE OR DELETE ON semicolony.skill_capability_leases
446
+ FOR EACH ROW EXECUTE FUNCTION semicolony.reject_skill_governance_mutation();
447
+
448
+ DROP TRIGGER IF EXISTS protect_skill_capability_lease_revocations
449
+ ON semicolony.skill_capability_lease_revocations;
450
+ CREATE TRIGGER protect_skill_capability_lease_revocations
451
+ BEFORE UPDATE OR DELETE ON semicolony.skill_capability_lease_revocations
452
+ FOR EACH ROW EXECUTE FUNCTION semicolony.reject_skill_governance_mutation();
453
+
454
+ DROP TRIGGER IF EXISTS protect_skill_change_proposals ON semicolony.skill_change_proposals;
455
+ CREATE TRIGGER protect_skill_change_proposals
456
+ BEFORE UPDATE OR DELETE ON semicolony.skill_change_proposals
457
+ FOR EACH ROW EXECUTE FUNCTION semicolony.reject_skill_governance_mutation();
458
+
459
+ DROP TRIGGER IF EXISTS protect_project_skill_releases ON semicolony.project_skill_releases;
460
+ CREATE TRIGGER protect_project_skill_releases
461
+ BEFORE UPDATE OR DELETE ON semicolony.project_skill_releases
462
+ FOR EACH ROW EXECUTE FUNCTION semicolony.reject_skill_governance_mutation();
463
+
464
+ -- The central migration/owner role retains lease issuance and revocation
465
+ -- authority. kb-gateway runs as sc_app and receives only the append operations
466
+ -- its worker-only routes need.
467
+ GRANT USAGE ON SCHEMA semicolony TO sc_app;
468
+
469
+ REVOKE ALL ON TABLE
470
+ semicolony.skill_capability_leases,
471
+ semicolony.skill_capability_lease_revocations
472
+ FROM PUBLIC, sc_app;
473
+
474
+ REVOKE ALL ON TABLE
475
+ semicolony.skill_releases,
476
+ semicolony.skill_release_files,
477
+ semicolony.skill_release_activations,
478
+ semicolony.skill_change_proposals,
479
+ semicolony.project_skill_releases,
480
+ semicolony.v_current_skill_capability_leases,
481
+ semicolony.v_current_skill_release_activations
482
+ FROM PUBLIC, sc_app;
483
+
484
+ GRANT SELECT, INSERT ON TABLE
485
+ semicolony.skill_releases,
486
+ semicolony.skill_release_files,
487
+ semicolony.skill_release_activations
488
+ TO sc_app;
489
+
490
+ GRANT INSERT ON TABLE
491
+ semicolony.skill_change_proposals,
492
+ semicolony.project_skill_releases
493
+ TO sc_app;
494
+
495
+ GRANT SELECT (proposal_id, recorded_at) ON TABLE
496
+ semicolony.skill_change_proposals
497
+ TO sc_app;
498
+
499
+ GRANT SELECT (project_release_id, recorded_at) ON TABLE
500
+ semicolony.project_skill_releases
501
+ TO sc_app;
502
+
503
+ GRANT SELECT ON TABLE
504
+ semicolony.v_current_skill_capability_leases,
505
+ semicolony.v_current_skill_release_activations
506
+ TO sc_app;
507
+
508
+ -- Existing kb-gateway authentication and KB routes must also remain viable
509
+ -- after the service is moved from the schema-owner account to sc_app.
510
+ GRANT SELECT (
511
+ id,
512
+ tenant_id,
513
+ tenant_slug,
514
+ token_hash,
515
+ scopes,
516
+ status,
517
+ expires_at,
518
+ metadata
519
+ ) ON TABLE semicolony.gateway_credentials TO sc_app;
520
+ GRANT UPDATE (last_used_at) ON TABLE
521
+ semicolony.gateway_credentials
522
+ TO sc_app;
523
+ GRANT SELECT, INSERT ON TABLE semicolony.ontology TO sc_app;
524
+ GRANT SELECT ON TABLE semicolony.kb_type_schema TO sc_app;
525
+
526
+ REVOKE ALL ON FUNCTION semicolony.validate_skill_release_activation()
527
+ FROM PUBLIC, sc_app;
528
+ REVOKE ALL ON FUNCTION semicolony.reject_skill_governance_mutation()
529
+ FROM PUBLIC, sc_app;
@@ -0,0 +1,39 @@
1
+ -- 161_workspace_projection_grants.sql
2
+ -- Explicit project/capability grants for SEMO Ops worker gateway credentials.
3
+ --
4
+ -- Safety:
5
+ -- - Additive only.
6
+ -- - References the hashed gateway credential record; no plaintext token or
7
+ -- projected value is stored here.
8
+ -- - The migration runner supplies BEGIN/COMMIT.
9
+
10
+ CREATE TABLE IF NOT EXISTS semicolony.workspace_projection_grants (
11
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
12
+ gateway_credential_id UUID NOT NULL
13
+ REFERENCES semicolony.gateway_credentials(id) ON DELETE CASCADE,
14
+ project_id TEXT NOT NULL
15
+ CHECK (project_id ~ '^[a-z][a-z0-9-]{0,99}$'),
16
+ capability TEXT NOT NULL
17
+ CHECK (capability IN ('credentials-read', 'assets-read')),
18
+ environments TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
19
+ status TEXT NOT NULL DEFAULT 'active'
20
+ CHECK (status IN ('active', 'revoked')),
21
+ expires_at TIMESTAMPTZ,
22
+ metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
23
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
24
+ UNIQUE (gateway_credential_id, project_id, capability),
25
+ CHECK (environments <@ ARRAY['dev', 'e2e']::TEXT[]),
26
+ CHECK (
27
+ (capability = 'credentials-read' AND cardinality(environments) > 0)
28
+ OR
29
+ (capability = 'assets-read' AND cardinality(environments) = 0)
30
+ )
31
+ );
32
+
33
+ CREATE INDEX IF NOT EXISTS idx_workspace_projection_grants_lookup
34
+ ON semicolony.workspace_projection_grants
35
+ (gateway_credential_id, project_id, capability)
36
+ WHERE status = 'active';
37
+
38
+ COMMENT ON TABLE semicolony.workspace_projection_grants IS
39
+ 'Explicit project/capability grants for wck_ worker identities. Contains no projected values.';
@@ -0,0 +1,96 @@
1
+ -- @schema-retarget: off
2
+ -- Preserve KB history writes under the least-privileged Gateway role without
3
+ -- granting that role direct access to forge audit rows. The existing trigger
4
+ -- function remains owner-controlled and executes with a pinned search_path.
5
+
6
+ DO $$
7
+ BEGIN
8
+ IF to_regrole('sc_app') IS NULL THEN
9
+ RAISE EXCEPTION 'required role sc_app does not exist';
10
+ END IF;
11
+
12
+ IF to_regclass('semicolony.knowledge_base') IS NULL THEN
13
+ RAISE EXCEPTION 'required table semicolony.knowledge_base does not exist';
14
+ END IF;
15
+
16
+ IF to_regclass('semicolony.knowledge_base_history') IS NULL THEN
17
+ RAISE EXCEPTION 'required table semicolony.knowledge_base_history does not exist';
18
+ END IF;
19
+
20
+ IF to_regprocedure('semicolony.kb_history_trigger_fn()') IS NULL THEN
21
+ RAISE EXCEPTION 'required function semicolony.kb_history_trigger_fn() does not exist';
22
+ END IF;
23
+
24
+ IF NOT EXISTS (
25
+ SELECT 1
26
+ FROM pg_proc p
27
+ WHERE p.oid = to_regprocedure('semicolony.kb_history_trigger_fn()')
28
+ AND p.proowner = (SELECT oid FROM pg_roles WHERE rolname = current_user)
29
+ ) THEN
30
+ RAISE EXCEPTION 'kb_history_trigger_fn must be owned by the migration role';
31
+ END IF;
32
+
33
+ IF NOT EXISTS (
34
+ SELECT 1
35
+ FROM pg_trigger tg
36
+ WHERE tg.tgrelid = to_regclass('semicolony.knowledge_base')
37
+ AND tg.tgfoid = to_regprocedure('semicolony.kb_history_trigger_fn()')
38
+ AND tg.tgname = 'kb_history_trigger'
39
+ AND NOT tg.tgisinternal
40
+ AND tg.tgenabled IN ('O', 'A')
41
+ AND tg.tgtype & 1 = 1
42
+ AND tg.tgtype & 2 = 0
43
+ AND tg.tgtype & 4 = 4
44
+ AND tg.tgtype & 8 = 8
45
+ AND tg.tgtype & 16 = 16
46
+ ) THEN
47
+ RAISE EXCEPTION 'expected enabled AFTER ROW KB history trigger is missing or drifted';
48
+ END IF;
49
+ END $$;
50
+
51
+ CREATE OR REPLACE FUNCTION semicolony.kb_history_trigger_fn()
52
+ RETURNS trigger
53
+ LANGUAGE plpgsql
54
+ SECURITY DEFINER
55
+ SET search_path = pg_catalog, semicolony
56
+ AS $$
57
+ BEGIN
58
+ IF TG_OP = 'INSERT' THEN
59
+ INSERT INTO semicolony.knowledge_base_history(
60
+ kb_id,
61
+ operation,
62
+ changed_by,
63
+ kb_snapshot
64
+ )
65
+ VALUES (NEW.kb_id, 'insert', NEW.created_by, to_jsonb(NEW));
66
+ RETURN NEW;
67
+ ELSIF TG_OP = 'UPDATE' THEN
68
+ INSERT INTO semicolony.knowledge_base_history(
69
+ kb_id,
70
+ operation,
71
+ changed_by,
72
+ kb_snapshot
73
+ )
74
+ VALUES (
75
+ OLD.kb_id,
76
+ 'update',
77
+ COALESCE(NEW.created_by, OLD.created_by),
78
+ to_jsonb(OLD)
79
+ );
80
+ RETURN NEW;
81
+ ELSIF TG_OP = 'DELETE' THEN
82
+ INSERT INTO semicolony.knowledge_base_history(
83
+ kb_id,
84
+ operation,
85
+ changed_by,
86
+ kb_snapshot
87
+ )
88
+ VALUES (OLD.kb_id, 'delete', OLD.created_by, to_jsonb(OLD));
89
+ RETURN OLD;
90
+ END IF;
91
+ RETURN NULL;
92
+ END;
93
+ $$;
94
+
95
+ REVOKE ALL ON TABLE semicolony.knowledge_base_history FROM PUBLIC, sc_app;
96
+ REVOKE ALL ON FUNCTION semicolony.kb_history_trigger_fn() FROM PUBLIC, sc_app;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semicolony-cli",
3
- "version": "4.18.51",
3
+ "version": "4.18.52",
4
4
  "description": "SemiColony CLI - AI operations and agent orchestration installer",
5
5
  "main": "dist/bundle.js",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "build": "tsc --noEmit && node scripts/bundle.mjs && rm -rf dist/templates && cp -r src/templates dist/templates && xattr -cr dist/ 2>/dev/null || true",
13
13
  "build:tsc": "tsc --noEmit",
14
14
  "bundle": "node scripts/bundle.mjs",
15
+ "release:worker": "npm run build && node scripts/build-worker-release.mjs",
15
16
  "start": "node dist/bundle.js",
16
17
  "dev": "ts-node src/index.ts",
17
18
  "lint": "eslint src/",
@@ -41,7 +42,8 @@
41
42
  "cron-parser": "^4.9.0",
42
43
  "inquirer": "^8.2.6",
43
44
  "ora": "^5.4.1",
44
- "smol-toml": "^1.6.1"
45
+ "smol-toml": "^1.6.1",
46
+ "yaml": "2.9.0"
45
47
  },
46
48
  "optionalDependencies": {
47
49
  "@team-semicolon/semicolony-common": "*",