1import json
2import os
3from pathlib import Path
4import re
5import secrets
6import sqlite3
7from datetime import datetime, timezone
8import time
9import uuid
10from contextlib import closing
11
12from importlib import import_module
13
14Error = import_module('dashboard-run').Error
15
16
17def database(mode, timeout):
18 path = Path(os.environ.get('STUDIO_SHALE_DATABASE', '/srv/prod/shale/data/astheno.shale.db'))
19 return sqlite3.connect(path.as_uri() + '?mode=' + mode, uri=True, timeout=timeout)
20
21
22def identifier():
23 value = (int((time.time() - 1577836800) * 1000) << 80) | secrets.randbits(80)
24 alphabet = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
25 return ''.join(alphabet[(value >> (5 * i)) & 31] for i in range(25, -1, -1))
26
27
28def update_label_counts(db, labels, now):
29 for label in labels:
30 db.execute("UPDATE issue_labels SET open_issues=(SELECT count(*) FROM issues__labels il JOIN issues i ON i.id=il.issue WHERE il.label=issue_labels.id AND i.status NOT IN ('done','not_planned','duplicate','invalid')),last_updated=? WHERE id=?", (now, label))
31
32
33def metadata(request):
34 names = request['accounts']
35 if (not isinstance(names, list) or len(names) > 1024
36 or any(not isinstance(n, str) or len(n) > 254 for n in names)
37 or request['repository'] is not None and not isinstance(request['repository'], str)):
38 raise Error(400, 'Choose up to 1024 account names.')
39 accounts = Path(os.environ.get('STUDIO_DASHBOARD_DATA', '/var/lib/studio/dashboard'))
40 with closing(database('ro', 2)) as db:
41 db.execute('ATTACH DATABASE ? AS auth', ((accounts / 'accounts.sqlite').as_uri() + '?mode=ro',))
42 result = {}
43 for name, model, owner, picture in db.execute(
44 "SELECT u.name,json_extract(a.profile,'$.model'),o.name,json_extract(h.profile,'$.attributes.picture[0]') "
45 "FROM users u JOIN auth.users a ON a.id=u.snowflake "
46 "JOIN auth.users h ON h.id=json_extract(a.profile,'$.owner') "
47 "JOIN users o ON o.snowflake=h.id AND o.provider=u.provider "
48 "WHERE u.provider=? AND json_extract(a.profile,'$.kind')='ai' AND u.name IN (" + ','.join('?' for _ in names) + ")",
49 ['snowglobe.' + os.environ.get('STUDIO_DOMAIN', 'studio.test'), *names]):
50 result[name] = {'model': model, 'owner': owner, 'picture': picture}
51 for name, seed, picture in db.execute(
52 "SELECT u.name,u.uuid,json_extract(a.profile,'$.attributes.picture[0]') "
53 "FROM users u JOIN auth.users a ON a.id=u.snowflake "
54 "WHERE u.provider=? AND coalesce(json_extract(a.profile,'$.kind'),'user') NOT IN ('ai','guest') "
55 "AND u.name IN (" + ','.join('?' for _ in names) + ")",
56 ['snowglobe.' + os.environ.get('STUDIO_DOMAIN', 'studio.test'), *names]):
57 if picture:
58 result[name] = {'avatar': seed, 'picture': picture}
59 row = db.execute("SELECT (SELECT count(*) FROM issues WHERE repo=r.id AND status NOT IN ('done','not_planned','duplicate','invalid')) FROM repositories r WHERE name=?", (request['repository'],)).fetchone()
60 return {'accounts': result, 'openIssues': row[0] if row else None}
61
62
63def actor_account(db, owner, model, domain, now):
64 if not isinstance(model, str) or not re.fullmatch(r'[a-z0-9][a-z0-9._-]{0,127}', model):
65 raise Error(400, 'Supply the actual model identifier in model.')
66 human = db.execute('SELECT a.profile,u.name FROM auth.users a JOIN users u ON u.snowflake=a.id WHERE a.id=? AND u.provider=?',
67 (owner, 'snowglobe.' + domain)).fetchone()
68 if not human or json.loads(human[0]).get('enabled') is not True or json.loads(human[0]).get('kind') in ('ai', 'guest'):
69 raise Error(403, 'The account on whose behalf this model writes is unavailable.')
70 name = 'ai-' + human[1] + '-' + model
71 if len(name) > 254:
72 raise Error(400, 'Use a shorter model identifier.')
73 matches = db.execute("SELECT id,username,profile FROM auth.users WHERE json_extract(profile,'$.kind')='ai' AND json_extract(profile,'$.owner')=? AND json_extract(profile,'$.model')=?", (owner, model)).fetchall()
74 if len(matches) > 1:
75 raise Error(409, 'This model has multiple accounts. Ask the server owner to repair them.')
76 if matches:
77 subject, name, profile = matches[0]
78 if json.loads(profile).get('enabled') is not True:
79 raise Error(403, 'This model account is disabled. Ask the server owner to enable it.')
80 else:
81 if db.execute('SELECT 1 FROM auth.users WHERE username=?', (name,)).fetchone():
82 raise Error(409, 'This model account name is already taken. Ask the server owner to repair it.')
83 subject = str(uuid.uuid4())
84 db.execute('INSERT INTO auth.users(id,profile) VALUES(?,?)',
85 (subject, json.dumps({'kind': 'ai', 'username': name, 'owner': owner, 'model': model, 'enabled': True, 'requiredActions': []})))
86 actor = db.execute('SELECT id FROM users WHERE provider=? AND snowflake=?', ('snowglobe.' + domain, subject)).fetchone()
87 if not actor:
88 actor = (db.execute('INSERT INTO users(uuid,provider,snowflake,name,joined_on,last_updated) VALUES(?,?,?,?,?,?)',
89 (identifier(), 'snowglobe.' + domain, subject, name, now, now)).lastrowid,)
90 return actor[0], name
91
92
93def handle(request):
94 action, args = request['action'], request['arguments']
95 fields = {'create_label': {'name', 'description', 'color'}, 'update_label': {'label_id', 'name', 'description', 'color'},
96 'delete_label': {'label_id'}, 'set_issue_labels': {'id', 'labels'}, 'set_issue_status': {'id', 'status'}, 'create_issue': {'title', 'description', 'status'},
97 'comment_issue': {'id', 'comment'}, 'set_issue_title': {'id', 'title'}}
98 if action not in fields or not isinstance(args, dict) or set(args) != fields[action] | {'model'}:
99 raise Error(400, 'Use the fields listed for this tool.')
100 for key in ('id', 'label_id'):
101 if key in args and (type(args[key]) is not int or args[key] <= 0):
102 raise Error(400, 'Choose a positive issue or label ID.')
103 if action in ('create_label', 'update_label'):
104 if (not isinstance(args['name'], str) or not args['name'].strip() or len(args['name']) > 256
105 or not isinstance(args['description'], str) or len(args['description']) > 4096
106 or not isinstance(args['color'], str) or not re.fullmatch(r'#[0-9a-fA-F]{6}', args['color'])):
107 raise Error(400, 'Enter a label name, description, and six-digit hex color within the tool limits.')
108 if action == 'set_issue_status' and args['status'] not in ('needs_triage', 'incomplete', 'todo', 'done', 'not_planned', 'duplicate', 'invalid'):
109 raise Error(400, 'Choose a status available in Shale.')
110 if action == 'set_issue_labels':
111 labels = args['labels']
112 if (not isinstance(labels, list) or len(labels) > 100 or any(type(i) is not int or i <= 0 for i in labels)
113 or len(set(labels)) != len(labels)):
114 raise Error(400, 'Choose up to 100 distinct positive label IDs. An empty list clears the labels.')
115 for field, maximum in (('title', 4096), ('description', 262144), ('comment', 262144)):
116 if field in args and (not isinstance(args[field], str) or len(args[field].encode()) > maximum or field != 'description' and not args[field].strip()):
117 raise Error(400, 'Supply issue text within the tool limits.')
118 if action == 'create_issue' and args['status'] not in ('needs_triage', 'todo'):
119 raise Error(400, 'Choose triage or todo for a new issue.')
120 accounts = Path(os.environ.get('STUDIO_DASHBOARD_DATA', '/var/lib/studio/dashboard'))
121 domain = os.environ.get('STUDIO_DOMAIN', 'studio.test')
122 with closing(database('rw', 10)) as db, db:
123 db.row_factory = sqlite3.Row
124 db.execute('PRAGMA foreign_keys=ON')
125 db.execute('ATTACH DATABASE ? AS auth', ((accounts / 'accounts.sqlite').as_uri() + '?mode=rw',))
126 db.execute('ATTACH DATABASE ? AS mcp', ((accounts / 'connections.sqlite').as_uri() + '?mode=ro',))
127 db.execute('BEGIN IMMEDIATE')
128 row = db.execute('SELECT value FROM mcp.records WHERE key=?', ('grant:' + request['grant'],)).fetchone()
129 grant = json.loads(row[0]) if row else {}
130 repository = request['repository']
131 if (grant.get('resource') != 'https://snowglobe.' + domain + '/mcp/shale'
132 or 'shale:write' not in grant.get('scopes', [])
133 or (grant.get('resources') != 'all' and repository not in grant.get('resources', []))):
134 raise Error(403, 'This connection cannot edit this repository. Update its repository selection.')
135 caller = db.execute('SELECT username,profile FROM auth.users WHERE id=?', (grant.get('user'),)).fetchone()
136 if not caller or json.loads(caller[1]).get('enabled') is not True or json.loads(caller[1]).get('kind') in ('guest', 'ai'):
137 raise Error(401, 'This connection is unavailable. Connect again.')
138 repo = db.execute('SELECT id,owner,access_issues,access_issues_submit,access_issues_comment FROM repositories WHERE name=?', (repository,)).fetchone()
139 native_caller = db.execute('SELECT id FROM users WHERE provider=? AND snowflake=?', ('snowglobe.' + domain, grant['user'])).fetchone()
140 if not repo or not native_caller or repo[2] == 'off':
141 raise Error(403, 'This Shale connection cannot edit this repository.')
142 policy = json.loads(Path(os.environ.get('STUDIO_SHALE_POLICY', str(Path(__file__).resolve().parent.parent / 'config/shale.json'))).read_text())
143 owner = policy.get('actorOwners', {}).get(caller[0], grant['user'])
144 issue = None if action == 'create_issue' or 'id' not in args else db.execute('SELECT id,status,author,title FROM issues WHERE repo=? AND number=?', (repo[0], args['id'])).fetchone()
145 if 'id' in args and not issue:
146 raise Error(404, 'That issue is unavailable. List this repository’s issues again.')
147 owns_repo = repo[1] == native_caller[0]
148 if not owns_repo:
149 if action == 'create_issue':
150 permitted = repo[2] == 'public' and repo[3] == 'public'
151 elif action == 'comment_issue':
152 permitted = repo[2] == 'public' and repo[4] == 'public'
153 elif action == 'set_issue_title':
154 author = db.execute("SELECT u.snowflake,json_extract(a.profile,'$.owner') FROM users u LEFT JOIN auth.users a ON a.id=u.snowflake WHERE u.id=? AND u.provider=?", (issue[2], 'snowglobe.' + domain)).fetchone()
155 permitted = repo[2] == 'public' and author and owner in author
156 else:
157 permitted = action == 'set_issue_status' and repository in policy.get('statusDelegates', {}).get(caller[0], [])
158 if not permitted:
159 raise Error(403, 'This connection cannot make that change in this repository.')
160 linked = db.execute('SELECT value FROM mcp.records WHERE key=?', ('shale-session:' + grant['user'],)).fetchone()
161 if not linked or json.loads(linked[0]).get('origin') != 'https://shale.' + domain + '/':
162 raise Error(401, 'This Shale connection is unavailable. Connect again.')
163 now = datetime.now(timezone.utc).isoformat(timespec='milliseconds')
164 actor, actor_name = actor_account(db, owner, args['model'], domain, now)
165 repo = repo[0]
166 if action in ('create_issue', 'comment_issue', 'set_issue_title'):
167 if action == 'create_issue':
168 number = db.execute('SELECT coalesce(max(number),0)+1 FROM issues WHERE repo=?', (repo,)).fetchone()[0]
169 issue_id = db.execute('INSERT INTO issues(uuid,repo,number,author,last_updated,title,status) VALUES(?,?,?,?,?,?,?)',
170 (identifier(), repo, number, actor, now, args['title'], args['status'])).lastrowid
171 kind, payload = 'comment', args['description']
172 else:
173 issue_id, number = issue[0], args['id']
174 kind, payload = ('comment', args['comment']) if action == 'comment_issue' else ('update_title', args['title'])
175 if action == 'set_issue_title' and issue[3] == payload:
176 return {'repository': repository, 'id': number, 'actor': '~' + actor_name}
177 if action == 'set_issue_title':
178 db.execute('UPDATE issues SET title=? WHERE id=?', (payload, issue_id))
179 db.execute('UPDATE issues SET last_updated=? WHERE id=?', (now, issue_id))
180 db.execute('INSERT INTO issue_actions(uuid,issue,actor,kind,payload,last_updated,added_on) VALUES(?,?,?,?,?,?,?)',
181 (identifier(), issue_id, actor, kind, payload, now, now))
182 db.execute("UPDATE repositories SET open_issues=(SELECT count(*) FROM issues WHERE repo=repositories.id AND status NOT IN ('done','not_planned','duplicate','invalid')),last_updated=? WHERE id=?", (now, repo))
183 return {'repository': repository, 'id': number, 'actor': '~' + actor_name}
184 if action in ('update_label', 'delete_label'):
185 if not db.execute('SELECT 1 FROM issue_labels WHERE repo=? AND id=?', (repo, args['label_id'])).fetchone():
186 raise Error(404, 'That label is unavailable in this repository. List its labels again.')
187 if action == 'delete_label' and (db.execute('SELECT 1 FROM issues__labels WHERE label=? LIMIT 1', (args['label_id'],)).fetchone()
188 or db.execute("SELECT 1 FROM issue_actions a JOIN issues i ON a.issue=i.id WHERE i.repo=? AND a.kind IN ('add_label','remove_label') AND a.payload=? LIMIT 1", (repo, str(args['label_id']))).fetchone()):
189 raise Error(409, 'This label is referenced by issue history. Rename it or clear its assignments instead.')
190 if action == 'set_issue_status':
191 if issue[1] != args['status']:
192 db.execute('INSERT INTO issue_actions(uuid,issue,actor,kind,payload,last_updated,added_on) VALUES(?,?,?,?,?,?,?)',
193 (identifier(), issue[0], actor, 'update_status', args['status'], now, now))
194 db.execute('UPDATE issues SET status=?,last_updated=? WHERE id=?', (args['status'], now, issue[0]))
195 update_label_counts(db, (row[0] for row in db.execute('SELECT label FROM issues__labels WHERE issue=?', (issue[0],))), now)
196 db.execute("UPDATE repositories SET open_issues=(SELECT count(*) FROM issues WHERE repo=repositories.id AND status NOT IN ('done','not_planned','duplicate','invalid')),last_updated=? WHERE id=?", (now, repo))
197 return {'repository': repository, 'id': args['id'], 'status': args['status'], 'actor': '~' + actor_name}
198 if action in ('create_label', 'update_label'):
199 if db.execute('SELECT 1 FROM issue_labels WHERE repo=? AND name=? AND id!=?', (repo, args['name'], args.get('label_id', 0))).fetchone():
200 raise Error(409, 'A label already has that name. Choose another name.')
201 if action == 'create_label':
202 label_id = db.execute('INSERT INTO issue_labels(uuid,repo,name,description,color,last_updated) VALUES(?,?,?,?,?,?)',
203 (identifier(), repo, args['name'], args['description'], args['color'], now)).lastrowid
204 else:
205 label_id = args['label_id']
206 db.execute('UPDATE issue_labels SET name=?,description=?,color=?,last_updated=? WHERE id=?',
207 (args['name'], args['description'], args['color'], now, label_id))
208 elif action == 'delete_label':
209 db.execute('DELETE FROM issue_labels WHERE id=?', (args['label_id'],))
210 else:
211 desired = set(args['labels'])
212 available = {r[0] for r in db.execute('SELECT id FROM issue_labels WHERE repo=?', (repo,))}
213 if not desired <= available:
214 raise Error(400, 'Choose labels belonging to this repository. List its labels again.')
215 issue_id = issue[0]
216 current = {r[0] for r in db.execute('SELECT label FROM issues__labels WHERE issue=?', (issue_id,))}
217 affected = current ^ desired
218 for kind, labels in (('remove_label', current - desired), ('add_label', desired - current)):
219 for label in sorted(labels):
220 if kind == 'add_label':
221 db.execute('INSERT INTO issues__labels(uuid,issue,label) VALUES(?,?,?)', (identifier(), issue_id, label))
222 else:
223 db.execute('DELETE FROM issues__labels WHERE issue=? AND label=?', (issue_id, label))
224 db.execute('INSERT INTO issue_actions(uuid,issue,actor,kind,payload,last_updated,added_on) VALUES(?,?,?,?,?,?,?)',
225 (identifier(), issue_id, actor, kind, str(label), now, now))
226 if current != desired:
227 db.execute('UPDATE issues SET last_updated=? WHERE id=?', (now, issue_id))
228 update_label_counts(db, affected, now)
229 if action != 'set_issue_labels' or affected:
230 db.execute('UPDATE repositories SET last_updated=? WHERE id=?', (now, repo))
231 result = {'repository': repository, 'actor': '~' + actor_name}
232 if action == 'set_issue_labels':
233 result.update(id=args['id'], labels=args['labels'])
234 elif action == 'delete_label':
235 result.update(deleted=args['label_id'])
236 else:
237 result['label'] = dict(db.execute('SELECT id,name,description,color,open_issues AS openIssues FROM issue_labels WHERE id=?', (label_id,)).fetchone())
238 return result