tryharness 0.7.23 → 0.7.25

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.
Files changed (61) hide show
  1. package/assets/opencode-plugin.js +48 -11
  2. package/assets/pi-extension.ts +105 -26
  3. package/cli.mjs +1238 -322
  4. package/lib/activity.mjs +197 -40
  5. package/lib/adapters.mjs +337 -54
  6. package/lib/agentnode.mjs +175 -35
  7. package/lib/atlas.mjs +383 -83
  8. package/lib/cards.mjs +174 -40
  9. package/lib/claudemem.mjs +74 -18
  10. package/lib/commands.mjs +13 -5
  11. package/lib/context.mjs +409 -71
  12. package/lib/control-client.mjs +5 -2
  13. package/lib/coordinator.mjs +274 -57
  14. package/lib/daemon.mjs +5248 -1024
  15. package/lib/desktop.mjs +149 -35
  16. package/lib/digest.mjs +54 -13
  17. package/lib/environment.mjs +103 -21
  18. package/lib/files.mjs +246 -13
  19. package/lib/firewall.mjs +169 -35
  20. package/lib/folders.mjs +179 -36
  21. package/lib/frames.mjs +214 -40
  22. package/lib/inference.mjs +463 -126
  23. package/lib/learn.mjs +78 -16
  24. package/lib/live.mjs +286 -61
  25. package/lib/localmodels.mjs +424 -83
  26. package/lib/mcp-stdio.mjs +868 -151
  27. package/lib/media.mjs +138 -28
  28. package/lib/members.json +1 -1
  29. package/lib/members.mjs +35 -9
  30. package/lib/models.mjs +342 -80
  31. package/lib/network.mjs +201 -42
  32. package/lib/notify.mjs +49 -12
  33. package/lib/nudge.mjs +184 -33
  34. package/lib/ocr.mjs +223 -60
  35. package/lib/onboard.mjs +321 -70
  36. package/lib/parsers.mjs +132 -41
  37. package/lib/paths.mjs +33 -10
  38. package/lib/personal.mjs +51 -13
  39. package/lib/phones.mjs +382 -60
  40. package/lib/places.mjs +555 -107
  41. package/lib/provenance.mjs +19 -4
  42. package/lib/receipts.mjs +51 -11
  43. package/lib/redact.mjs +512 -116
  44. package/lib/runner.mjs +345 -55
  45. package/lib/runtime-install.mjs +55 -14
  46. package/lib/screens.mjs +154 -38
  47. package/lib/sealed.mjs +157 -28
  48. package/lib/secrets.mjs +18 -5
  49. package/lib/service.mjs +170 -34
  50. package/lib/snippets.mjs +94 -18
  51. package/lib/speech.mjs +688 -175
  52. package/lib/store.mjs +1654 -307
  53. package/lib/swarm.mjs +596 -117
  54. package/lib/t3.mjs +400 -79
  55. package/lib/trails.mjs +285 -61
  56. package/lib/ui.mjs +95 -23
  57. package/lib/vectors.mjs +5 -2
  58. package/lib/wakes.mjs +284 -68
  59. package/lib/watches.mjs +247 -48
  60. package/lib/worktrees.mjs +186 -38
  61. package/package.json +3 -3
package/lib/phones.mjs CHANGED
@@ -1,6 +1,15 @@
1
1
  import os from "node:os";
2
2
  import { randomBytes } from "node:crypto";
3
- import { b64u, channelKey, constantTimeEqual, loadOrCreateIdentity, MAX_SKEW_MS, open, pairProof, seal } from "./sealed.mjs";
3
+ import {
4
+ b64u,
5
+ channelKey,
6
+ constantTimeEqual,
7
+ loadOrCreateIdentity,
8
+ MAX_SKEW_MS,
9
+ open,
10
+ pairProof,
11
+ seal,
12
+ } from "./sealed.mjs";
4
13
 
5
14
  // The daemon's side of the person's own devices: pairing offers made on this machine (a QR code
6
15
  // for a phone, a link opened in a browser), and sealed calls from paired devices, opened here and
@@ -13,36 +22,134 @@ const OFFER_TTL_MS = 10 * 60_000;
13
22
  const MAX_OFFERS = 5;
14
23
  // A message queued at Harness while this computer slept may arrive up to 7 days late; only decisions and posts may.
15
24
  const QUEUED_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
