@team-semicolon/semicolony-cli 4.18.80 → 4.18.82
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,468 @@
|
|
|
1
|
+
-- 186_worker_agent_decision_capture.sql
|
|
2
|
+
--
|
|
3
|
+
-- Add one constrained decision capability for the two fixed Hermes actors.
|
|
4
|
+
-- An operator-owned authorization fixes actor, target, content digest, and
|
|
5
|
+
-- authority. The Gateway role can execute the recorder but cannot inspect or
|
|
6
|
+
-- mutate the authorization ledger or either target table directly.
|
|
7
|
+
|
|
8
|
+
CREATE TABLE semicolony.worker_agent_decision_authorizations (
|
|
9
|
+
decision_ref uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
10
|
+
actor_id text NOT NULL CHECK (actor_id IN ('semi', 'colony')),
|
|
11
|
+
target_domain text NOT NULL,
|
|
12
|
+
target_slug text NOT NULL,
|
|
13
|
+
content_sha256 text NOT NULL CHECK (content_sha256 ~ '^[0-9a-f]{64}$'),
|
|
14
|
+
decided_by text NOT NULL,
|
|
15
|
+
authority_kind text NOT NULL DEFAULT 'explicit-human-decision'
|
|
16
|
+
CHECK (authority_kind = 'explicit-human-decision'),
|
|
17
|
+
authority_reference text NOT NULL,
|
|
18
|
+
issued_by text NOT NULL,
|
|
19
|
+
issued_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
20
|
+
expires_at timestamptz NOT NULL,
|
|
21
|
+
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked')),
|
|
22
|
+
revoked_at timestamptz,
|
|
23
|
+
revoked_by text,
|
|
24
|
+
consumed_at timestamptz,
|
|
25
|
+
consumed_kb_id bigint,
|
|
26
|
+
CONSTRAINT worker_agent_decision_target_check CHECK (
|
|
27
|
+
target_domain ~ '^[a-z][a-z0-9-]{1,62}$'
|
|
28
|
+
AND target_domain <> 'semo'
|
|
29
|
+
AND target_domain !~ '^t-'
|
|
30
|
+
AND target_slug ~ '^[a-z0-9][a-z0-9-]{2,99}$'
|
|
31
|
+
),
|
|
32
|
+
CONSTRAINT worker_agent_decision_authority_check CHECK (
|
|
33
|
+
decided_by = btrim(decided_by)
|
|
34
|
+
AND char_length(decided_by) BETWEEN 1 AND 128
|
|
35
|
+
AND decided_by !~ '[[:cntrl:]]'
|
|
36
|
+
AND authority_reference = btrim(authority_reference)
|
|
37
|
+
AND char_length(authority_reference) BETWEEN 8 AND 500
|
|
38
|
+
AND authority_reference !~ '[[:cntrl:]]'
|
|
39
|
+
AND issued_by = btrim(issued_by)
|
|
40
|
+
AND char_length(issued_by) BETWEEN 1 AND 128
|
|
41
|
+
AND issued_by !~ '[[:cntrl:]]'
|
|
42
|
+
),
|
|
43
|
+
CONSTRAINT worker_agent_decision_revocation_check CHECK (
|
|
44
|
+
(status = 'active' AND revoked_at IS NULL AND revoked_by IS NULL)
|
|
45
|
+
OR (status = 'revoked' AND revoked_at IS NOT NULL AND revoked_by IS NOT NULL)
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE UNIQUE INDEX worker_agent_decision_authorizations_active_target
|
|
50
|
+
ON semicolony.worker_agent_decision_authorizations (actor_id, target_domain, target_slug)
|
|
51
|
+
WHERE status = 'active';
|
|
52
|
+
|
|
53
|
+
COMMENT ON TABLE semicolony.worker_agent_decision_authorizations IS
|
|
54
|
+
'Operator-issued exact authorization for actor-bound SEMO Ops decision recording; contains no bearer material.';
|
|
55
|
+
|
|
56
|
+
CREATE OR REPLACE FUNCTION semicolony.issue_worker_agent_decision_authorization(
|
|
57
|
+
p_actor_id text,
|
|
58
|
+
p_target_domain text,
|
|
59
|
+
p_target_slug text,
|
|
60
|
+
p_content_sha256 text,
|
|
61
|
+
p_decided_by text,
|
|
62
|
+
p_authority_kind text,
|
|
63
|
+
p_authority_reference text,
|
|
64
|
+
p_issued_by text,
|
|
65
|
+
p_expires_minutes integer
|
|
66
|
+
)
|
|
67
|
+
RETURNS uuid
|
|
68
|
+
LANGUAGE plpgsql
|
|
69
|
+
SECURITY DEFINER
|
|
70
|
+
SET search_path = pg_catalog
|
|
71
|
+
AS $function$
|
|
72
|
+
DECLARE
|
|
73
|
+
v_decision_ref uuid;
|
|
74
|
+
BEGIN
|
|
75
|
+
IF p_actor_id NOT IN ('semi', 'colony') THEN
|
|
76
|
+
RAISE EXCEPTION 'invalid Worker-agent actor' USING ERRCODE = '22023';
|
|
77
|
+
END IF;
|
|
78
|
+
IF p_target_domain IS NULL OR p_target_domain !~ '^[a-z][a-z0-9-]{1,62}$'
|
|
79
|
+
OR p_target_domain = 'semo' OR p_target_domain ~ '^t-' THEN
|
|
80
|
+
RAISE EXCEPTION 'invalid decision domain' USING ERRCODE = '22023';
|
|
81
|
+
END IF;
|
|
82
|
+
IF p_target_slug IS NULL OR p_target_slug !~ '^[a-z0-9][a-z0-9-]{2,99}$' THEN
|
|
83
|
+
RAISE EXCEPTION 'invalid decision slug' USING ERRCODE = '22023';
|
|
84
|
+
END IF;
|
|
85
|
+
IF p_content_sha256 IS NULL OR p_content_sha256 !~ '^[0-9a-f]{64}$' THEN
|
|
86
|
+
RAISE EXCEPTION 'invalid decision content digest' USING ERRCODE = '22023';
|
|
87
|
+
END IF;
|
|
88
|
+
IF p_authority_kind IS DISTINCT FROM 'explicit-human-decision' THEN
|
|
89
|
+
RAISE EXCEPTION 'decision authority must be explicit' USING ERRCODE = '22023';
|
|
90
|
+
END IF;
|
|
91
|
+
IF p_decided_by IS NULL OR p_decided_by <> btrim(p_decided_by)
|
|
92
|
+
OR char_length(p_decided_by) < 1 OR char_length(p_decided_by) > 128
|
|
93
|
+
OR p_decided_by ~ '[[:cntrl:]]' THEN
|
|
94
|
+
RAISE EXCEPTION 'invalid decision authority' USING ERRCODE = '22023';
|
|
95
|
+
END IF;
|
|
96
|
+
IF p_authority_reference IS NULL OR p_authority_reference <> btrim(p_authority_reference)
|
|
97
|
+
OR char_length(p_authority_reference) < 8 OR char_length(p_authority_reference) > 500
|
|
98
|
+
OR p_authority_reference ~ '[[:cntrl:]]' THEN
|
|
99
|
+
RAISE EXCEPTION 'invalid authority reference' USING ERRCODE = '22023';
|
|
100
|
+
END IF;
|
|
101
|
+
IF p_issued_by IS NULL OR p_issued_by <> btrim(p_issued_by)
|
|
102
|
+
OR char_length(p_issued_by) < 1 OR char_length(p_issued_by) > 128
|
|
103
|
+
OR p_issued_by ~ '[[:cntrl:]]' THEN
|
|
104
|
+
RAISE EXCEPTION 'invalid authorization issuer' USING ERRCODE = '22023';
|
|
105
|
+
END IF;
|
|
106
|
+
IF p_expires_minutes IS NULL OR p_expires_minutes < 1 OR p_expires_minutes > 1440 THEN
|
|
107
|
+
RAISE EXCEPTION 'decision authorization expiry must be between 1 and 1440 minutes'
|
|
108
|
+
USING ERRCODE = '22023';
|
|
109
|
+
END IF;
|
|
110
|
+
|
|
111
|
+
PERFORM wd.device_id
|
|
112
|
+
FROM semicolony.worker_devices AS wd
|
|
113
|
+
WHERE wd.device_id = 'hermes-principals@hermes'
|
|
114
|
+
AND wd.installation_kind = 'agent-runtime'
|
|
115
|
+
AND wd.installed_profile_id = 'semo-ops-worker-agent'
|
|
116
|
+
AND wd.kb_access_mode = 'worker-gateway'
|
|
117
|
+
AND wd.status = 'active';
|
|
118
|
+
IF NOT FOUND THEN
|
|
119
|
+
RAISE EXCEPTION 'active Worker-agent installation not found' USING ERRCODE = 'P0002';
|
|
120
|
+
END IF;
|
|
121
|
+
|
|
122
|
+
INSERT INTO semicolony.worker_agent_decision_authorizations (
|
|
123
|
+
actor_id, target_domain, target_slug, content_sha256, decided_by,
|
|
124
|
+
authority_kind, authority_reference, issued_by, expires_at
|
|
125
|
+
) VALUES (
|
|
126
|
+
p_actor_id, p_target_domain, p_target_slug, p_content_sha256, p_decided_by,
|
|
127
|
+
p_authority_kind, p_authority_reference, p_issued_by,
|
|
128
|
+
clock_timestamp() + make_interval(mins => p_expires_minutes)
|
|
129
|
+
)
|
|
130
|
+
RETURNING decision_ref INTO v_decision_ref;
|
|
131
|
+
|
|
132
|
+
RETURN v_decision_ref;
|
|
133
|
+
END;
|
|
134
|
+
$function$;
|
|
135
|
+
|
|
136
|
+
CREATE OR REPLACE FUNCTION semicolony.revoke_worker_agent_decision_authorization(
|
|
137
|
+
p_decision_ref uuid,
|
|
138
|
+
p_revoked_by text
|
|
139
|
+
)
|
|
140
|
+
RETURNS boolean
|
|
141
|
+
LANGUAGE plpgsql
|
|
142
|
+
SECURITY DEFINER
|
|
143
|
+
SET search_path = pg_catalog
|
|
144
|
+
AS $function$
|
|
145
|
+
BEGIN
|
|
146
|
+
IF p_revoked_by IS NULL OR p_revoked_by <> btrim(p_revoked_by)
|
|
147
|
+
OR char_length(p_revoked_by) < 1 OR char_length(p_revoked_by) > 128
|
|
148
|
+
OR p_revoked_by ~ '[[:cntrl:]]' THEN
|
|
149
|
+
RAISE EXCEPTION 'invalid revocation authority' USING ERRCODE = '22023';
|
|
150
|
+
END IF;
|
|
151
|
+
UPDATE semicolony.worker_agent_decision_authorizations
|
|
152
|
+
SET status = 'revoked', revoked_at = clock_timestamp(), revoked_by = p_revoked_by
|
|
153
|
+
WHERE decision_ref = p_decision_ref AND status = 'active';
|
|
154
|
+
RETURN FOUND;
|
|
155
|
+
END;
|
|
156
|
+
$function$;
|
|
157
|
+
|
|
158
|
+
CREATE OR REPLACE FUNCTION semicolony.record_worker_agent_decision(
|
|
159
|
+
p_credential_id uuid,
|
|
160
|
+
p_decision_ref uuid,
|
|
161
|
+
p_domain text,
|
|
162
|
+
p_slug text,
|
|
163
|
+
p_content text,
|
|
164
|
+
p_embedding text
|
|
165
|
+
)
|
|
166
|
+
RETURNS TABLE(
|
|
167
|
+
kb_id bigint,
|
|
168
|
+
duplicate boolean,
|
|
169
|
+
actor_id text,
|
|
170
|
+
decided_by text,
|
|
171
|
+
decision_ref uuid
|
|
172
|
+
)
|
|
173
|
+
LANGUAGE plpgsql
|
|
174
|
+
SECURITY DEFINER
|
|
175
|
+
SET search_path = pg_catalog
|
|
176
|
+
AS $function$
|
|
177
|
+
DECLARE
|
|
178
|
+
v_actor_id text;
|
|
179
|
+
v_runtime_profile text;
|
|
180
|
+
v_authorization semicolony.worker_agent_decision_authorizations%ROWTYPE;
|
|
181
|
+
v_content_sha256 text;
|
|
182
|
+
v_kb_id bigint;
|
|
183
|
+
v_existing_ref text;
|
|
184
|
+
v_existing_content text;
|
|
185
|
+
v_duplicate boolean := false;
|
|
186
|
+
BEGIN
|
|
187
|
+
IF p_credential_id IS NULL OR p_decision_ref IS NULL THEN
|
|
188
|
+
RAISE EXCEPTION 'trusted credential and decision reference are required'
|
|
189
|
+
USING ERRCODE = '22023';
|
|
190
|
+
END IF;
|
|
191
|
+
IF p_domain IS NULL OR p_domain !~ '^[a-z][a-z0-9-]{1,62}$'
|
|
192
|
+
OR p_domain = 'semo' OR p_domain ~ '^t-' THEN
|
|
193
|
+
RAISE EXCEPTION 'invalid decision domain' USING ERRCODE = '22023';
|
|
194
|
+
END IF;
|
|
195
|
+
IF p_slug IS NULL OR p_slug !~ '^[a-z0-9][a-z0-9-]{2,99}$' THEN
|
|
196
|
+
RAISE EXCEPTION 'invalid decision slug' USING ERRCODE = '22023';
|
|
197
|
+
END IF;
|
|
198
|
+
IF p_content IS NULL OR p_content <> btrim(p_content)
|
|
199
|
+
OR char_length(p_content) < 30 OR char_length(p_content) > 10000
|
|
200
|
+
OR translate(p_content, E'\n\t', '') ~ '[[:cntrl:]]'
|
|
201
|
+
OR p_content ~* '(DATABASE_URL|postgres://|SEMO_OPS_WORKER_KB_TOKEN|Authorization:[[:space:]]*Bearer|wck_[A-Za-z0-9_-]+)' THEN
|
|
202
|
+
RAISE EXCEPTION 'invalid decision content' USING ERRCODE = '22023';
|
|
203
|
+
END IF;
|
|
204
|
+
PERFORM p_embedding::public.vector;
|
|
205
|
+
v_content_sha256 := encode(public.digest(convert_to(p_content, 'UTF8'), 'sha256'), 'hex');
|
|
206
|
+
|
|
207
|
+
SELECT binding.actor_id, binding.runtime_profile
|
|
208
|
+
INTO v_actor_id, v_runtime_profile
|
|
209
|
+
FROM semicolony.worker_agent_actor_bindings AS binding
|
|
210
|
+
JOIN semicolony.gateway_credentials AS gc
|
|
211
|
+
ON gc.id = binding.gateway_credential_id
|
|
212
|
+
WHERE binding.gateway_credential_id = p_credential_id
|
|
213
|
+
AND binding.installation_id = 'hermes-principals@hermes'
|
|
214
|
+
AND binding.profile_id = 'semo-ops-worker-agent'
|
|
215
|
+
AND binding.runtime_kind = 'hermes-cli'
|
|
216
|
+
AND binding.actor_id IN ('semi', 'colony')
|
|
217
|
+
AND binding.status = 'active'
|
|
218
|
+
AND gc.status = 'active'
|
|
219
|
+
AND coalesce(gc.expires_at, 'infinity'::timestamptz) > clock_timestamp()
|
|
220
|
+
AND gc.metadata ->> 'credential_kind' = 'worker-agent-actor'
|
|
221
|
+
AND gc.metadata ->> 'actor_id' = binding.actor_id
|
|
222
|
+
AND gc.metadata ->> 'installation_id' = binding.installation_id
|
|
223
|
+
AND gc.metadata ->> 'profile_id' = binding.profile_id
|
|
224
|
+
AND gc.metadata ->> 'runtime_kind' = binding.runtime_kind
|
|
225
|
+
AND gc.metadata ->> 'runtime_profile' = binding.runtime_profile
|
|
226
|
+
AND gc.scopes = ARRAY['kb:read', 'feedback:write', 'decision:write']::text[]
|
|
227
|
+
FOR UPDATE OF binding, gc;
|
|
228
|
+
IF NOT FOUND THEN
|
|
229
|
+
RAISE EXCEPTION 'active decision credential binding not found' USING ERRCODE = '42501';
|
|
230
|
+
END IF;
|
|
231
|
+
|
|
232
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('worker-agent-decision:' || p_decision_ref::text, 0));
|
|
233
|
+
|
|
234
|
+
SELECT auth.*
|
|
235
|
+
INTO v_authorization
|
|
236
|
+
FROM semicolony.worker_agent_decision_authorizations AS auth
|
|
237
|
+
WHERE auth.decision_ref = p_decision_ref
|
|
238
|
+
FOR UPDATE;
|
|
239
|
+
IF NOT FOUND THEN
|
|
240
|
+
RAISE EXCEPTION 'decision authorization not found' USING ERRCODE = 'P0002';
|
|
241
|
+
END IF;
|
|
242
|
+
IF v_authorization.status <> 'active'
|
|
243
|
+
OR v_authorization.expires_at <= clock_timestamp()
|
|
244
|
+
OR v_authorization.authority_kind <> 'explicit-human-decision'
|
|
245
|
+
OR v_authorization.actor_id <> v_actor_id
|
|
246
|
+
OR v_authorization.target_domain <> p_domain
|
|
247
|
+
OR v_authorization.target_slug <> p_slug
|
|
248
|
+
OR v_authorization.content_sha256 <> v_content_sha256 THEN
|
|
249
|
+
RAISE EXCEPTION 'decision authorization mismatch' USING ERRCODE = '42501';
|
|
250
|
+
END IF;
|
|
251
|
+
|
|
252
|
+
SELECT kb.kb_id, kb.metadata ->> 'decision_ref', kb.content
|
|
253
|
+
INTO v_kb_id, v_existing_ref, v_existing_content
|
|
254
|
+
FROM semicolony.knowledge_base AS kb
|
|
255
|
+
WHERE kb.domain = p_domain AND kb.key = 'decision' AND kb.sub_key = p_slug
|
|
256
|
+
FOR UPDATE;
|
|
257
|
+
|
|
258
|
+
IF FOUND THEN
|
|
259
|
+
IF v_existing_ref <> p_decision_ref::text OR v_existing_content <> p_content THEN
|
|
260
|
+
RAISE EXCEPTION 'decision target already exists with different authority'
|
|
261
|
+
USING ERRCODE = '23505';
|
|
262
|
+
END IF;
|
|
263
|
+
v_duplicate := true;
|
|
264
|
+
ELSE
|
|
265
|
+
INSERT INTO semicolony.knowledge_base (
|
|
266
|
+
domain, key, sub_key, content, created_by, embedding, metadata
|
|
267
|
+
) VALUES (
|
|
268
|
+
p_domain,
|
|
269
|
+
'decision',
|
|
270
|
+
p_slug,
|
|
271
|
+
p_content,
|
|
272
|
+
'decision-pipeline:' || v_actor_id,
|
|
273
|
+
p_embedding::public.vector,
|
|
274
|
+
jsonb_build_object(
|
|
275
|
+
'source', 'runtime-native-tool',
|
|
276
|
+
'decision_ref', p_decision_ref::text,
|
|
277
|
+
'decided_by', v_authorization.decided_by,
|
|
278
|
+
'authority_kind', v_authorization.authority_kind,
|
|
279
|
+
'authority_reference', v_authorization.authority_reference,
|
|
280
|
+
'reported_by', v_actor_id,
|
|
281
|
+
'installation_id', 'hermes-principals@hermes',
|
|
282
|
+
'runtime_kind', 'hermes-cli',
|
|
283
|
+
'profile_id', 'semo-ops-worker-agent',
|
|
284
|
+
'runtime_profile', v_runtime_profile
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
RETURNING knowledge_base.kb_id INTO v_kb_id;
|
|
288
|
+
END IF;
|
|
289
|
+
|
|
290
|
+
UPDATE semicolony.worker_agent_decision_authorizations
|
|
291
|
+
SET consumed_at = coalesce(consumed_at, clock_timestamp()),
|
|
292
|
+
consumed_kb_id = v_kb_id
|
|
293
|
+
WHERE worker_agent_decision_authorizations.decision_ref = p_decision_ref;
|
|
294
|
+
|
|
295
|
+
RETURN QUERY SELECT v_kb_id, v_duplicate, v_actor_id,
|
|
296
|
+
v_authorization.decided_by, p_decision_ref;
|
|
297
|
+
END;
|
|
298
|
+
$function$;
|
|
299
|
+
|
|
300
|
+
REVOKE ALL ON TABLE semicolony.worker_agent_decision_authorizations
|
|
301
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
302
|
+
|
|
303
|
+
REVOKE ALL ON FUNCTION semicolony.issue_worker_agent_decision_authorization(
|
|
304
|
+
text, text, text, text, text, text, text, text, integer
|
|
305
|
+
) FROM PUBLIC, sc_app, sc_provider;
|
|
306
|
+
GRANT EXECUTE ON FUNCTION semicolony.issue_worker_agent_decision_authorization(
|
|
307
|
+
text, text, text, text, text, text, text, text, integer
|
|
308
|
+
) TO app;
|
|
309
|
+
|
|
310
|
+
REVOKE ALL ON FUNCTION semicolony.revoke_worker_agent_decision_authorization(uuid, text)
|
|
311
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
312
|
+
GRANT EXECUTE ON FUNCTION semicolony.revoke_worker_agent_decision_authorization(uuid, text)
|
|
313
|
+
TO app;
|
|
314
|
+
|
|
315
|
+
REVOKE ALL ON FUNCTION semicolony.record_worker_agent_decision(
|
|
316
|
+
uuid, uuid, text, text, text, text
|
|
317
|
+
) FROM PUBLIC, app, sc_provider;
|
|
318
|
+
GRANT EXECUTE ON FUNCTION semicolony.record_worker_agent_decision(
|
|
319
|
+
uuid, uuid, text, text, text, text
|
|
320
|
+
) TO sc_app;
|
|
321
|
+
|
|
322
|
+
CREATE OR REPLACE FUNCTION semicolony.issue_worker_agent_actor_credential(
|
|
323
|
+
p_team_tenant_id uuid,
|
|
324
|
+
p_actor_id text,
|
|
325
|
+
p_token_hash text,
|
|
326
|
+
p_token_prefix text,
|
|
327
|
+
p_issued_by text,
|
|
328
|
+
p_expires_days integer
|
|
329
|
+
)
|
|
330
|
+
RETURNS uuid
|
|
331
|
+
LANGUAGE plpgsql
|
|
332
|
+
SECURITY DEFINER
|
|
333
|
+
SET search_path = pg_catalog
|
|
334
|
+
AS $function$
|
|
335
|
+
DECLARE
|
|
336
|
+
v_credential_id uuid;
|
|
337
|
+
v_previous_credential_id uuid;
|
|
338
|
+
v_runtime_profile text;
|
|
339
|
+
BEGIN
|
|
340
|
+
IF p_actor_id NOT IN ('semi', 'colony') THEN
|
|
341
|
+
RAISE EXCEPTION 'invalid Worker-agent actor' USING ERRCODE = '22023';
|
|
342
|
+
END IF;
|
|
343
|
+
v_runtime_profile := CASE p_actor_id WHEN 'semi' THEN 'semo-semi' ELSE 'semo-colony' END;
|
|
344
|
+
IF p_token_hash IS NULL OR p_token_hash !~ '^[0-9a-f]{64}$' THEN
|
|
345
|
+
RAISE EXCEPTION 'invalid worker-agent actor credential token hash' USING ERRCODE = '22023';
|
|
346
|
+
END IF;
|
|
347
|
+
IF p_token_prefix IS NULL
|
|
348
|
+
OR p_token_prefix !~ ('^wck_' || p_actor_id || '_[A-Za-z0-9_-]{6}$') THEN
|
|
349
|
+
RAISE EXCEPTION 'invalid worker-agent actor credential token prefix' USING ERRCODE = '22023';
|
|
350
|
+
END IF;
|
|
351
|
+
IF p_issued_by IS NULL OR p_issued_by <> btrim(p_issued_by)
|
|
352
|
+
OR btrim(p_issued_by) = '' OR char_length(p_issued_by) > 128
|
|
353
|
+
OR p_issued_by ~ '[[:cntrl:]]' THEN
|
|
354
|
+
RAISE EXCEPTION 'invalid credential issuer' USING ERRCODE = '22023';
|
|
355
|
+
END IF;
|
|
356
|
+
IF p_expires_days IS NULL OR p_expires_days < 1 OR p_expires_days > 90 THEN
|
|
357
|
+
RAISE EXCEPTION 'worker-agent credential expiry must be between 1 and 90 days'
|
|
358
|
+
USING ERRCODE = '22023';
|
|
359
|
+
END IF;
|
|
360
|
+
|
|
361
|
+
PERFORM t.id
|
|
362
|
+
FROM public.tenants AS t
|
|
363
|
+
WHERE t.id = p_team_tenant_id
|
|
364
|
+
AND t.slug = 'team-semicolon'
|
|
365
|
+
AND t.tenant_type = 'team'
|
|
366
|
+
AND coalesce(t.metadata ->> 'status', 'active') = 'active';
|
|
367
|
+
IF NOT FOUND THEN
|
|
368
|
+
RAISE EXCEPTION 'active team-semicolon tenant not found' USING ERRCODE = 'P0002';
|
|
369
|
+
END IF;
|
|
370
|
+
|
|
371
|
+
PERFORM wd.device_id
|
|
372
|
+
FROM semicolony.worker_devices AS wd
|
|
373
|
+
WHERE wd.device_id = 'hermes-principals@hermes'
|
|
374
|
+
AND wd.installation_kind = 'agent-runtime'
|
|
375
|
+
AND wd.installed_profile_id = 'semo-ops-worker-agent'
|
|
376
|
+
AND wd.kb_access_mode = 'worker-gateway'
|
|
377
|
+
AND wd.status = 'active'
|
|
378
|
+
FOR UPDATE;
|
|
379
|
+
IF NOT FOUND THEN
|
|
380
|
+
RAISE EXCEPTION 'active Worker-agent installation not found' USING ERRCODE = 'P0002';
|
|
381
|
+
END IF;
|
|
382
|
+
|
|
383
|
+
SELECT binding.gateway_credential_id
|
|
384
|
+
INTO v_previous_credential_id
|
|
385
|
+
FROM semicolony.worker_agent_actor_bindings AS binding
|
|
386
|
+
WHERE binding.installation_id = 'hermes-principals@hermes'
|
|
387
|
+
AND binding.actor_id = p_actor_id
|
|
388
|
+
FOR UPDATE;
|
|
389
|
+
|
|
390
|
+
INSERT INTO semicolony.gateway_credentials (
|
|
391
|
+
tenant_id, tenant_slug, token_hash, token_prefix, scopes, issued_by, expires_at, metadata
|
|
392
|
+
) VALUES (
|
|
393
|
+
p_team_tenant_id,
|
|
394
|
+
'team-semicolon',
|
|
395
|
+
p_token_hash,
|
|
396
|
+
p_token_prefix,
|
|
397
|
+
ARRAY['kb:read', 'feedback:write', 'decision:write']::text[],
|
|
398
|
+
p_issued_by,
|
|
399
|
+
clock_timestamp() + make_interval(days => p_expires_days),
|
|
400
|
+
jsonb_build_object(
|
|
401
|
+
'credential_kind', 'worker-agent-actor',
|
|
402
|
+
'actor_id', p_actor_id,
|
|
403
|
+
'installation_id', 'hermes-principals@hermes',
|
|
404
|
+
'profile_id', 'semo-ops-worker-agent',
|
|
405
|
+
'runtime_kind', 'hermes-cli',
|
|
406
|
+
'runtime_profile', v_runtime_profile,
|
|
407
|
+
'tenant_anchor', 'team-semicolon',
|
|
408
|
+
'lineage_status', 'active'
|
|
409
|
+
)
|
|
410
|
+
)
|
|
411
|
+
RETURNING id INTO v_credential_id;
|
|
412
|
+
|
|
413
|
+
INSERT INTO semicolony.worker_agent_actor_bindings (
|
|
414
|
+
installation_id, actor_id, runtime_profile, profile_id, runtime_kind,
|
|
415
|
+
gateway_credential_id, status, issued_at, rotated_at, updated_at
|
|
416
|
+
) VALUES (
|
|
417
|
+
'hermes-principals@hermes', p_actor_id, v_runtime_profile,
|
|
418
|
+
'semo-ops-worker-agent', 'hermes-cli', v_credential_id, 'active',
|
|
419
|
+
clock_timestamp(), CASE WHEN v_previous_credential_id IS NULL THEN NULL ELSE clock_timestamp() END,
|
|
420
|
+
clock_timestamp()
|
|
421
|
+
)
|
|
422
|
+
ON CONFLICT (installation_id, actor_id) DO UPDATE
|
|
423
|
+
SET runtime_profile = EXCLUDED.runtime_profile,
|
|
424
|
+
profile_id = EXCLUDED.profile_id,
|
|
425
|
+
runtime_kind = EXCLUDED.runtime_kind,
|
|
426
|
+
gateway_credential_id = EXCLUDED.gateway_credential_id,
|
|
427
|
+
status = 'active',
|
|
428
|
+
rotated_at = clock_timestamp(),
|
|
429
|
+
updated_at = clock_timestamp();
|
|
430
|
+
|
|
431
|
+
IF v_previous_credential_id IS NOT NULL AND v_previous_credential_id <> v_credential_id THEN
|
|
432
|
+
UPDATE semicolony.gateway_credentials
|
|
433
|
+
SET status = 'revoked',
|
|
434
|
+
revoked_at = clock_timestamp(),
|
|
435
|
+
revoked_reason = 'worker-agent actor credential rotated'
|
|
436
|
+
WHERE id = v_previous_credential_id
|
|
437
|
+
AND status = 'active';
|
|
438
|
+
END IF;
|
|
439
|
+
|
|
440
|
+
RETURN v_credential_id;
|
|
441
|
+
END;
|
|
442
|
+
$function$;
|
|
443
|
+
|
|
444
|
+
REVOKE ALL ON FUNCTION semicolony.issue_worker_agent_actor_credential(
|
|
445
|
+
uuid, text, text, text, text, integer
|
|
446
|
+
) FROM PUBLIC, sc_provider;
|
|
447
|
+
GRANT EXECUTE ON FUNCTION semicolony.issue_worker_agent_actor_credential(
|
|
448
|
+
uuid, text, text, text, text, integer
|
|
449
|
+
) TO sc_app;
|
|
450
|
+
|
|
451
|
+
UPDATE semicolony.gateway_credentials AS gc
|
|
452
|
+
SET scopes = ARRAY['kb:read', 'feedback:write', 'decision:write']::text[]
|
|
453
|
+
FROM semicolony.worker_agent_actor_bindings AS binding
|
|
454
|
+
WHERE binding.gateway_credential_id = gc.id
|
|
455
|
+
AND binding.installation_id = 'hermes-principals@hermes'
|
|
456
|
+
AND binding.profile_id = 'semo-ops-worker-agent'
|
|
457
|
+
AND binding.runtime_kind = 'hermes-cli'
|
|
458
|
+
AND binding.actor_id IN ('semi', 'colony')
|
|
459
|
+
AND binding.status = 'active'
|
|
460
|
+
AND gc.status = 'active'
|
|
461
|
+
AND coalesce(gc.expires_at, 'infinity'::timestamptz) > clock_timestamp()
|
|
462
|
+
AND gc.metadata ->> 'credential_kind' = 'worker-agent-actor'
|
|
463
|
+
AND gc.metadata ->> 'actor_id' = binding.actor_id
|
|
464
|
+
AND gc.metadata ->> 'installation_id' = binding.installation_id
|
|
465
|
+
AND gc.metadata ->> 'profile_id' = binding.profile_id
|
|
466
|
+
AND gc.metadata ->> 'runtime_kind' = binding.runtime_kind
|
|
467
|
+
AND gc.metadata ->> 'runtime_profile' = binding.runtime_profile
|
|
468
|
+
AND gc.scopes IS DISTINCT FROM ARRAY['kb:read', 'feedback:write', 'decision:write']::text[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
-- 187_worker_agent_decision_digest_repair.sql
|
|
2
|
+
--
|
|
3
|
+
-- Migration 186 qualified pgcrypto.digest through public, but production keeps
|
|
4
|
+
-- pgcrypto in the isolated knowledge_fabric_crypto schema. Repair the already
|
|
5
|
+
-- installed recorder without depending on any extension schema. Fresh installs
|
|
6
|
+
-- already receive the pg_catalog SHA-256 primitive from the corrected 186 file.
|
|
7
|
+
|
|
8
|
+
DO $repair$
|
|
9
|
+
DECLARE
|
|
10
|
+
v_function regprocedure :=
|
|
11
|
+
'semicolony.record_worker_agent_decision(uuid,uuid,text,text,text,text)'::regprocedure;
|
|
12
|
+
v_definition text;
|
|
13
|
+
v_repaired text;
|
|
14
|
+
v_old constant text :=
|
|
15
|
+
'public.digest(convert_to(p_content, ''UTF8''), ''sha256'')';
|
|
16
|
+
v_new constant text :=
|
|
17
|
+
'pg_catalog.sha256(convert_to(p_content, ''UTF8''))';
|
|
18
|
+
v_old_occurrences integer;
|
|
19
|
+
BEGIN
|
|
20
|
+
SELECT pg_get_functiondef(v_function::oid) INTO STRICT v_definition;
|
|
21
|
+
v_old_occurrences :=
|
|
22
|
+
(char_length(v_definition) - char_length(replace(v_definition, v_old, '')))
|
|
23
|
+
/ char_length(v_old);
|
|
24
|
+
|
|
25
|
+
IF v_old_occurrences = 0 AND position(v_new IN v_definition) > 0 THEN
|
|
26
|
+
RETURN;
|
|
27
|
+
END IF;
|
|
28
|
+
IF v_old_occurrences <> 1 THEN
|
|
29
|
+
RAISE EXCEPTION 'unexpected Worker-agent decision recorder digest definition';
|
|
30
|
+
END IF;
|
|
31
|
+
|
|
32
|
+
v_repaired := replace(v_definition, v_old, v_new);
|
|
33
|
+
EXECUTE v_repaired;
|
|
34
|
+
END;
|
|
35
|
+
$repair$;
|
|
36
|
+
|
|
37
|
+
REVOKE ALL ON FUNCTION semicolony.record_worker_agent_decision(
|
|
38
|
+
uuid, uuid, text, text, text, text
|
|
39
|
+
) FROM PUBLIC, app, sc_provider;
|
|
40
|
+
GRANT EXECUTE ON FUNCTION semicolony.record_worker_agent_decision(
|
|
41
|
+
uuid, uuid, text, text, text, text
|
|
42
|
+
) TO sc_app;
|
package/migrations/README.md
CHANGED
|
@@ -24,6 +24,8 @@ SEMICOLONY CLI의 PostgreSQL 마이그레이션 파일들.
|
|
|
24
24
|
| `183_worker_agent_gateway_credentials.sql` | Hermes Agent host release credential과 Semi/Colony actor KB-read credential의 서버 바인딩 및 회전 |
|
|
25
25
|
| `184_worker_agent_workspace_profile.sql` | Hermes Agent immutable release가 참조하는 `semo-ops-worker-agent` DB 정본 프로필 시드 |
|
|
26
26
|
| `185_worker_agent_feedback_scope.sql` | Hermes Semi/Colony actor에 정확한 feedback 범위·귀속을 추가하고 기존 바인딩만 백필 |
|
|
27
|
+
| `186_worker_agent_decision_capture.sql` | operator 승인 reference와 actor-bound decision recorder를 추가하고 정확한 decision scope만 백필 |
|
|
28
|
+
| `187_worker_agent_decision_digest_repair.sql` | Phase 6 decision recorder를 extension schema와 무관한 PostgreSQL SHA-256 primitive로 교정 |
|
|
27
29
|
|
|
28
30
|
## 007: Flat Skill Naming
|
|
29
31
|
|