@team-semicolon/semicolony-cli 4.18.59 → 4.18.61

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,201 @@
1
+ -- 170_worker_release_credential_scopes.sql
2
+ --
3
+ -- Migration 169 added the worker release Gateway surface, but the
4
+ -- owner-controlled issuance function from migration 167 still rejected the
5
+ -- corresponding read/report scopes. Replace only that function and preserve
6
+ -- its validation, atomic rotation/device binding, and privilege boundary.
7
+ --
8
+ -- The migration runner supplies BEGIN/COMMIT. All object references are
9
+ -- schema-qualified and the SECURITY DEFINER search path is fixed.
10
+
11
+ CREATE OR REPLACE FUNCTION semicolony.issue_worker_gateway_credential(
12
+ p_team_tenant_id uuid,
13
+ p_worker_id text,
14
+ p_device_id text,
15
+ p_token_hash text,
16
+ p_token_prefix text,
17
+ p_scopes text[],
18
+ p_issued_by text,
19
+ p_expires_days integer,
20
+ p_workspace_grants jsonb
21
+ )
22
+ RETURNS uuid
23
+ LANGUAGE plpgsql
24
+ SECURITY DEFINER
25
+ SET search_path = pg_catalog
26
+ AS $function$
27
+ DECLARE
28
+ v_credential_id uuid;
29
+ v_previous_credential_id uuid;
30
+ v_workspace_grants jsonb := coalesce(p_workspace_grants, '[]'::jsonb);
31
+ v_grant jsonb;
32
+ v_project_id text;
33
+ v_capability text;
34
+ v_environments text[];
35
+ v_grant_key text;
36
+ v_seen_grant_keys text[] := ARRAY[]::text[];
37
+ v_has_credential_grant boolean := false;
38
+ v_has_asset_grant boolean := false;
39
+ BEGIN
40
+ IF p_worker_id IS NULL OR p_worker_id !~ '^[a-z][a-z0-9-]{0,63}$'
41
+ OR p_device_id IS NULL OR p_device_id !~ '^[a-z][a-z0-9-]{0,99}$' THEN
42
+ RAISE EXCEPTION 'invalid worker/device binding' USING ERRCODE = '22023';
43
+ END IF;
44
+ IF p_token_hash IS NULL OR p_token_hash !~ '^[0-9a-f]{64}$' THEN
45
+ RAISE EXCEPTION 'invalid worker credential token hash' USING ERRCODE = '22023';
46
+ END IF;
47
+ IF p_token_prefix IS NULL
48
+ OR p_token_prefix !~ ('^wck_' || p_worker_id || '_[A-Za-z0-9_-]{6}$') THEN
49
+ RAISE EXCEPTION 'invalid worker credential token prefix' USING ERRCODE = '22023';
50
+ END IF;
51
+ IF p_issued_by IS NULL OR p_issued_by <> btrim(p_issued_by)
52
+ OR btrim(p_issued_by) = '' OR char_length(p_issued_by) > 128
53
+ OR p_issued_by ~ '[[:cntrl:]]' THEN
54
+ RAISE EXCEPTION 'invalid credential issuer' USING ERRCODE = '22023';
55
+ END IF;
56
+ IF p_expires_days IS NULL OR p_expires_days < 1 OR p_expires_days > 90 THEN
57
+ RAISE EXCEPTION 'worker credential expiry must be between 1 and 90 days' USING ERRCODE = '22023';
58
+ END IF;
59
+ IF p_scopes IS NULL OR cardinality(p_scopes) = 0
60
+ OR EXISTS (
61
+ SELECT 1
62
+ FROM unnest(p_scopes) AS requested(scope)
63
+ WHERE requested.scope IS NULL
64
+ OR requested.scope <> ALL (ARRAY[
65
+ 'kb:read',
66
+ 'skills:read',
67
+ 'skills:write',
68
+ 'workspace:credentials-read',
69
+ 'workspace:assets-read',
70
+ 'worker:release:read',
71
+ 'worker:release:report'
72
+ ]::text[])
73
+ )
74
+ OR cardinality(p_scopes) <> (
75
+ SELECT count(DISTINCT requested.scope)::integer FROM unnest(p_scopes) AS requested(scope)
76
+ )
77
+ OR ('skills:write' = ANY(p_scopes) AND NOT ('skills:read' = ANY(p_scopes))) THEN
78
+ RAISE EXCEPTION 'worker credential scopes are not permitted' USING ERRCODE = '22023';
79
+ END IF;
80
+ IF jsonb_typeof(v_workspace_grants) <> 'array' THEN
81
+ RAISE EXCEPTION 'workspace grants must be a JSON array' USING ERRCODE = '22023';
82
+ END IF;
83
+
84
+ FOR v_grant IN SELECT item.value FROM jsonb_array_elements(v_workspace_grants) AS item(value) LOOP
85
+ IF jsonb_typeof(v_grant) <> 'object'
86
+ OR EXISTS (
87
+ SELECT 1
88
+ FROM jsonb_object_keys(v_grant) AS object_key(key)
89
+ WHERE object_key.key NOT IN ('projectId', 'capability', 'environments')
90
+ ) THEN
91
+ RAISE EXCEPTION 'invalid workspace grant shape' USING ERRCODE = '22023';
92
+ END IF;
93
+ v_project_id := v_grant ->> 'projectId';
94
+ v_capability := v_grant ->> 'capability';
95
+ IF v_project_id IS NULL OR v_project_id !~ '^[a-z][a-z0-9-]{0,99}$'
96
+ OR v_capability NOT IN ('credentials-read', 'assets-read')
97
+ OR jsonb_typeof(v_grant -> 'environments') <> 'array' THEN
98
+ RAISE EXCEPTION 'invalid workspace grant' USING ERRCODE = '22023';
99
+ END IF;
100
+ v_environments := ARRAY(
101
+ SELECT jsonb_array_elements_text(v_grant -> 'environments')
102
+ );
103
+ IF cardinality(v_environments) <> (
104
+ SELECT count(DISTINCT environment.value)::integer
105
+ FROM unnest(v_environments) AS environment(value)
106
+ ) OR NOT (v_environments <@ ARRAY['dev', 'e2e']::text[])
107
+ OR (v_capability = 'credentials-read' AND cardinality(v_environments) = 0)
108
+ OR (v_capability = 'assets-read' AND cardinality(v_environments) <> 0) THEN
109
+ RAISE EXCEPTION 'invalid workspace grant environments' USING ERRCODE = '22023';
110
+ END IF;
111
+ v_grant_key := v_project_id || ':' || v_capability;
112
+ IF v_grant_key = ANY(v_seen_grant_keys) THEN
113
+ RAISE EXCEPTION 'duplicate workspace grant' USING ERRCODE = '22023';
114
+ END IF;
115
+ v_seen_grant_keys := array_append(v_seen_grant_keys, v_grant_key);
116
+ v_has_credential_grant := v_has_credential_grant OR v_capability = 'credentials-read';
117
+ v_has_asset_grant := v_has_asset_grant OR v_capability = 'assets-read';
118
+ END LOOP;
119
+ IF v_has_credential_grant <> ('workspace:credentials-read' = ANY(p_scopes))
120
+ OR v_has_asset_grant <> ('workspace:assets-read' = ANY(p_scopes)) THEN
121
+ RAISE EXCEPTION 'workspace scopes must exactly match workspace grants' USING ERRCODE = '22023';
122
+ END IF;
123
+
124
+ PERFORM t.id
125
+ FROM public.tenants AS t
126
+ WHERE t.id = p_team_tenant_id
127
+ AND t.slug = 'team-semicolon'
128
+ AND t.tenant_type = 'team'
129
+ AND coalesce(t.metadata ->> 'status', 'active') = 'active';
130
+ IF NOT FOUND THEN
131
+ RAISE EXCEPTION 'active team-semicolon tenant not found' USING ERRCODE = 'P0002';
132
+ END IF;
133
+
134
+ SELECT wd.gateway_credential_id
135
+ INTO v_previous_credential_id
136
+ FROM semicolony.worker_devices AS wd
137
+ WHERE wd.device_id = p_device_id
138
+ AND wd.worker_id = p_worker_id
139
+ AND wd.status = 'active'
140
+ AND wd.kb_access_mode = 'worker-gateway'
141
+ AND wd.last_doctor_status = 'pass'
142
+ AND wd.last_seen_at >= clock_timestamp() - interval '24 hours'
143
+ FOR UPDATE;
144
+ IF NOT FOUND THEN
145
+ RAISE EXCEPTION 'active worker device binding not found' USING ERRCODE = 'P0002';
146
+ END IF;
147
+
148
+ INSERT INTO semicolony.gateway_credentials (
149
+ tenant_id, tenant_slug, token_hash, token_prefix, scopes, issued_by, expires_at, metadata
150
+ ) VALUES (
151
+ p_team_tenant_id,
152
+ 'team-semicolon',
153
+ p_token_hash,
154
+ p_token_prefix,
155
+ p_scopes,
156
+ p_issued_by,
157
+ clock_timestamp() + make_interval(days => p_expires_days),
158
+ jsonb_build_object(
159
+ 'credential_kind', 'worker',
160
+ 'worker_id', p_worker_id,
161
+ 'device_id', p_device_id,
162
+ 'tenant_anchor', 'team-semicolon',
163
+ 'lineage_status', 'active'
164
+ )
165
+ )
166
+ RETURNING id INTO v_credential_id;
167
+
168
+ INSERT INTO semicolony.workspace_projection_grants (
169
+ gateway_credential_id, project_id, capability, environments
170
+ )
171
+ SELECT
172
+ v_credential_id,
173
+ item.value ->> 'projectId',
174
+ item.value ->> 'capability',
175
+ ARRAY(SELECT jsonb_array_elements_text(item.value -> 'environments'))
176
+ FROM jsonb_array_elements(v_workspace_grants) AS item(value);
177
+
178
+ IF v_previous_credential_id IS NOT NULL THEN
179
+ UPDATE semicolony.gateway_credentials
180
+ SET status = 'revoked',
181
+ revoked_at = clock_timestamp(),
182
+ revoked_reason = 'worker credential rotated'
183
+ WHERE id = v_previous_credential_id
184
+ AND status = 'active';
185
+ END IF;
186
+
187
+ UPDATE semicolony.worker_devices
188
+ SET gateway_credential_id = v_credential_id
189
+ WHERE device_id = p_device_id
190
+ AND worker_id = p_worker_id;
191
+
192
+ RETURN v_credential_id;
193
+ END;
194
+ $function$;
195
+
196
+ REVOKE ALL ON FUNCTION semicolony.issue_worker_gateway_credential(
197
+ uuid, text, text, text, text, text[], text, integer, jsonb
198
+ ) FROM PUBLIC, sc_app, sc_provider;
199
+ GRANT EXECUTE ON FUNCTION semicolony.issue_worker_gateway_credential(
200
+ uuid, text, text, text, text, text[], text, integer, jsonb
201
+ ) TO sc_app;
@@ -16,6 +16,8 @@ SEMICOLONY CLI의 PostgreSQL 마이그레이션 파일들.
16
16
  | `007_flatten_skill_names.sql` | Flat skill naming 마이그레이션 |
17
17
  | `165_skill_release_retirements.sql` | 거버넌스 스킬 은퇴 프리미티브 (append-only 기록 + 뷰) |
18
18
  | `166_lease_operations_retire.sql` | `skill_capability_leases` profile→operations CHECK 재생성: operate 프로필에 `skills.release.retire` 추가 |
19
+ | `169_worker_profile_releases.sql` | Immutable SEMO Ops worker profile releases and device rollout state |
20
+ | `170_worker_release_credential_scopes.sql` | Worker credential owner 함수의 허용 범위에 release read/report scope 추가 |
19
21
 
20
22
  ## 007: Flat Skill Naming
21
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semicolony-cli",
3
- "version": "4.18.59",
3
+ "version": "4.18.61",
4
4
  "description": "SemiColony CLI - AI operations and agent orchestration installer",
5
5
  "main": "dist/bundle.js",
6
6
  "bin": {