@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.
- package/dist/bundle.js +1524 -1046
- package/migrations/149_hive_cron_capability_templates.sql +44 -0
- package/migrations/150_hive_cron_action_contract.sql +38 -0
- package/migrations/151_hive_cron_occurrence_leases.sql +150 -0
- package/migrations/152_hive_cron_relay_outbox.sql +243 -0
- package/migrations/153_hive_cron_evidence_uniqueness.sql +9 -0
- package/migrations/154_hive_cron_evidence_provider_grants.sql +816 -0
- package/migrations/155_hive_cron_occurrence_lease_provider_grants.sql +44 -0
- package/migrations/156_hive_cron_relay_outbox_provider_grants.sql +47 -0
- package/migrations/157_hive_cron_trusted_config_revisions.sql +243 -0
- package/migrations/158_hive_cron_typed_dispatch_outbox.sql +1406 -0
- package/migrations/159_hive_cron_shadow_cursor.sql +600 -0
- package/migrations/160_hive_cron_failure_alert_outbox.sql +406 -0
- package/migrations/161_skill_governance.sql +529 -0
- package/migrations/161_workspace_projection_grants.sql +39 -0
- package/migrations/162_sc_app_kb_history_trigger_hardening.sql +96 -0
- package/package.json +4 -2
|
@@ -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.
|
|
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": "*",
|