16
- const QUEUEABLE = new Set(["board.post", "board.run", "board.release", "board.answer", "board.remove", "board.thread.post", "board.thread.send", "board.thread.discard", "board.thread.end", "board.goal.stop",
17
- "board.swarm.start", "board.swarm.post", "board.swarm.stop", "board.swarm.stop_all", "board.swarm.resume", "board.swarm.start_run"]);
25
+ const QUEUEABLE = new Set([
26
+ "board.post",
27
+ "board.run",
28
+ "board.release",
29
+ "board.answer",
30
+ "board.remove",
31
+ "board.thread.post",
32
+ "board.thread.send",
33
+ "board.thread.discard",
34
+ "board.thread.end",
35
+ "board.goal.stop",
36
+ "board.swarm.start",
37
+ "board.swarm.post",
38
+ "board.swarm.stop",
39
+ "board.swarm.stop_all",
40
+ "board.swarm.resume",
41
+ "board.swarm.start_run",
42
+ ]);
18
43
  const ID = /^[A-Za-z0-9_-]{8,64}$/;
19
44
  const KEY = /^[A-Za-z0-9_-]{80,100}$/;
20
45
  const KINDS = new Set(["phone", "browser"]);
21
46
  const PHONE_COMMANDS = new Set([
22
- "board.list", "board.get", "board.children", "board.agents", "board.post", "board.run", "board.release", "board.answer", "board.remove",
23
- "board.thread.read", "board.thread.post", "board.thread.send", "board.thread.discard", "board.thread.auto", "board.thread.end",
24
- "board.goal.list", "board.goal.get", "board.goal.stop", "board.room.read",
47
+ "board.list",
48
+ "board.get",
49
+ "board.children",
50
+ "board.agents",
51
+ "board.post",
52
+ "board.run",
53
+ "board.release",
54
+ "board.answer",
55
+ "board.remove",
56
+ "board.thread.read",
57
+ "board.thread.post",
58
+ "board.thread.send",
59
+ "board.thread.discard",
60
+ "board.thread.auto",
61
+ "board.thread.end",
62
+ "board.goal.list",
63
+ "board.goal.get",
64
+ "board.goal.stop",
65
+ "board.room.read",
25
66
  // A thread's files: shown, opened and downloaded, and shared or kept on this computer, by the person.
26
- "board.file.get", "board.file.share",
27
- "board.swarm.list", "board.swarm.read", "board.swarm.start", "board.swarm.post", "board.swarm.stop", "board.swarm.stop_all", "board.swarm.resume", "board.swarm.folder", "board.swarm.coordinator",
28
- "board.swarm.diff", "board.swarm.merge", "board.swarm.discard", "board.swarm.start_run", "board.swarm.agents", "board.swarm.note",
67
+ "board.file.get",
68
+ "board.file.share",
69
+ "board.swarm.list",
70
+ "board.swarm.read",
71
+ "board.swarm.start",
72
+ "board.swarm.post",
73
+ "board.swarm.stop",
74
+ "board.swarm.stop_all",
75
+ "board.swarm.resume",
76
+ "board.swarm.folder",
77
+ "board.swarm.coordinator",
78
+ "board.swarm.diff",
79
+ "board.swarm.merge",
80
+ "board.swarm.discard",
81
+ "board.swarm.start_run",
82
+ "board.swarm.agents",
83
+ "board.swarm.note",
29
84
  // Where a thread works: the folders offered, any folder browsed to, and where a message would work (folders.mjs).
30
- "board.swarm.places", "board.swarm.browse", "board.swarm.place",
85
+ "board.swarm.places",
86
+ "board.swarm.browse",
87
+ "board.swarm.place",
31
88
  // The person's own running sessions, read and messaged as from the desktop app; never queued (a session may be gone).
32
- "board.live", "board.live.read", "board.live.say",
89
+ "board.live",
90
+ "board.live.read",
91
+ "board.live.say",
33
92
  ]);
34
93
  // What the chat owner does on the board, in Harness's name (was src/server/mcp/board.ts HARNESS_COMMANDS).
