@team-semicolon/semicolony-cli 4.18.95 → 4.18.96
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 +1036 -1036
- package/migrations/194_worker_agent_note_capture.sql +222 -0
- package/package.json +1 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
-- 194_worker_agent_note_capture.sql
|
|
2
|
+
--
|
|
3
|
+
-- Give the two fixed Hermes actors one self-authorized, append-only fact lane.
|
|
4
|
+
--
|
|
5
|
+
-- Decision capture (186) requires an operator to pre-issue an authorization that
|
|
6
|
+
-- pins actor, target, content digest, and authority. That is the right contract
|
|
7
|
+
-- for a decision, but it leaves the actors with no way to record a plain fact
|
|
8
|
+
-- when a human asks them to. This migration adds that lane and bounds it
|
|
9
|
+
-- structurally instead of by pre-authorization: the recorder pins key='note' as
|
|
10
|
+
-- a literal, refuses to touch a row that already exists, requires a domain that
|
|
11
|
+
-- is already in the ontology, and enforces a rolling hourly quota per actor.
|
|
12
|
+
--
|
|
13
|
+
-- Blast radius is therefore one namespace. Curated knowledge (decision, process,
|
|
14
|
+
-- infra, ...) is unreachable from this function, and every row it writes carries
|
|
15
|
+
-- reported_by so the whole lane can be recalled with a single predicate.
|
|
16
|
+
--
|
|
17
|
+
-- The migration runner owns BEGIN/COMMIT.
|
|
18
|
+
|
|
19
|
+
-- ---------------------------------------------------------------------------
|
|
20
|
+
-- 1. Widen the actor credential scope contract from three scopes to four.
|
|
21
|
+
--
|
|
22
|
+
-- Both the decision recorder and the credential issuer compare gc.scopes by
|
|
23
|
+
-- exact array equality. Adding a scope without moving them in the same
|
|
24
|
+
-- transaction would break decision capture the moment a credential is rotated,
|
|
25
|
+
-- so the two are repaired here rather than in a follow-up.
|
|
26
|
+
--
|
|
27
|
+
-- The repair rewrites only the scope array inside the installed definition
|
|
28
|
+
-- (the 187 pattern). Copying the function bodies would silently revert 187's
|
|
29
|
+
-- pg_catalog.sha256 correction.
|
|
30
|
+
-- ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
DO $scope_widen$
|
|
33
|
+
DECLARE
|
|
34
|
+
v_targets constant text[] := ARRAY[
|
|
35
|
+
'semicolony.record_worker_agent_decision(uuid,uuid,text,text,text,text)',
|
|
36
|
+
'semicolony.issue_worker_agent_actor_credential(uuid,text,text,text,text,integer)'
|
|
37
|
+
];
|
|
38
|
+
v_old constant text :=
|
|
39
|
+
'ARRAY[''kb:read'', ''feedback:write'', ''decision:write'']::text[]';
|
|
40
|
+
v_new constant text :=
|
|
41
|
+
'ARRAY[''kb:read'', ''feedback:write'', ''decision:write'', ''note:write'']::text[]';
|
|
42
|
+
v_target text;
|
|
43
|
+
v_definition text;
|
|
44
|
+
v_old_occurrences integer;
|
|
45
|
+
BEGIN
|
|
46
|
+
FOREACH v_target IN ARRAY v_targets LOOP
|
|
47
|
+
SELECT pg_get_functiondef(v_target::regprocedure::oid) INTO STRICT v_definition;
|
|
48
|
+
|
|
49
|
+
v_old_occurrences :=
|
|
50
|
+
(char_length(v_definition) - char_length(replace(v_definition, v_old, '')))
|
|
51
|
+
/ char_length(v_old);
|
|
52
|
+
|
|
53
|
+
IF v_old_occurrences = 0 AND position(v_new IN v_definition) > 0 THEN
|
|
54
|
+
CONTINUE;
|
|
55
|
+
END IF;
|
|
56
|
+
IF v_old_occurrences <> 1 THEN
|
|
57
|
+
RAISE EXCEPTION 'unexpected Worker-agent actor scope definition in %', v_target;
|
|
58
|
+
END IF;
|
|
59
|
+
|
|
60
|
+
EXECUTE replace(v_definition, v_old, v_new);
|
|
61
|
+
END LOOP;
|
|
62
|
+
END;
|
|
63
|
+
$scope_widen$;
|
|
64
|
+
|
|
65
|
+
-- Existing active actor credentials keep working only if their stored scope
|
|
66
|
+
-- array moves with the contract above.
|
|
67
|
+
UPDATE semicolony.gateway_credentials AS gc
|
|
68
|
+
SET scopes = ARRAY['kb:read', 'feedback:write', 'decision:write', 'note:write']::text[]
|
|
69
|
+
FROM semicolony.worker_agent_actor_bindings AS binding
|
|
70
|
+
WHERE binding.gateway_credential_id = gc.id
|
|
71
|
+
AND binding.installation_id = 'hermes-principals@hermes'
|
|
72
|
+
AND binding.profile_id = 'semo-ops-worker-agent'
|
|
73
|
+
AND binding.runtime_kind = 'hermes-cli'
|
|
74
|
+
AND binding.actor_id IN ('semi', 'colony')
|
|
75
|
+
AND binding.status = 'active'
|
|
76
|
+
AND gc.status = 'active'
|
|
77
|
+
AND coalesce(gc.expires_at, 'infinity'::timestamptz) > clock_timestamp()
|
|
78
|
+
AND gc.metadata ->> 'credential_kind' = 'worker-agent-actor'
|
|
79
|
+
AND gc.metadata ->> 'actor_id' = binding.actor_id
|
|
80
|
+
AND gc.metadata ->> 'installation_id' = binding.installation_id
|
|
81
|
+
AND gc.metadata ->> 'profile_id' = binding.profile_id
|
|
82
|
+
AND gc.metadata ->> 'runtime_kind' = binding.runtime_kind
|
|
83
|
+
AND gc.metadata ->> 'runtime_profile' = binding.runtime_profile
|
|
84
|
+
AND gc.scopes
|
|
85
|
+
IS DISTINCT FROM
|
|
86
|
+
ARRAY['kb:read', 'feedback:write', 'decision:write', 'note:write']::text[];
|
|
87
|
+
|
|
88
|
+
-- ---------------------------------------------------------------------------
|
|
89
|
+
-- 2. The note recorder.
|
|
90
|
+
-- ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
CREATE OR REPLACE FUNCTION semicolony.record_worker_agent_note(
|
|
93
|
+
p_credential_id uuid,
|
|
94
|
+
p_domain text,
|
|
95
|
+
p_slug text,
|
|
96
|
+
p_content text,
|
|
97
|
+
p_embedding text
|
|
98
|
+
)
|
|
99
|
+
RETURNS TABLE(
|
|
100
|
+
kb_id bigint,
|
|
101
|
+
actor_id text,
|
|
102
|
+
quota_remaining integer
|
|
103
|
+
)
|
|
104
|
+
LANGUAGE plpgsql
|
|
105
|
+
SECURITY DEFINER
|
|
106
|
+
SET search_path = pg_catalog
|
|
107
|
+
AS $function$
|
|
108
|
+
DECLARE
|
|
109
|
+
-- The note lane never accepts a caller-supplied key. Anything outside this
|
|
110
|
+
-- namespace is unreachable from this function.
|
|
111
|
+
c_key constant text := 'note';
|
|
112
|
+
c_quota_per_hour constant integer := 20;
|
|
113
|
+
v_actor_id text;
|
|
114
|
+
v_runtime_profile text;
|
|
115
|
+
v_recent_notes integer;
|
|
116
|
+
v_kb_id bigint;
|
|
117
|
+
BEGIN
|
|
118
|
+
IF p_credential_id IS NULL THEN
|
|
119
|
+
RAISE EXCEPTION 'trusted credential is required' USING ERRCODE = '22023';
|
|
120
|
+
END IF;
|
|
121
|
+
IF p_domain IS NULL OR p_domain !~ '^[a-z][a-z0-9-]{1,62}$'
|
|
122
|
+
OR p_domain = 'semo' OR p_domain ~ '^t-' THEN
|
|
123
|
+
RAISE EXCEPTION 'invalid note domain' USING ERRCODE = '22023';
|
|
124
|
+
END IF;
|
|
125
|
+
IF p_slug IS NULL OR p_slug !~ '^[a-z0-9][a-z0-9-]{2,99}$' THEN
|
|
126
|
+
RAISE EXCEPTION 'invalid note slug' USING ERRCODE = '22023';
|
|
127
|
+
END IF;
|
|
128
|
+
IF p_content IS NULL OR p_content <> btrim(p_content)
|
|
129
|
+
OR char_length(p_content) < 30 OR char_length(p_content) > 10000
|
|
130
|
+
OR translate(p_content, E'\n\t', '') ~ '[[:cntrl:]]'
|
|
131
|
+
OR p_content ~* '(DATABASE_URL|postgres://|SEMO_OPS_WORKER_KB_TOKEN|Authorization:[[:space:]]*Bearer|wck_[A-Za-z0-9_-]+)' THEN
|
|
132
|
+
RAISE EXCEPTION 'invalid note content' USING ERRCODE = '22023';
|
|
133
|
+
END IF;
|
|
134
|
+
PERFORM p_embedding::public.vector;
|
|
135
|
+
|
|
136
|
+
SELECT binding.actor_id, binding.runtime_profile
|
|
137
|
+
INTO v_actor_id, v_runtime_profile
|
|
138
|
+
FROM semicolony.worker_agent_actor_bindings AS binding
|
|
139
|
+
JOIN semicolony.gateway_credentials AS gc
|
|
140
|
+
ON gc.id = binding.gateway_credential_id
|
|
141
|
+
WHERE binding.gateway_credential_id = p_credential_id
|
|
142
|
+
AND binding.installation_id = 'hermes-principals@hermes'
|
|
143
|
+
AND binding.profile_id = 'semo-ops-worker-agent'
|
|
144
|
+
AND binding.runtime_kind = 'hermes-cli'
|
|
145
|
+
AND binding.actor_id IN ('semi', 'colony')
|
|
146
|
+
AND binding.status = 'active'
|
|
147
|
+
AND gc.status = 'active'
|
|
148
|
+
AND coalesce(gc.expires_at, 'infinity'::timestamptz) > clock_timestamp()
|
|
149
|
+
AND gc.metadata ->> 'credential_kind' = 'worker-agent-actor'
|
|
150
|
+
AND gc.metadata ->> 'actor_id' = binding.actor_id
|
|
151
|
+
AND gc.metadata ->> 'installation_id' = binding.installation_id
|
|
152
|
+
AND gc.metadata ->> 'profile_id' = binding.profile_id
|
|
153
|
+
AND gc.metadata ->> 'runtime_kind' = binding.runtime_kind
|
|
154
|
+
AND gc.metadata ->> 'runtime_profile' = binding.runtime_profile
|
|
155
|
+
AND gc.scopes = ARRAY['kb:read', 'feedback:write', 'decision:write', 'note:write']::text[]
|
|
156
|
+
FOR UPDATE OF binding, gc;
|
|
157
|
+
IF NOT FOUND THEN
|
|
158
|
+
RAISE EXCEPTION 'active note credential binding not found' USING ERRCODE = '42501';
|
|
159
|
+
END IF;
|
|
160
|
+
|
|
161
|
+
-- A note may only attach to a domain the team already curates. A typo would
|
|
162
|
+
-- otherwise mint a new domain that no ontology consumer knows about.
|
|
163
|
+
PERFORM 1 FROM semicolony.ontology AS o WHERE o.domain = p_domain;
|
|
164
|
+
IF NOT FOUND THEN
|
|
165
|
+
RAISE EXCEPTION 'note domain is not registered in the ontology' USING ERRCODE = 'P0002';
|
|
166
|
+
END IF;
|
|
167
|
+
|
|
168
|
+
-- Serialize this actor so concurrent calls cannot both pass the quota check.
|
|
169
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('worker-agent-note:' || v_actor_id, 0));
|
|
170
|
+
|
|
171
|
+
SELECT count(*)::integer
|
|
172
|
+
INTO v_recent_notes
|
|
173
|
+
FROM semicolony.knowledge_base AS kb
|
|
174
|
+
WHERE kb.key = c_key
|
|
175
|
+
AND kb.metadata ->> 'reported_by' = v_actor_id
|
|
176
|
+
AND kb.metadata ->> 'note_kind' = 'actor-self-reported'
|
|
177
|
+
AND kb.created_at > clock_timestamp() - interval '1 hour';
|
|
178
|
+
IF v_recent_notes >= c_quota_per_hour THEN
|
|
179
|
+
RAISE EXCEPTION 'note quota exhausted for this actor' USING ERRCODE = '53400';
|
|
180
|
+
END IF;
|
|
181
|
+
|
|
182
|
+
-- Append-only: an existing coordinate is never rewritten, not even with
|
|
183
|
+
-- identical content. Corrections are new slugs.
|
|
184
|
+
PERFORM 1
|
|
185
|
+
FROM semicolony.knowledge_base AS kb
|
|
186
|
+
WHERE kb.domain = p_domain AND kb.key = c_key AND kb.sub_key = p_slug;
|
|
187
|
+
IF FOUND THEN
|
|
188
|
+
RAISE EXCEPTION 'note target already exists' USING ERRCODE = '23505';
|
|
189
|
+
END IF;
|
|
190
|
+
|
|
191
|
+
INSERT INTO semicolony.knowledge_base (
|
|
192
|
+
domain, key, sub_key, content, created_by, embedding, metadata
|
|
193
|
+
) VALUES (
|
|
194
|
+
p_domain,
|
|
195
|
+
c_key,
|
|
196
|
+
p_slug,
|
|
197
|
+
p_content,
|
|
198
|
+
'note-pipeline:' || v_actor_id,
|
|
199
|
+
p_embedding::public.vector,
|
|
200
|
+
jsonb_build_object(
|
|
201
|
+
'source', 'runtime-native-tool',
|
|
202
|
+
'note_kind', 'actor-self-reported',
|
|
203
|
+
'reported_by', v_actor_id,
|
|
204
|
+
'installation_id', 'hermes-principals@hermes',
|
|
205
|
+
'runtime_kind', 'hermes-cli',
|
|
206
|
+
'profile_id', 'semo-ops-worker-agent',
|
|
207
|
+
'runtime_profile', v_runtime_profile
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
RETURNING knowledge_base.kb_id INTO v_kb_id;
|
|
211
|
+
|
|
212
|
+
RETURN QUERY
|
|
213
|
+
SELECT v_kb_id, v_actor_id, c_quota_per_hour - v_recent_notes - 1;
|
|
214
|
+
END;
|
|
215
|
+
$function$;
|
|
216
|
+
|
|
217
|
+
REVOKE ALL ON FUNCTION semicolony.record_worker_agent_note(
|
|
218
|
+
uuid, text, text, text, text
|
|
219
|
+
) FROM PUBLIC, app, sc_provider;
|
|
220
|
+
GRANT EXECUTE ON FUNCTION semicolony.record_worker_agent_note(
|
|
221
|
+
uuid, text, text, text, text
|
|
222
|
+
) TO sc_app;
|