@team-semicolon/semicolony-cli 4.18.95 → 4.18.97
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,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;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- 195_worker_device_target_release.sql
|
|
2
|
+
--
|
|
3
|
+
-- Fix: /api/worker-fleet returns HTTP 500 for every role in production.
|
|
4
|
+
-- lib/worker-fleet/queries.ts DEVICE_ROWS_SQL selects
|
|
5
|
+
-- COALESCE(wd.active_release_id, p.active_release_id) AS target_release_id
|
|
6
|
+
-- but semo_ops.worker_devices has no active_release_id column — only
|
|
7
|
+
-- semo_ops.workspace_profiles does. The COALESCE already encodes the intended
|
|
8
|
+
-- contract (device-level override falls back to the profile default) and is
|
|
9
|
+
-- pinned by an existing contract test, so the fix is to add the missing
|
|
10
|
+
-- column, not to change the query.
|
|
11
|
+
--
|
|
12
|
+
-- Nullable is required: NULL means "no device-level override, use the
|
|
13
|
+
-- profile default", which is exactly what the COALESCE expects. No backfill.
|
|
14
|
+
--
|
|
15
|
+
-- Type/FK mirror semo_ops.workspace_profiles.active_release_id exactly
|
|
16
|
+
-- (uuid, nullable, FK -> semo_ops.worker_profile_releases(release_id)
|
|
17
|
+
-- ON DELETE SET NULL), verified against the live schema before writing this.
|
|
18
|
+
--
|
|
19
|
+
-- Grants: sc_fleet_board's existing SELECT on semo_ops.worker_devices is a
|
|
20
|
+
-- table-level grant (relacl 'sc_fleet_board=r'), which automatically covers
|
|
21
|
+
-- new columns — verified against pg_class.relacl before writing this. No
|
|
22
|
+
-- additional GRANT is needed for the fleet board read role.
|
|
23
|
+
--
|
|
24
|
+
-- Safety: additive only, single nullable column on one table.
|
|
25
|
+
-- The migration runner owns BEGIN/COMMIT; no transaction control here.
|
|
26
|
+
|
|
27
|
+
ALTER TABLE semo_ops.worker_devices
|
|
28
|
+
ADD COLUMN IF NOT EXISTS active_release_id UUID
|
|
29
|
+
REFERENCES semo_ops.worker_profile_releases(release_id) ON DELETE SET NULL;
|
|
30
|
+
|
|
31
|
+
COMMENT ON COLUMN semo_ops.worker_devices.active_release_id IS
|
|
32
|
+
'Device-level target release override. NULL means use the profile default (semo_ops.workspace_profiles.active_release_id) via COALESCE in DEVICE_ROWS_SQL.';
|
package/migrations/README.md
CHANGED
|
@@ -26,6 +26,7 @@ SEMICOLONY CLI의 PostgreSQL 마이그레이션 파일들.
|
|
|
26
26
|
| `185_worker_agent_feedback_scope.sql` | Hermes Semi/Colony actor에 정확한 feedback 범위·귀속을 추가하고 기존 바인딩만 백필 |
|
|
27
27
|
| `186_worker_agent_decision_capture.sql` | operator 승인 reference와 actor-bound decision recorder를 추가하고 정확한 decision scope만 백필 |
|
|
28
28
|
| `187_worker_agent_decision_digest_repair.sql` | Phase 6 decision recorder를 extension schema와 무관한 PostgreSQL SHA-256 primitive로 교정 |
|
|
29
|
+
| `195_worker_device_target_release.sql` | `worker_devices.active_release_id` (nullable, FK) 추가 — `/api/worker-fleet` DEVICE_ROWS_SQL의 COALESCE 대상 컬럼 누락으로 발생한 500 수정 |
|
|
29
30
|
|
|
30
31
|
## 007: Flat Skill Naming
|
|
31
32
|
|