@team-semicolon/semicolony-cli 4.18.92 → 4.18.93

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,237 @@
1
+ -- 192_workspace_asset_management.sql
2
+ --
3
+ -- Add explicit, project-bound read/write capabilities to the existing
4
+ -- immutable workspace asset projection. Worker issuance continues to pass
5
+ -- through the hardened migration-170 issuer. The operator amendment function
6
+ -- repairs both halves together so a recently rotated read-less worker key can
7
+ -- be granted useful access without an ad-hoc table write.
8
+ --
9
+ -- Slack actors keep their exact Phase-6 credential scopes because the
10
+ -- decision recorder pins that array. Asset capabilities are derived from a
11
+ -- separate DB binding table by the Gateway resolver.
12
+ --
13
+ -- The migration runner owns BEGIN/COMMIT.
14
+
15
+ ALTER TABLE semicolony.workspace_projection_grants
16
+ DROP CONSTRAINT workspace_projection_grants_capability_check;
17
+ ALTER TABLE semicolony.workspace_projection_grants
18
+ DROP CONSTRAINT workspace_projection_grants_check;
19
+ ALTER TABLE semicolony.workspace_projection_grants
20
+ ADD CONSTRAINT workspace_projection_grants_capability_check
21
+ CHECK (capability IN ('credentials-read', 'assets-read', 'assets-write', 'skills-read'));
22
+ ALTER TABLE semicolony.workspace_projection_grants
23
+ ADD CONSTRAINT workspace_projection_grants_environment_contract_check
24
+ CHECK (
25
+ (capability = 'credentials-read' AND cardinality(environments) > 0)
26
+ OR (capability IN ('assets-read', 'assets-write', 'skills-read')
27
+ AND cardinality(environments) = 0)
28
+ );
29
+
30
+ CREATE OR REPLACE FUNCTION semicolony.issue_worker_asset_gateway_credential(
31
+ p_team_tenant_id uuid,
32
+ p_worker_id text,
33
+ p_device_id text,
34
+ p_token_hash text,
35
+ p_token_prefix text,
36
+ p_scopes text[],
37
+ p_issued_by text,
38
+ p_expires_days integer,
39
+ p_workspace_grants jsonb
40
+ )
41
+ RETURNS uuid
42
+ LANGUAGE plpgsql
43
+ SECURITY DEFINER
44
+ SET search_path = pg_catalog
45
+ AS $function$
46
+ DECLARE
47
+ v_credential_id uuid;
48
+ v_base_grants jsonb;
49
+ v_base_scopes text[];
50
+ v_write_count integer;
51
+ BEGIN
52
+ IF jsonb_typeof(p_workspace_grants) <> 'array' THEN
53
+ RAISE EXCEPTION 'workspace grants must be a JSON array' USING ERRCODE = '22023';
54
+ END IF;
55
+ SELECT count(*)::integer
56
+ INTO v_write_count
57
+ FROM jsonb_array_elements(p_workspace_grants) AS grant_row(value)
58
+ WHERE grant_row.value ->> 'capability' = 'assets-write';
59
+ IF v_write_count < 1 OR NOT ('workspace:assets-write' = ANY(p_scopes)) THEN
60
+ RAISE EXCEPTION 'asset write scope and grant are required' USING ERRCODE = '22023';
61
+ END IF;
62
+ IF cardinality(p_scopes) <> (
63
+ SELECT count(DISTINCT requested.scope)::integer FROM unnest(p_scopes) AS requested(scope)
64
+ ) THEN
65
+ RAISE EXCEPTION 'worker credential scopes contain duplicates' USING ERRCODE = '22023';
66
+ END IF;
67
+ IF EXISTS (
68
+ SELECT 1
69
+ FROM jsonb_array_elements(p_workspace_grants) AS write_grant(value)
70
+ WHERE write_grant.value ->> 'capability' = 'assets-write'
71
+ AND (
72
+ jsonb_typeof(write_grant.value) <> 'object'
73
+ OR write_grant.value ->> 'projectId' !~ '^[a-z][a-z0-9-]{0,99}$'
74
+ OR write_grant.value -> 'environments' <> '[]'::jsonb
75
+ OR NOT EXISTS (
76
+ SELECT 1
77
+ FROM jsonb_array_elements(p_workspace_grants) AS read_grant(value)
78
+ WHERE read_grant.value ->> 'projectId' = write_grant.value ->> 'projectId'
79
+ AND read_grant.value ->> 'capability' = 'assets-read'
80
+ AND read_grant.value -> 'environments' = '[]'::jsonb
81
+ )
82
+ )
83
+ ) THEN
84
+ RAISE EXCEPTION 'every asset write grant requires a paired read grant'
85
+ USING ERRCODE = '22023';
86
+ END IF;
87
+
88
+ SELECT coalesce(jsonb_agg(grant_row.value), '[]'::jsonb)
89
+ INTO v_base_grants
90
+ FROM jsonb_array_elements(p_workspace_grants) AS grant_row(value)
91
+ WHERE grant_row.value ->> 'capability' <> 'assets-write';
92
+ SELECT array_agg(scope ORDER BY ordinal)
93
+ INTO v_base_scopes
94
+ FROM unnest(p_scopes) WITH ORDINALITY AS requested(scope, ordinal)
95
+ WHERE scope <> 'workspace:assets-write';
96
+
97
+ v_credential_id := semicolony.issue_worker_gateway_credential(
98
+ p_team_tenant_id,
99
+ p_worker_id,
100
+ p_device_id,
101
+ p_token_hash,
102
+ p_token_prefix,
103
+ v_base_scopes,
104
+ p_issued_by,
105
+ p_expires_days,
106
+ v_base_grants
107
+ );
108
+
109
+ INSERT INTO semicolony.workspace_projection_grants (
110
+ gateway_credential_id, project_id, capability, environments
111
+ )
112
+ SELECT
113
+ v_credential_id,
114
+ grant_row.value ->> 'projectId',
115
+ 'assets-write',
116
+ ARRAY[]::text[]
117
+ FROM jsonb_array_elements(p_workspace_grants) AS grant_row(value)
118
+ WHERE grant_row.value ->> 'capability' = 'assets-write';
119
+
120
+ UPDATE semicolony.gateway_credentials
121
+ SET scopes = p_scopes
122
+ WHERE id = v_credential_id;
123
+
124
+ RETURN v_credential_id;
125
+ END;
126
+ $function$;
127
+
128
+ REVOKE ALL ON FUNCTION semicolony.issue_worker_asset_gateway_credential(
129
+ uuid, text, text, text, text, text[], text, integer, jsonb
130
+ ) FROM PUBLIC, sc_app, sc_provider;
131
+ GRANT EXECUTE ON FUNCTION semicolony.issue_worker_asset_gateway_credential(
132
+ uuid, text, text, text, text, text[], text, integer, jsonb
133
+ ) TO sc_app;
134
+
135
+ CREATE TABLE semicolony.worker_agent_actor_capabilities (
136
+ installation_id text NOT NULL,
137
+ actor_id text NOT NULL,
138
+ capability text NOT NULL CHECK (capability IN ('assets:read', 'assets:write')),
139
+ status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked')),
140
+ granted_by text NOT NULL DEFAULT 'migration-192',
141
+ granted_at timestamptz NOT NULL DEFAULT clock_timestamp(),
142
+ PRIMARY KEY (installation_id, actor_id, capability),
143
+ FOREIGN KEY (installation_id, actor_id)
144
+ REFERENCES semicolony.worker_agent_actor_bindings(installation_id, actor_id)
145
+ ON DELETE CASCADE
146
+ );
147
+
148
+ INSERT INTO semicolony.worker_agent_actor_capabilities (
149
+ installation_id, actor_id, capability
150
+ )
151
+ SELECT binding.installation_id, binding.actor_id, capability
152
+ FROM semicolony.worker_agent_actor_bindings AS binding
153
+ CROSS JOIN (VALUES ('assets:read'), ('assets:write')) AS capabilities(capability)
154
+ WHERE binding.status = 'active'
155
+ ON CONFLICT (installation_id, actor_id, capability) DO NOTHING;
156
+
157
+ REVOKE ALL ON TABLE semicolony.worker_agent_actor_capabilities
158
+ FROM PUBLIC, sc_app, sc_provider;
159
+ GRANT SELECT ON TABLE semicolony.worker_agent_actor_capabilities TO sc_app;
160
+
161
+ CREATE OR REPLACE FUNCTION semicolony.grant_worker_asset_write(
162
+ p_credential_id uuid,
163
+ p_project_id text,
164
+ p_granted_by text,
165
+ p_reason text
166
+ )
167
+ RETURNS uuid
168
+ LANGUAGE plpgsql
169
+ SECURITY DEFINER
170
+ SET search_path = pg_catalog
171
+ AS $function$
172
+ DECLARE
173
+ v_amendment_id uuid;
174
+ v_previous_scopes text[];
175
+ v_next_scopes text[];
176
+ BEGIN
177
+ IF p_credential_id IS NULL
178
+ OR p_project_id IS NULL OR p_project_id !~ '^[a-z][a-z0-9-]{0,99}$'
179
+ OR NULLIF(btrim(coalesce(p_granted_by, '')), '') IS NULL
180
+ OR NULLIF(btrim(coalesce(p_reason, '')), '') IS NULL THEN
181
+ RAISE EXCEPTION 'credential, project, grantor, and reason are required'
182
+ USING ERRCODE = '22023';
183
+ END IF;
184
+
185
+ SELECT gc.scopes
186
+ INTO v_previous_scopes
187
+ FROM semicolony.gateway_credentials AS gc
188
+ WHERE gc.id = p_credential_id
189
+ AND gc.status = 'active'
190
+ AND gc.revoked_at IS NULL
191
+ AND (gc.expires_at IS NULL OR gc.expires_at > clock_timestamp())
192
+ AND gc.metadata ->> 'credential_kind' = 'worker'
193
+ FOR UPDATE;
194
+ IF NOT FOUND THEN
195
+ RAISE EXCEPTION 'active worker credential not found' USING ERRCODE = 'P0002';
196
+ END IF;
197
+ INSERT INTO semicolony.workspace_projection_grants (
198
+ gateway_credential_id, project_id, capability, environments, status,
199
+ expires_at, metadata
200
+ )
201
+ SELECT
202
+ p_credential_id, p_project_id, capabilities.capability, ARRAY[]::text[], 'active',
203
+ NULL, jsonb_build_object('granted_by', btrim(p_granted_by), 'reason', btrim(p_reason))
204
+ FROM (VALUES ('assets-read'), ('assets-write')) AS capabilities(capability)
205
+ ON CONFLICT (gateway_credential_id, project_id, capability) DO UPDATE
206
+ SET status = 'active',
207
+ expires_at = NULL,
208
+ metadata = EXCLUDED.metadata;
209
+
210
+ v_next_scopes := v_previous_scopes;
211
+ IF NOT ('workspace:assets-read' = ANY(v_next_scopes)) THEN
212
+ v_next_scopes := array_append(v_next_scopes, 'workspace:assets-read');
213
+ END IF;
214
+ IF NOT ('workspace:assets-write' = ANY(v_next_scopes)) THEN
215
+ v_next_scopes := array_append(v_next_scopes, 'workspace:assets-write');
216
+ END IF;
217
+
218
+ INSERT INTO semicolony.gateway_credential_scope_amendments (
219
+ credential_id, previous_scopes, next_scopes, reason, amended_by
220
+ ) VALUES (
221
+ p_credential_id, v_previous_scopes, v_next_scopes,
222
+ btrim(p_reason), btrim(p_granted_by)
223
+ )
224
+ RETURNING amendment_id INTO v_amendment_id;
225
+
226
+ UPDATE semicolony.gateway_credentials
227
+ SET scopes = v_next_scopes
228
+ WHERE id = p_credential_id;
229
+
230
+ RETURN v_amendment_id;
231
+ END;
232
+ $function$;
233
+
234
+ REVOKE ALL ON FUNCTION semicolony.grant_worker_asset_write(uuid, text, text, text)
235
+ FROM PUBLIC, sc_app, sc_provider;
236
+ GRANT EXECUTE ON FUNCTION semicolony.grant_worker_asset_write(uuid, text, text, text)
237
+ TO sc_app;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semicolony-cli",
3
- "version": "4.18.92",
3
+ "version": "4.18.93",
4
4
  "description": "SemiColony CLI - AI operations and agent orchestration installer",
5
5
  "main": "dist/bundle.js",
6
6
  "bin": {