ai_co_discuss 0.3.10 → 0.3.11

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.
@@ -5,6 +5,10 @@ connected browser would send to the local bridge; the paired computer claims it
5
5
  sync, resolves the chosen project to its own directory and runs it. The Worker backend
6
6
  implements the same contract in ``worker.mjs``; both operate on ``local_machines`` and
7
7
  ``remote_launches``.
8
+
9
+ Every other local-service operation travels as a machine command: the device encrypts it with the
10
+ computer's published P-256 key, the site only queues ciphertext, and the computer long-polls, runs it
11
+ through the same local entrypoint and returns an encrypted response.
8
12
  """
9
13
 
10
14
  import hashlib
@@ -27,6 +31,13 @@ MACHINE_TOKEN = re.compile(r"[0-9a-f]{64}")
27
31
  LAUNCH_ID = re.compile(r"[A-Za-z0-9_-]{1,80}")
28
32
  PROJECT_ID = re.compile(r"[0-9a-f]{8,32}")
29
33
  LAUNCH_STAGES = ("not_started", "unconfirmed", "created", "started")
34
+ COMMAND_ID = re.compile(r"[A-Za-z0-9_-]{16,80}")
35
+ COMMAND_TTL_SECONDS = 180
36
+ COMMAND_WAIT_MAX = 20
37
+ ENVELOPE_DATA_MAX = 512 * 1024
38
+ BASE64 = re.compile(r"[A-Za-z0-9+/]*={0,2}")
39
+ BASE64URL_COORDINATE = re.compile(r"[A-Za-z0-9_-]{43}")
40
+ IV_BASE64 = re.compile(r"[A-Za-z0-9+/]{16}")
30
41
 
31
42
 
32
43
  def token_digest(token):
@@ -34,9 +45,13 @@ def token_digest(token):
34
45
  return hashlib.sha256(token.encode()).hexdigest() if isinstance(token, str) and MACHINE_TOKEN.fullmatch(token) else ""
35
46
 
36
47
 
48
+ MACHINE_SELECT = "SELECT m.*, k.public_key FROM local_machines m LEFT JOIN machine_keys k ON k.machine_id=m.id"
49
+
50
+
37
51
  def machine_view(row, now):
38
- """Public machine fields: projects by name and id, and which platforms it can wake."""
52
+ """Public machine fields: projects by name and id, which platforms it can wake, and its command key if any."""
39
53
  return {"id": row["id"], "name": row["name"], "projects": json.loads(row["projects"]), "clients": json.loads(row["clients"]),
54
+ "public_key": json.loads(row["public_key"]) if row["public_key"] else None,
40
55
  "seen_at": row["seen_at"], "online": now - row["seen_at"] <= MACHINE_ONLINE_SECONDS}
41
56
 
42
57
 
@@ -48,7 +63,7 @@ def launch_view(row):
48
63
 
49
64
  def list_machines(connection, now):
50
65
  """Paired computers, most recently seen first."""
51
- rows = connection.execute("SELECT * FROM local_machines ORDER BY seen_at DESC, created_at DESC").fetchall()
66
+ rows = connection.execute(f"{MACHINE_SELECT} ORDER BY m.seen_at DESC, m.created_at DESC").fetchall()
52
67
  return {"machines": [machine_view(row, now) for row in rows]}
53
68
 
54
69
 
@@ -64,7 +79,7 @@ def pair_machine(connection, payload, now):
64
79
  connection.execute("INSERT INTO local_machines (id,name,token_hash,created_at) VALUES (?,?,?,?) "
65
80
  "ON CONFLICT(id) DO UPDATE SET name=excluded.name, token_hash=excluded.token_hash",
66
81
  (machine_id, name, token_digest(token), now))
67
- return machine_view(connection.execute("SELECT * FROM local_machines WHERE id=?", (machine_id,)).fetchone(), now)
82
+ return machine_view(connection.execute(f"{MACHINE_SELECT} WHERE m.id=?", (machine_id,)).fetchone(), now)
68
83
 
69
84
 
70
85
  def require_machine(connection, machine_id, token):
@@ -98,13 +113,33 @@ def valid_clients(value):
98
113
  return value
99
114
 
100
115
 
116
+ def valid_public_key(value):
117
+ """An ECDH P-256 public key as JWK coordinates; anything else is rejected."""
118
+ if (not isinstance(value, dict) or value.get("kty") != "EC" or value.get("crv") != "P-256"
119
+ or not all(isinstance(value.get(axis), str) and BASE64URL_COORDINATE.fullmatch(value[axis]) for axis in ("x", "y"))):
120
+ raise ValueError("电脑公钥须为 P-256 JWK")
121
+ return {"kty": "EC", "crv": "P-256", "x": value["x"], "y": value["y"]}
122
+
123
+
124
+ def valid_envelope(value, with_key):
125
+ """Ciphertext and IV, plus the sender's ephemeral key on requests; the site never reads the content."""
126
+ if (not isinstance(value, dict) or not isinstance(value.get("iv"), str) or not IV_BASE64.fullmatch(value["iv"])
127
+ or not isinstance(value.get("data"), str) or not 0 < len(value["data"]) <= ENVELOPE_DATA_MAX or not BASE64.fullmatch(value["data"])):
128
+ raise ValueError("加密命令格式无效")
129
+ return {"iv": value["iv"], "data": value["data"], **({"key": valid_public_key(value.get("key"))} if with_key else {})}
130
+
131
+
101
132
  def machine_sync(connection, machine_id, token, payload, now):
