| 1 | import json |
| 2 | import os |
| 3 | from pathlib import Path |
| 4 | import re |
| 5 | import secrets |
| 6 | import sqlite3 |
| 7 | from datetime import datetime, timezone |
| 8 | import time |
| 9 | import uuid |
| 10 | from contextlib import closing |
| 11 | |
| 12 | from importlib import import_module |
| 13 | |
| 14 | Error = import_module('dashboard-run').Error |
| 15 | |
| 16 | |
| 17 | def 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 | |
| 22 | def 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 | |
| 28 | def 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 | |
| 33 | def 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 | |
| 63 | def 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 | |
| 93 | def 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 |