35
94
  const HARNESS_COMMANDS = new Set([
36
- "board.list", "board.get", "board.post", "board.claim", "board.report", "board.answer", "board.thread.read", "board.thread.post", "board.standing.check",
37
- "board.goal.create", "board.goal.get", "board.goal.list", "board.goal.finish", "board.room.read", "board.room.post", "board.room.search", "board.children", "board.agents",
38
- "board.swarm.list", "board.swarm.read", "board.swarm.diff",
95
+ "board.list",
96
+ "board.get",
97
+ "board.post",
98
+ "board.claim",
99
+ "board.report",
100
+ "board.answer",
101
+ "board.thread.read",
102
+ "board.thread.post",
103
+ "board.standing.check",
104
+ "board.goal.create",
105
+ "board.goal.get",
106
+ "board.goal.list",
107
+ "board.goal.finish",
108
+ "board.room.read",
109
+ "board.room.post",
110
+ "board.room.search",
111
+ "board.children",
112
+ "board.agents",
113
+ "board.swarm.list",
114
+ "board.swarm.read",
115
+ "board.swarm.diff",
39
116
  ]);
40
117
  // Swarms talking to swarms (network.mjs), from the Harness window: the person's own, never Harness's.
41
- const RELAY_PERSON_COMMANDS = ["board.relay.list", "board.relay.network", "board.relay.decide", "board.relay.request", "board.relay.seen", "board.relay.defaults", "board.relay.peers"];
42
- const BROWSER_PERSON_COMMANDS = new Set([...PHONE_COMMANDS, "board.goal.resume", "board.standing", "board.standing.check", "board.standing.off", ...RELAY_PERSON_COMMANDS]);
43
- const BROWSER_READS = new Set(["sessions.search", "sessions.read", "sessions.raw", "tasks.status", "relay.request", "relay.open"]);
118
+ const RELAY_PERSON_COMMANDS = [
119
+ "board.relay.list",
120
+ "board.relay.network",
121
+ "board.relay.decide",
122
+ "board.relay.request",
123
+ "board.relay.seen",
124
+ "board.relay.defaults",
125
+ "board.relay.peers",
126
+ ];
127
+ const BROWSER_PERSON_COMMANDS = new Set([
128
+ ...PHONE_COMMANDS,
129
+ "board.goal.resume",
130
+ "board.standing",
131
+ "board.standing.check",
132
+ "board.standing.off",
133
+ ...RELAY_PERSON_COMMANDS,
134
+ ]);
135
+ const BROWSER_READS = new Set([
136
+ "sessions.search",
137
+ "sessions.read",
138
+ "sessions.raw",
139
+ "tasks.status",
140
+ "relay.request",
141
+ "relay.open",
142
+ ]);
44
143
 