102
- """Record the computer's projects and clients, and hand it every launch it has not finished."""
133
+ """Record the computer's projects, clients and command key, and hand it every launch it has not finished."""
103
134
  require_machine(connection, machine_id, token)
104
135
  projects, clients = valid_projects(payload.get("projects")), valid_clients(payload.get("clients"))
136
+ public_key = valid_public_key(payload["public_key"]) if "public_key" in payload else None
105
137
  begin_write(connection)
106
138
  connection.execute("UPDATE local_machines SET projects=?, clients=?, seen_at=? WHERE id=?",
107
139
  (json.dumps(projects, ensure_ascii=False), json.dumps(clients, ensure_ascii=False), now, machine_id))
140
+ if public_key:
141
+ connection.execute("INSERT INTO machine_keys (machine_id,public_key) VALUES (?,?) "
142
+ "ON CONFLICT(machine_id) DO UPDATE SET public_key=excluded.public_key", (machine_id, json.dumps(public_key)))
108
143
  rows = connection.execute("SELECT id,request FROM remote_launches WHERE machine_id=? AND status IN ('pending','claimed') ORDER BY created_at",
109
144
  (machine_id,)).fetchall()
110
145
  return {"launches": [{"id": row["id"], "request": json.loads(row["request"])} for row in rows]}
@@ -173,3 +208,60 @@ def remote_launch_status(connection, launch_id):
173
208
  if not row:
174
209
  raise LookupError("启动请求不存在")
175
210
  return launch_view(row)
