@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.';
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semicolony-cli",
3
- "version": "4.18.95",
3
+ "version": "4.18.97",
4
4
  "description": "SemiColony CLI - AI operations and agent orchestration installer",
5
5
  "main": "dist/bundle.js",
6
6
  "bin": {