@team-semicolon/semicolony-cli 4.18.102 → 4.18.104

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,140 @@
1
+ -- 197_ops_dashboard_asset_credential.sql
2
+ --
3
+ -- SEMO Ops 대시보드가 팀 자산 게이트웨이를 호출하기 위한 디바이스 비의존 자격증명 발급기.
4
+ --
5
+ -- 배경: 게이트웨이의 `authorizeRead`(packages/kb-gateway/src/lib/workspace-assets.ts)는
6
+ -- `ctx.kind === 'worker'` + `workspace:assets-read` scope + 프로젝트별 grant 행을 요구한다.
7
+ -- 대시보드는 워커 디바이스가 아니고, 기존 내부 HMAC 경로는 `kind:'internal'` 이라 자산
8
+ -- 라우트에서 전부 403 이다. 그래서 대시보드 전용 principal 이 필요하다.
9
+ --
10
+ -- 왜 기존 발급기를 못 쓰나: `issue_worker_gateway_credential` 은 활성 `worker_devices` 행
11
+ -- (doctor pass + 24h 내 last_seen)을 `FOR UPDATE` 로 잡고, 없으면 P0002 로 실패한다.
12
+ -- 대시보드는 디바이스 등록·doctor 보고를 하는 주체가 아니므로 그 전제를 만족할 수 없다.
13
+ --
14
+ -- 왜 이게 안전한가: 자격증명 해석기(kb-gateway `tenant-credentials.ts`)는 `worker_devices` 를
15
+ -- 조회하지 않는다. `metadata.credential_kind='worker'` 와 grant 행만 본다. 즉 디바이스 요구는
16
+ -- 발급 측 정책이지 검증 측 불변식이 아니다. 이 함수는 그 정책만 다르게 두고 나머지 검증
17
+ -- (scope 화이트리스트, 토큰 형식, 만료 상한, grant 모양)은 기존 발급기와 동일하게 유지한다.
18
+ --
19
+ -- 경계: 실제 워커 디바이스와 절대 충돌하지 않도록, 같은 worker/device 이름의 `worker_devices`
20
+ -- 행이 존재하면 발급을 거부한다. 그리고 이 발급기는 `workspace:assets-read` 외의 scope 를
21
+ -- 허용하지 않는다 — 쓰기 승격은 기존 `grant_worker_asset_write()` 로만 한다(감사 amendment 경유).
22
+ --
23
+ -- Safety: additive only. Runner가 BEGIN/COMMIT으로 감싸므로 트랜잭션 구문을 넣지 않는다.
24
+ -- `semicolony.` 접두는 러너의 retargetSchemaSql 이 활성 스키마로 바꾼다(중앙 DB 에서는 semo_ops).
25
+
26
+ CREATE OR REPLACE FUNCTION semicolony.issue_ops_dashboard_gateway_credential(
27
+ p_team_tenant_id uuid,
28
+ p_token_hash text,
29
+ p_token_prefix text,
30
+ p_project_id text,
31
+ p_issued_by text,
32
+ p_expires_days integer
33
+ )
34
+ RETURNS uuid
35
+ LANGUAGE plpgsql
36
+ SECURITY DEFINER
37
+ SET search_path = pg_catalog
38
+ AS $function$
39
+ DECLARE
40
+ v_principal constant text := 'semo-ops-dashboard';
41
+ v_credential_id uuid;
42
+ v_previous_credential_id uuid;
43
+ BEGIN
44
+ IF p_token_hash IS NULL OR p_token_hash !~ '^[0-9a-f]{64}$' THEN
45
+ RAISE EXCEPTION 'invalid ops dashboard credential token hash' USING ERRCODE = '22023';
46
+ END IF;
47
+ IF p_token_prefix IS NULL
48
+ OR p_token_prefix !~ ('^wck_' || v_principal || '_[A-Za-z0-9_-]{6}$') THEN
49
+ RAISE EXCEPTION 'invalid ops dashboard credential token prefix' USING ERRCODE = '22023';
50
+ END IF;
51
+ IF p_project_id IS NULL OR p_project_id !~ '^[a-z][a-z0-9-]{0,99}$' THEN
52
+ RAISE EXCEPTION 'invalid project id' USING ERRCODE = '22023';
53
+ END IF;
54
+ IF p_issued_by IS NULL OR p_issued_by <> btrim(p_issued_by)
55
+ OR btrim(p_issued_by) = '' OR char_length(p_issued_by) > 128
56
+ OR p_issued_by ~ '[[:cntrl:]]' THEN
57
+ RAISE EXCEPTION 'invalid credential issuer' USING ERRCODE = '22023';
58
+ END IF;
59
+ IF p_expires_days IS NULL OR p_expires_days < 1 OR p_expires_days > 90 THEN
60
+ RAISE EXCEPTION 'credential expiry must be between 1 and 90 days' USING ERRCODE = '22023';
61
+ END IF;
62
+
63
+ -- 실제 워커 디바이스와의 이름 충돌을 구조적으로 차단한다. 충돌하면 이 함수가 발급한
64
+ -- 자격증명이 워커 회전 로직에 휩쓸리거나 그 반대가 될 수 있다.
65
+ IF EXISTS (
66
+ SELECT 1 FROM semicolony.worker_devices AS wd
67
+ WHERE wd.worker_id = v_principal OR wd.device_id = v_principal
68
+ ) THEN
69
+ RAISE EXCEPTION 'a worker device already claims the ops dashboard principal'
70
+ USING ERRCODE = '23505';
71
+ END IF;
72
+
73
+ PERFORM t.id
74
+ FROM public.tenants AS t
75
+ WHERE t.id = p_team_tenant_id
76
+ AND t.slug = 'team-semicolon'
77
+ AND t.tenant_type = 'team'
78
+ AND coalesce(t.metadata ->> 'status', 'active') = 'active';
79
+ IF NOT FOUND THEN
80
+ RAISE EXCEPTION 'active team-semicolon tenant not found' USING ERRCODE = 'P0002';
81
+ END IF;
82
+
83
+ -- 회전: 이전 활성 대시보드 자격증명을 폐기한다. 워커 디바이스 행을 건드리지 않는다.
84
+ SELECT gc.id
85
+ INTO v_previous_credential_id
86
+ FROM semicolony.gateway_credentials AS gc
87
+ WHERE gc.status = 'active'
88
+ AND gc.metadata ->> 'principal_class' = 'ops-dashboard'
89
+ AND gc.metadata ->> 'worker_id' = v_principal
90
+ ORDER BY gc.issued_at DESC
91
+ LIMIT 1
92
+ FOR UPDATE;
93
+
94
+ INSERT INTO semicolony.gateway_credentials (
95
+ tenant_id, tenant_slug, token_hash, token_prefix, scopes, issued_by, expires_at, metadata
96
+ ) VALUES (
97
+ p_team_tenant_id,
98
+ 'team-semicolon',
99
+ p_token_hash,
100
+ p_token_prefix,
101
+ ARRAY['workspace:assets-read']::text[],
102
+ p_issued_by,
103
+ clock_timestamp() + make_interval(days => p_expires_days),
104
+ jsonb_build_object(
105
+ 'credential_kind', 'worker',
106
+ 'principal_class', 'ops-dashboard',
107
+ 'worker_id', v_principal,
108
+ 'device_id', v_principal,
109
+ 'tenant_anchor', 'team-semicolon',
110
+ 'lineage_status', 'active'
111
+ )
112
+ )
113
+ RETURNING id INTO v_credential_id;
114
+
115
+ -- assets-read grant 는 환경 바인딩이 없어야 한다(192 의 environment 계약).
116
+ INSERT INTO semicolony.workspace_projection_grants (
117
+ gateway_credential_id, project_id, capability, environments
118
+ ) VALUES (
119
+ v_credential_id, p_project_id, 'assets-read', ARRAY[]::text[]
120
+ );
121
+
122
+ IF v_previous_credential_id IS NOT NULL THEN
123
+ UPDATE semicolony.gateway_credentials
124
+ SET status = 'revoked',
125
+ revoked_at = clock_timestamp(),
126
+ revoked_reason = 'ops dashboard credential rotated'
127
+ WHERE id = v_previous_credential_id
128
+ AND status = 'active';
129
+ END IF;
130
+
131
+ RETURN v_credential_id;
132
+ END;
133
+ $function$;
134
+
135
+ REVOKE ALL ON FUNCTION semicolony.issue_ops_dashboard_gateway_credential(
136
+ uuid, text, text, text, text, integer
137
+ ) FROM PUBLIC, sc_app, sc_provider;
138
+ GRANT EXECUTE ON FUNCTION semicolony.issue_ops_dashboard_gateway_credential(
139
+ uuid, text, text, text, text, integer
140
+ ) TO sc_app;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-semicolon/semicolony-cli",
3
- "version": "4.18.102",
3
+ "version": "4.18.104",
4
4
  "description": "SemiColony CLI - AI operations and agent orchestration installer",
5
5
  "main": "dist/bundle.js",
6
6
  "bin": {