211
+
212
+
213
+ def submit_machine_command(connection, machine_id, payload, now):
214
+ """Queue one encrypted local-service operation for the paired computer."""
215
+ command_id = payload.get("id")
216
+ if not isinstance(command_id, str) or not COMMAND_ID.fullmatch(command_id):
217
+ raise ValueError("命令编号无效")
218
+ envelope = valid_envelope(payload.get("envelope"), with_key=True)
219
+ begin_write(connection)
220
+ if not connection.execute("SELECT 1 FROM local_machines WHERE id=?", (machine_id,)).fetchone():
221
+ raise LookupError("这台电脑尚未连接网站")
222
+ if not connection.execute("SELECT 1 FROM machine_keys WHERE machine_id=?", (machine_id,)).fetchone():
223
+ raise FileExistsError("这台电脑的 codiscuss 版本过旧,请更新后重试")
224
+ if connection.execute("SELECT 1 FROM machine_commands WHERE id=?", (command_id,)).fetchone():
225
+ raise FileExistsError("命令编号已使用")
226
+ connection.execute("INSERT INTO machine_commands (id,machine_id,request,created_at,updated_at) VALUES (?,?,?,?,?)",
227
+ (command_id, machine_id, json.dumps(envelope), now, now))
228
+ return {"id": command_id, "status": "pending"}
229
+
230
+
231
+ def machine_command_status(connection, machine_id, command_id):
232
+ """Report a command's progress; a finished command hands over its response once and is deleted."""
233
+ row = connection.execute("SELECT * FROM machine_commands WHERE id=? AND machine_id=?", (command_id, machine_id)).fetchone()
234
+ if not row:
235
+ raise LookupError("命令不存在或已过期")
236
+ if row["status"] != "done":
237
+ return {"id": command_id, "status": row["status"]}
238
+ begin_write(connection)
239
+ connection.execute("DELETE FROM machine_commands WHERE id=?", (command_id,))
240
+ return {"id": command_id, "status": "done", "response": json.loads(row["response"])}
241
+
242
+
243
+ def claim_machine_commands(connection, machine_id, token, now):
244
+ """Drop expired commands and hand the computer every pending one, clearing the queued ciphertext."""
245
+ require_machine(connection, machine_id, token)
246
+ begin_write(connection)
247
+ connection.execute("DELETE FROM machine_commands WHERE machine_id=? AND created_at<?", (machine_id, now - COMMAND_TTL_SECONDS))
248
+ connection.execute("UPDATE local_machines SET seen_at=? WHERE id=?", (now, machine_id))
249
+ rows = connection.execute("SELECT id,request FROM machine_commands WHERE machine_id=? AND status='pending' ORDER BY created_at",
250
+ (machine_id,)).fetchall()
251
+ for row in rows:
252
+ connection.execute("UPDATE machine_commands SET status='claimed', request='{}', updated_at=? WHERE id=?", (now, row["id"]))
253
+ return {"commands": [{"id": row["id"], "envelope": json.loads(row["request"])} for row in rows]}
254
+
255
+
256
+ def finish_machine_command(connection, machine_id, command_id, token, payload, now):
257
+ """Store the computer's encrypted response for the device that is waiting on it."""
258
+ require_machine(connection, machine_id, token)
259
+ envelope = valid_envelope(payload.get("envelope"), with_key=False)
260
+ begin_write(connection)
261
+ row = connection.execute("SELECT status FROM machine_commands WHERE id=? AND machine_id=?", (command_id, machine_id)).fetchone()
262
+ if not row:
263
+ raise LookupError("命令不存在或已过期")
264
+ if row["status"] != "claimed":
265
+ raise FileExistsError("命令已结束")
266
+ connection.execute("UPDATE machine_commands SET status='done', response=?, updated_at=? WHERE id=?", (json.dumps(envelope), now, command_id))
267
+ return {"id": command_id, "status": "done"}
@@ -831,6 +831,24 @@ CREATE TABLE IF NOT EXISTS remote_launches (
831
831
  updated_at INTEGER NOT NULL
832
832
  );
833
833
  CREATE INDEX IF NOT EXISTS remote_launches_machine ON remote_launches(machine_id, status);
834
+ -- The P-256 public key a paired computer publishes so other devices can encrypt commands only it can read.
835
+ CREATE TABLE IF NOT EXISTS machine_keys (
836
+ machine_id TEXT PRIMARY KEY,
837
+ public_key TEXT NOT NULL
838
+ );
839
+ -- A local-service operation relayed from a device without the local service. Request and response are end-to-end
840
+ -- encrypted between that device and the computer, so the site never holds keys, paths or results in plaintext;
841
+ -- the request is cleared once claimed and the row is deleted once read or expired.
842
+ CREATE TABLE IF NOT EXISTS machine_commands (
843
+ id TEXT PRIMARY KEY,
844
+ machine_id TEXT NOT NULL,
845
+ request TEXT NOT NULL DEFAULT '{}',
846
+ response TEXT NOT NULL DEFAULT '{}',
847
+ status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','claimed','done')),
848
+ created_at INTEGER NOT NULL,
849
+ updated_at INTEGER NOT NULL
850
+ );
851
+ CREATE INDEX IF NOT EXISTS machine_commands_machine ON machine_commands(machine_id, status);
834
852
 
835
853
  -- Normalize legacy online relay members and retired brainstorm rooms for the shared free-room model.
836
854
  INSERT OR IGNORE INTO member_manual_sources(member_id,source)