45
- export async function createPhoneDesk({ store, identityFile, boardCommand, reads = {}, version, onMessage = () => {}, machine = () => null }) {
144
+ export async function createPhoneDesk({
145
+ store,
146
+ identityFile,
147
+ boardCommand,
148
+ reads = {},
149
+ version,
150
+ onMessage = () => {},
151
+ machine = () => null,
152
+ }) {
46
153
  const identity = await loadOrCreateIdentity(identityFile);
47
154
  const offers = new Map();
48
155
  const keys = new Map();
@@ -50,10 +157,15 @@ export async function createPhoneDesk({ store, identityFile, boardCommand, reads
50
157
 
51
158
  const sweep = () => {
52
159
  const now = Date.now();
53
- for (const [id, offer] of offers) if (offer.expiresAt < now) offers.delete(id);
160
+ for (const [id, offer] of offers)
161
+ if (offer.expiresAt < now) offers.delete(id);
54
162
  };
55
163
  const keyFor = async (device) => {
56
- if (!keys.has(device.id)) keys.set(device.id, await channelKey(identity.privateKey, device.public_key, device.id));
164
+ if (!keys.has(device.id))
165
+ keys.set(
166
+ device.id,
167
+ await channelKey(identity.privateKey, device.public_key, device.id)
168
+ );
57
169
  return keys.get(device.id);
58
170
  };
59
171
 
@@ -66,52 +178,152 @@ export async function createPhoneDesk({ store, identityFile, boardCommand, reads
66
178
  const offerId = b64u(randomBytes(16));
67
179
  const secret = b64u(randomBytes(32));
68
180
  offers.set(offerId, { secret, kind, expiresAt: Date.now() + OFFER_TTL_MS });
69
- const fragment = new URLSearchParams({ v: "1", o: offerId, s: secret, k: identity.publicKey, h: hostname });
181
+ const fragment = new URLSearchParams({
182
+ v: "1",
183
+ o: offerId,
184
+ s: secret,
185
+ k: identity.publicKey,
186
+ h: hostname,
187
+ });
70
188
  const page = kind === "browser" ? "/link" : "/m/pair";
71
- return { offerId, kind, expiresInSeconds: OFFER_TTL_MS / 1000, url: `${String(origin).replace(/\/$/, "")}${page}#${fragment}` };
189
+ return {
190
+ offerId,
191
+ kind,
192
+ expiresInSeconds: OFFER_TTL_MS / 1000,
193
+ url: `${String(origin).replace(/\/$/, "")}${page}#${fragment}`,
194
+ };
72
195
  };
73
196
 
74
197
  const pair = async (input) => {
75
198
  sweep();
76
199
  const { offerId, deviceId, devicePublicKey, proof, requestId } = input;
77
- if (![offerId, deviceId, requestId].every((value) => ID.test(String(value))) || !KEY.test(String(devicePublicKey))) return { ok: false, error: "invalid pairing request" };
200
+ if (
201
+ ![offerId, deviceId, requestId].every((value) =>
202
+ ID.test(String(value))
203
+ ) ||
204
+ !KEY.test(String(devicePublicKey))
205
+ )
206
+ return { ok: false, error: "invalid pairing request" };
78
207
  const pending = offers.get(String(offerId));
79
208
  // One attempt per code, right or wrong: a guessed proof burns the code.
80
209
  offers.delete(String(offerId));
81
- if (!pending) return { ok: false, error: "this pairing code expired or was already used; make a new one on the computer" };
82
- if (!constantTimeEqual(proof, await pairProof(pending.secret, offerId, devicePublicKey, identity.publicKey))) return { ok: false, error: "the pairing proof did not match" };
83
- if (store.getPhone(String(deviceId))) return { ok: false, error: "this device is already paired" };
210
+ if (!pending)
211
+ return {
212
+ ok: false,
213
+ error:
214
+ "this pairing code expired or was already used; make a new one on the computer",
215
+ };
216
+ if (
217
+ !constantTimeEqual(
218
+ proof,
219
+ await pairProof(
220
+ pending.secret,
221
+ offerId,
222
+ devicePublicKey,
223
+ identity.publicKey
224
+ )
225
+ )
226
+ )
227
+ return { ok: false, error: "the pairing proof did not match" };
228
+ if (store.getPhone(String(deviceId)))
229
+ return { ok: false, error: "this device is already paired" };
84
230
  const fallback = pending.kind === "browser" ? "Browser" : "Phone";
85
- const label = String(input.label || fallback).replace(/[^\p{L}\p{N} ._'-]/gu, "").slice(0, 40) || fallback;
86
- const device = store.addPhone({ id: String(deviceId), label, publicKey: String(devicePublicKey), kind: pending.kind });
87
- return { ok: true, envelope: await seal(await keyFor(device), "d2p", device.id, String(requestId), { paired: true, hostname, label, kind: device.kind, version }) };
231
+ const label =
232
+ String(input.label || fallback)
233
+ .replace(/[^\p{L}\p{N} ._'-]/gu, "")
234
+ .slice(0, 40) || fallback;
235
+ const device = store.addPhone({
236
+ id: String(deviceId),
237
+ label,
238
+ publicKey: String(devicePublicKey),
239
+ kind: pending.kind,
240
+ });
241
+ return {
242
+ ok: true,
243
+ envelope: await seal(
244
+ await keyFor(device),
245
+ "d2p",
246
+ device.id,
247
+ String(requestId),
248
+ { paired: true, hostname, label, kind: device.kind, version }
249
+ ),
250
+ };
88
251
  };
89
252
 
90
253
  const call = async (envelope) => {
91
254
  sweep();
92
- if (!envelope || envelope.v !== 1 || !ID.test(String(envelope.deviceId)) || !ID.test(String(envelope.requestId)) || typeof envelope.iv !== "string" || typeof envelope.ct !== "string") {
255
+ if (
256
+ !envelope ||
257
+ envelope.v !== 1 ||
258
+ !ID.test(String(envelope.deviceId)) ||
259
+ !ID.test(String(envelope.requestId)) ||
260
+ typeof envelope.iv !== "string" ||
261
+ typeof envelope.ct !== "string"
262
+ ) {
93
263
  return { ok: false, error: "invalid envelope" };
94
264
  }
95
265
  const device = store.getPhone(envelope.deviceId);
96
- if (!device) return { ok: false, error: "unknown device", code: "unpaired" };
97
- if (device.revoked_at) return { ok: false, error: "this device was disconnected from the computer", code: "revoked" };
266
+ if (!device)
267
+ return { ok: false, error: "unknown device", code: "unpaired" };
268
+ if (device.revoked_at)
269
+ return {
270
+ ok: false,
271
+ error: "this device was disconnected from the computer",
272
+ code: "revoked",
273
+ };
98
274
  const key = await keyFor(device);
99
275
  let request;
100
- try { request = await open(key, "p2d", envelope); } catch { return { ok: false, error: "the message could not be opened", code: "unreadable" }; }
276
+ try {
277
+ request = await open(key, "p2d", envelope);
278
+ } catch {
279
+ return {
280
+ ok: false,
281
+ error: "the message could not be opened",
282
+ code: "unreadable",
283
+ };
284
+ }
101
285
  const queued = request?.queued === true;
102
- if (queued && !QUEUEABLE.has(String(request?.call?.command || ""))) return { ok: false, error: "only posts and decisions wait for the computer", code: "refused" };
103
- const age = typeof request?.ts === "number" ? Date.now() - request.ts : Infinity;
104
- if (queued ? age > QUEUED_MAX_AGE_MS || age < -MAX_SKEW_MS : Math.abs(age) > MAX_SKEW_MS) return { ok: false, error: "the message is too old or the device's clock is off", code: "stale" };
105
- if (!store.firstPhoneRequest(device.id, String(envelope.requestId))) return { ok: false, error: "this message was already received", code: "replayed" };
286
+ if (queued && !QUEUEABLE.has(String(request?.call?.command || "")))
287
+ return {
288
+ ok: false,
289
+ error: "only posts and decisions wait for the computer",
290
+ code: "refused",
291
+ };
292
+ const age =
293
+ typeof request?.ts === "number" ? Date.now() - request.ts : Infinity;
294
+ if (
295
+ queued
296
+ ? age > QUEUED_MAX_AGE_MS || age < -MAX_SKEW_MS
297
+ : Math.abs(age) > MAX_SKEW_MS
298
+ )
299
+ return {
300
+ ok: false,
301
+ error: "the message is too old or the device's clock is off",
302
+ code: "stale",
303
+ };
304
+ if (!store.firstPhoneRequest(device.id, String(envelope.requestId)))
305
+ return {
306
+ ok: false,
307
+ error: "this message was already received",
308
+ code: "replayed",
309
+ };
106
310
  store.touchPhone(device.id);
107
311
  // Any message from a paired device keeps a Mac in Standby awake for the next hour (Lid-Closed Mode).
108
312
  onMessage();
109
313
  if (request.call?.command === "phone.forget") {
110
314
  const forgotten = revoke(device.id);
111
- return { ok: true, envelope: await seal(key, "d2p", device.id, envelope.requestId, { ok: forgotten.ok }) };
315
+ return {
316
+ ok: true,
317
+ envelope: await seal(key, "d2p", device.id, envelope.requestId, {
318
+ ok: forgotten.ok,
319
+ }),
320
+ };
112
321
  }
113
322
  const result = await answer(request.call, device);
114
- return { ok: true, envelope: await seal(key, "d2p", device.id, envelope.requestId, result) };
323
+ return {
324
+ ok: true,
325
+ envelope: await seal(key, "d2p", device.id, envelope.requestId, result),
326
+ };
115
327
  };
116
328
 
117
329
  const settled = async (run) => {
@@ -119,37 +331,93 @@ export async function createPhoneDesk({ store, identityFile, boardCommand, reads
119
331
  const result = run();
120
332
  return result?.pending ? { ok: true, ...(await result.pending) } : result;
121
333
  } catch (error) {
122
- return { ok: false, error: `the board could not answer: ${error.message}` };
334
+ return {
335
+ ok: false,
336
+ error: `the board could not answer: ${error.message}`,
337
+ };
123
338
  }
124
339
  };
125
340
 
126
341
  const answer = async (callInput, device) => {
127
342
  const command = String(callInput?.command || "");
128
- if (command === "phone.hello") return { ok: true, hostname, version, kind: device.kind };
343
+ if (command === "phone.hello")
344
+ return { ok: true, hostname, version, kind: device.kind };
129
345
  if (device.kind === "browser") {
130
346
  if (BROWSER_READS.has(command)) {
131
347
  const read = reads[command];
132
- if (!read) return { ok: false, error: `this computer cannot answer ${command}` };
133
- try { return { ok: true, result: await read(callInput) }; } catch (error) { return { ok: false, error: error.message }; }
348
+ if (!read)
349
+ return { ok: false, error: `this computer cannot answer ${command}` };
350
+ try {
351
+ return { ok: true, result: await read(callInput) };
352
+ } catch (error) {
353
+ return { ok: false, error: error.message };
354
+ }
134
355
  }
135
356
  // The browser says whose call it is: the person's own action, or the chat owner's tool call.
136
357
  const as = callInput.as === "harness" ? "harness" : "user";
137
- if (!(as === "harness" ? HARNESS_COMMANDS : BROWSER_PERSON_COMMANDS).has(command)) return { ok: false, error: `a browser cannot call ${command || "that"} as ${as}` };
358
+ if (
359
+ !(as === "harness" ? HARNESS_COMMANDS : BROWSER_PERSON_COMMANDS).has(
360
+ command
361
+ )
362
+ )
363
+ return {
364
+ ok: false,
365
+ error: `a browser cannot call ${command || "that"} as ${as}`,
366
+ };
138
367
  // Words another agent wrote that drove Harness's call ride as a taint the board keeps.
139
- const writtenBy = Array.isArray(callInput.writtenBy) ? callInput.writtenBy.map(String).slice(0, 16) : [];
140
- if (as === "harness" && command === "board.goal.create" && writtenBy.length) return { ok: false, error: "a goal is set only in a turn the owner started" };
141
- const { as: _as, writtenBy: _written, provenance: _provenance, ...rest } = callInput;
142
- return settled(() => boardCommand({ ...rest, command, agent: as, ...(writtenBy.length ? { provenance: { trust: "agent", chain: writtenBy } } : {}) }));
368
+ const writtenBy = Array.isArray(callInput.writtenBy)
369
+ ? callInput.writtenBy.map(String).slice(0, 16)
370
+ : [];
371
+ if (
372
+ as === "harness" &&
373
+ command === "board.goal.create" &&
374
+ writtenBy.length
375
+ )
376
+ return {
377
+ ok: false,
378
+ error: "a goal is set only in a turn the owner started",
379
+ };
380
+ const {
381
+ as: _as,
382
+ writtenBy: _written,
383
+ provenance: _provenance,
384
+ ...rest
385
+ } = callInput;
386
+ return settled(() =>
387
+ boardCommand({
388
+ ...rest,
389
+ command,
390
+ agent: as,
391
+ ...(writtenBy.length
392
+ ? { provenance: { trust: "agent", chain: writtenBy } }
393
+ : {}),
394
+ })
395
+ );
143
396
  }
144
397
  // Lid-Closed Mode's wake: no arguments, never queued, and it can only keep the Mac awake and say how it is.
145
398
  if (command === "machine.wake") return { ok: true, machine: machine() };
146
- if (!PHONE_COMMANDS.has(command)) return { ok: false, error: `a phone cannot call ${command || "that"}` };
399
+ if (!PHONE_COMMANDS.has(command))
400
+ return { ok: false, error: `a phone cannot call ${command || "that"}` };
147
401
  // The phone is the person; whatever it names itself, it acts as "user".
148
- return settled(() => boardCommand({ ...callInput, command, agent: "user", ...(command === "board.list" && callInput.all === undefined ? { all: true } : {}) }));
402
+ return settled(() =>
403
+ boardCommand({
404
+ ...callInput,
405
+ command,
406
+ agent: "user",
407
+ ...(command === "board.list" && callInput.all === undefined
408
+ ? { all: true }
409
+ : {}),
410
+ })
411
+ );
149
412
  };
150
413
 
151
414
  // Harness's bridge message: a pairing or a sealed call. Everything returned is safe to pass through Harness.
152
- const handle = async (input) => (input?.kind === "pair" ? pair(input) : input?.kind === "call" ? call(input.envelope) : { ok: false, error: "unknown sealed request" });
415
+ const handle = async (input) =>
416
+ input?.kind === "pair"
417
+ ? pair(input)
418
+ : input?.kind === "call"
419
+ ? call(input.envelope)
420
+ : { ok: false, error: "unknown sealed request" };
153
421
 
154
422
  // The other way round: this computer asking the person's own Harness window for what only the window holds (its
155
423
  // screen history). The question is sealed to every browser linked here ("d2b") under one fresh request id; Harness
@@ -157,19 +425,65 @@ export async function createPhoneDesk({ store, identityFile, boardCommand, reads
157
425
  // links can open its envelope, and its answer comes back sealed ("b2d") for this request id alone. Harness sees which
158
426
  // devices, sizes and timing, never the question or the answer.
159
427
  const ask = async (call, carry) => {
160
- const browsers = store.phones().filter((device) => device.kind === "browser" && !device.revoked_at).slice(0, 8).map((device) => store.getPhone(device.id));
161
- if (!browsers.length) return { ok: false, code: "unpaired", error: "no Harness window is linked to this computer: the person links one with npx tryharness link" };
428
+ const browsers = store
429
+ .phones()
430
+ .filter((device) => device.kind === "browser" && !device.revoked_at)
431
+ .slice(0, 8)
432
+ .map((device) => store.getPhone(device.id));
433
+ if (!browsers.length)
434
+ return {
435
+ ok: false,
436
+ code: "unpaired",
437
+ error:
438
+ "no Harness window is linked to this computer: the person links one with npx tryharness link",
439
+ };
162
440
  const requestId = b64u(randomBytes(16));
163
441
  const ts = Date.now();
164
- const envelopes = await Promise.all(browsers.map(async (device) => seal(await keyFor(device), "d2b", device.id, requestId, { ts, call })));
442
+ const envelopes = await Promise.all(
443
+ browsers.map(async (device) =>
444
+ seal(await keyFor(device), "d2b", device.id, requestId, { ts, call })
445
+ )
446
+ );
165
447
  const reply = await carry(envelopes);
166
- if (reply?.window === "closed") return { ok: false, code: "closed", error: "the person's Harness window isn't open: ask them to open Harness, then try again" };
167
- if (reply?.window !== "open") return { ok: false, error: reply?.error || "Harness could not carry the question" };
168
- if (!reply.ok || !reply.envelope) return { ok: false, code: reply.code, error: reply.code === "window" ? `the open Harness window could not answer: ${reply.error}` : reply.error };
448
+ if (reply?.window === "closed")
449
+ return {
450
+ ok: false,
451
+ code: "closed",
452
+ error:
453
+ "the person's Harness window isn't open: ask them to open Harness, then try again",
454
+ };
455
+ if (reply?.window !== "open")
456
+ return {
457
+ ok: false,
458
+ error: reply?.error || "Harness could not carry the question",
459
+ };
460
+ if (!reply.ok || !reply.envelope)
461
+ return {
462
+ ok: false,
463
+ code: reply.code,
464
+ error:
465
+ reply.code === "window"
466
+ ? `the open Harness window could not answer: ${reply.error}`
467
+ : reply.error,
468
+ };
169
469
  const device = browsers.find((item) => item.id === reply.envelope.deviceId);
170
- if (!device || reply.envelope.requestId !== requestId) return { ok: false, code: "unreadable", error: "the answer was for another question" };
470
+ if (!device || reply.envelope.requestId !== requestId)
471
+ return {
472
+ ok: false,
473
+ code: "unreadable",
474
+ error: "the answer was for another question",
475
+ };
171
476
  let answer;
172
- try { answer = await open(await keyFor(device), "b2d", reply.envelope); } catch { return { ok: false, code: "unreadable", error: "the answer could not be opened: it did not come from a window linked to this computer" }; }
477
+ try {
478
+ answer = await open(await keyFor(device), "b2d", reply.envelope);
479
+ } catch {
480
+ return {
481
+ ok: false,
482
+ code: "unreadable",
483
+ error:
484
+ "the answer could not be opened: it did not come from a window linked to this computer",
485
+ };
486
+ }
173
487
  store.touchPhone(device.id);
174
488
  return answer;
175
489
  };
@@ -179,5 +493,13 @@ export async function createPhoneDesk({ store, identityFile, boardCommand, reads
179
493
  return store.revokePhone(String(id));
180
494
  };
181
495
 
182
- return { offer, handle, ask, revoke, list: () => store.phones(), publicKey: identity.publicKey, identity };
496
+ return {
497
+ offer,
498
+ handle,
499
+ ask,
500
+ revoke,
501
+ list: () => store.phones(),
502
+ publicKey: identity.publicKey,
503
+ identity,
504
+ };
183
505
  }