@parlor/agent-host 0.0.0-stage → 0.1.0

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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -2
  3. package/dist/acp.d.ts +165 -0
  4. package/dist/acp.d.ts.map +1 -0
  5. package/dist/acp.js +626 -0
  6. package/dist/acp.js.map +1 -0
  7. package/dist/adapter-launch.d.ts +7 -0
  8. package/dist/adapter-launch.d.ts.map +1 -0
  9. package/dist/adapter-launch.js +35 -0
  10. package/dist/adapter-launch.js.map +1 -0
  11. package/dist/agents.d.ts +32 -0
  12. package/dist/agents.d.ts.map +1 -0
  13. package/dist/agents.js +132 -0
  14. package/dist/agents.js.map +1 -0
  15. package/dist/assertion.d.ts +9 -0
  16. package/dist/assertion.d.ts.map +1 -0
  17. package/dist/assertion.js +22 -0
  18. package/dist/assertion.js.map +1 -0
  19. package/dist/control.d.ts +106 -0
  20. package/dist/control.d.ts.map +1 -0
  21. package/dist/control.js +32 -0
  22. package/dist/control.js.map +1 -0
  23. package/dist/daemon.d.ts +50 -0
  24. package/dist/daemon.d.ts.map +1 -0
  25. package/dist/daemon.js +543 -0
  26. package/dist/daemon.js.map +1 -0
  27. package/dist/dev-host.d.ts +5 -0
  28. package/dist/dev-host.d.ts.map +1 -0
  29. package/dist/dev-host.js +29 -0
  30. package/dist/dev-host.js.map +1 -0
  31. package/dist/device-list.d.ts +6 -0
  32. package/dist/device-list.d.ts.map +1 -0
  33. package/dist/device-list.js +30 -0
  34. package/dist/device-list.js.map +1 -0
  35. package/dist/embedded.d.ts +6 -0
  36. package/dist/embedded.d.ts.map +1 -0
  37. package/dist/embedded.js +3 -0
  38. package/dist/embedded.js.map +1 -0
  39. package/dist/fold.d.ts +38 -0
  40. package/dist/fold.d.ts.map +1 -0
  41. package/dist/fold.js +165 -0
  42. package/dist/fold.js.map +1 -0
  43. package/dist/grants.d.ts +54 -0
  44. package/dist/grants.d.ts.map +1 -0
  45. package/dist/grants.js +157 -0
  46. package/dist/grants.js.map +1 -0
  47. package/dist/index.d.ts +17 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +17 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/json-file.d.ts +6 -0
  52. package/dist/json-file.d.ts.map +1 -0
  53. package/dist/json-file.js +35 -0
  54. package/dist/json-file.js.map +1 -0
  55. package/dist/lines.d.ts +5 -0
  56. package/dist/lines.d.ts.map +1 -0
  57. package/dist/lines.js +9 -0
  58. package/dist/lines.js.map +1 -0
  59. package/dist/link-device.d.ts +28 -0
  60. package/dist/link-device.d.ts.map +1 -0
  61. package/dist/link-device.js +40 -0
  62. package/dist/link-device.js.map +1 -0
  63. package/dist/link.d.ts +52 -0
  64. package/dist/link.d.ts.map +1 -0
  65. package/dist/link.js +277 -0
  66. package/dist/link.js.map +1 -0
  67. package/dist/lock.d.ts +7 -0
  68. package/dist/lock.d.ts.map +1 -0
  69. package/dist/lock.js +105 -0
  70. package/dist/lock.js.map +1 -0
  71. package/dist/log.d.ts +3 -0
  72. package/dist/log.d.ts.map +1 -0
  73. package/dist/log.js +18 -0
  74. package/dist/log.js.map +1 -0
  75. package/dist/main.d.ts +3 -0
  76. package/dist/main.d.ts.map +1 -0
  77. package/dist/main.js +520 -0
  78. package/dist/main.js.map +1 -0
  79. package/dist/mcp-proxy.d.ts +33 -0
  80. package/dist/mcp-proxy.d.ts.map +1 -0
  81. package/dist/mcp-proxy.js +317 -0
  82. package/dist/mcp-proxy.js.map +1 -0
  83. package/dist/naming.d.ts +7 -0
  84. package/dist/naming.d.ts.map +1 -0
  85. package/dist/naming.js +42 -0
  86. package/dist/naming.js.map +1 -0
  87. package/dist/pairing.d.ts +68 -0
  88. package/dist/pairing.d.ts.map +1 -0
  89. package/dist/pairing.js +312 -0
  90. package/dist/pairing.js.map +1 -0
  91. package/dist/paths.d.ts +19 -0
  92. package/dist/paths.d.ts.map +1 -0
  93. package/dist/paths.js +23 -0
  94. package/dist/paths.js.map +1 -0
  95. package/dist/policy.d.ts +37 -0
  96. package/dist/policy.d.ts.map +1 -0
  97. package/dist/policy.js +60 -0
  98. package/dist/policy.js.map +1 -0
  99. package/dist/pool.d.ts +34 -0
  100. package/dist/pool.d.ts.map +1 -0
  101. package/dist/pool.js +112 -0
  102. package/dist/pool.js.map +1 -0
  103. package/dist/prompter.d.ts +33 -0
  104. package/dist/prompter.d.ts.map +1 -0
  105. package/dist/prompter.js +98 -0
  106. package/dist/prompter.js.map +1 -0
  107. package/dist/release-main.d.ts +2 -0
  108. package/dist/release-main.d.ts.map +1 -0
  109. package/dist/release-main.js +50 -0
  110. package/dist/release-main.js.map +1 -0
  111. package/dist/release.d.ts +22 -0
  112. package/dist/release.d.ts.map +1 -0
  113. package/dist/release.js +81 -0
  114. package/dist/release.js.map +1 -0
  115. package/dist/revoke.d.ts +10 -0
  116. package/dist/revoke.d.ts.map +1 -0
  117. package/dist/revoke.js +23 -0
  118. package/dist/revoke.js.map +1 -0
  119. package/dist/self.d.ts +7 -0
  120. package/dist/self.d.ts.map +1 -0
  121. package/dist/self.js +10 -0
  122. package/dist/self.js.map +1 -0
  123. package/dist/service.d.ts +97 -0
  124. package/dist/service.d.ts.map +1 -0
  125. package/dist/service.js +205 -0
  126. package/dist/service.js.map +1 -0
  127. package/dist/shell-path.d.ts +10 -0
  128. package/dist/shell-path.d.ts.map +1 -0
  129. package/dist/shell-path.js +64 -0
  130. package/dist/shell-path.js.map +1 -0
  131. package/dist/skeleton-kit.d.ts +8 -0
  132. package/dist/skeleton-kit.d.ts.map +1 -0
  133. package/dist/skeleton-kit.js +143 -0
  134. package/dist/skeleton-kit.js.map +1 -0
  135. package/dist/spec-kit.d.ts +102 -0
  136. package/dist/spec-kit.d.ts.map +1 -0
  137. package/dist/spec-kit.js +372 -0
  138. package/dist/spec-kit.js.map +1 -0
  139. package/dist/testing/fake-agent.d.ts +65 -0
  140. package/dist/testing/fake-agent.d.ts.map +1 -0
  141. package/dist/testing/fake-agent.js +238 -0
  142. package/dist/testing/fake-agent.js.map +1 -0
  143. package/dist/trust.d.ts +20 -0
  144. package/dist/trust.d.ts.map +1 -0
  145. package/dist/trust.js +27 -0
  146. package/dist/trust.js.map +1 -0
  147. package/dist/turn.d.ts +32 -0
  148. package/dist/turn.d.ts.map +1 -0
  149. package/dist/turn.js +650 -0
  150. package/dist/turn.js.map +1 -0
  151. package/dist/version.d.ts +2 -0
  152. package/dist/version.d.ts.map +1 -0
  153. package/dist/version.js +6 -0
  154. package/dist/version.js.map +1 -0
  155. package/package.json +57 -3
package/dist/turn.js ADDED
@@ -0,0 +1,650 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { answerStatement, checkPairingProof, deviceCommitment, vouchStatement, signedPrompt, statementBytes, verifyStatement, } from "@parlor/auth";
3
+ import { canonicalBytes, encodeBase64url } from "@parlor/capabilities/encoding";
4
+ import { ActionError, asId } from "@parlor/core";
5
+ import { SessionRefused, } from "./acp.js";
6
+ import { finishFold, foldUpdate, startFold } from "./fold.js";
7
+ import { logEvent } from "./log.js";
8
+ import { appServerOf, computerName } from "./naming.js";
9
+ import { approvalFor, isAppTool, presenceDueAt } from "./policy.js";
10
+ import { agentEnvironment } from "./shell-path.js";
11
+ export const PROMPT_MAX_AGE_MS = 10 * 60_000;
12
+ /** A vouch is bound to one device's keys, so it may wait for that device's first prompt. */
13
+ const VOUCH_MAX_AGE_MS = 30 * 24 * 60 * 60_000;
14
+ const SIGNAL_POLL_MS = 500;
15
+ const MAX_APPEND_ITEMS = 64;
16
+ const APPEND_RETRY_BASE_MS = 250;
17
+ const APPEND_RETRY_MAX_MS = 4_000;
18
+ const MIN_LEASE_MS = 1_000;
19
+ const CANCEL_SETTLE_MS = 5_000;
20
+ const QUESTION_CHOICES = [
21
+ { id: "accept", label: "Submit", effect: "allow_once" },
22
+ { id: "decline", label: "Decline", effect: "reject_once" },
23
+ ];
24
+ class AgentExited extends Error {
25
+ exit;
26
+ name = "AgentExited";
27
+ constructor(exit) {
28
+ super("The agent exited.");
29
+ this.exit = exit;
30
+ }
31
+ }
32
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
33
+ class LeaseLapsed extends Error {
34
+ name = "LeaseLapsed";
35
+ }
36
+ function lapseAfter(ms) {
37
+ let timer;
38
+ const lapsed = new Promise((_, reject) => {
39
+ timer = setTimeout(() => reject(new LeaseLapsed("The turn's lease lapsed.")), ms);
40
+ });
41
+ lapsed.catch(() => undefined);
42
+ return { lapsed, cancel: () => clearTimeout(timer) };
43
+ }
44
+ async function settlesWithin(work, ms) {
45
+ let timer;
46
+ const late = new Promise((resolve) => {
47
+ timer = setTimeout(() => resolve(false), ms);
48
+ });
49
+ try {
50
+ return await Promise.race([work.then(() => true), late]);
51
+ }
52
+ finally {
53
+ clearTimeout(timer);
54
+ }
55
+ }
56
+ function promptMaxAgeMs() {
57
+ const lowered = Number(process.env.PARLOR_HOST_PROMPT_MAX_AGE_MS);
58
+ return lowered > 0 ? Math.min(lowered, PROMPT_MAX_AGE_MS) : PROMPT_MAX_AGE_MS;
59
+ }
60
+ const hex = async (bytes) => [...new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(bytes)))]
61
+ .map((byte) => byte.toString(16).padStart(2, "0"))
62
+ .join("");
63
+ const fingerprint = (device) => encodeBase64url(canonicalBytes({ signing: device.signingKey, presence: device.presenceKey ?? null }));
64
+ function exitDetail(exit) {
65
+ if (exit.kind === "killed")
66
+ return "stopped by this computer";
67
+ const last = exit.stderrTail.trim().split("\n").at(-1)?.slice(0, 300);
68
+ return [`exit code ${exit.code ?? "none"}`, last].filter(Boolean).join(": ");
69
+ }
70
+ function askBody(ask, approval) {
71
+ const body = ask.kind === "permission"
72
+ ? {
73
+ question: ask.title,
74
+ choices: ask.options.map((option) => ({ ...option })),
75
+ }
76
+ : {
77
+ question: ask.message,
78
+ choices: QUESTION_CHOICES.map((choice) => ({ ...choice })),
79
+ form: { ...ask.schema },
80
+ };
81
+ return approval ? { ...body, approval } : body;
82
+ }
83
+ function answerOf(pending, body) {
84
+ if (pending.ask.kind === "question") {
85
+ if (body.choice === "accept")
86
+ return { kind: "values", values: { ...body.values } };
87
+ return { kind: "declined" };
88
+ }
89
+ if (body.choice === "declined")
90
+ return { kind: "declined" };
91
+ return pending.ask.options.some((option) => option.id === body.choice)
92
+ ? { kind: "selected", optionId: body.choice }
93
+ : undefined;
94
+ }
95
+ /**
96
+ * Runs one claimed turn to its outcome and reports it with `hostFinish`: where the
97
+ * claimed prompt, the agent's updates, the user's answers and stop, and the local policy meet.
98
+ */
99
+ export async function runTurn(claimed, deps) {
100
+ const { link, pool } = deps;
101
+ const { calls } = link.api;
102
+ const grant = link.grant;
103
+ const origin = grant.appOrigin;
104
+ const appServer = appServerOf(grant.appName);
105
+ const agentId = claimed.agent.agentId;
106
+ const threadId = claimed.id;
107
+ const turnId = claimed.turnId;
108
+ let fold = startFold(turnId);
109
+ let proxy;
110
+ let lost = false;
111
+ const lostOver = new AbortController();
112
+ const markLost = () => {
113
+ lost = true;
114
+ lostOver.abort();
115
+ };
116
+ const lostPromise = new Promise((resolve) => lostOver.signal.addEventListener("abort", () => resolve("lost"), { once: true }));
117
+ const leaseMs = Math.max(MIN_LEASE_MS, claimed.leaseUntil - deps.now());
118
+ let cancelled = false;
119
+ let lastWrite = deps.now();
120
+ let appending = Promise.resolve();
121
+ const pending = new Map();
122
+ const settle = (id, answer) => {
123
+ const waiting = pending.get(id);
124
+ if (!waiting)
125
+ return;
126
+ pending.delete(id);
127
+ waiting.dialog.abort();
128
+ waiting.resolve(answer);
129
+ };
130
+ const settleAll = () => {
131
+ for (const id of [...pending.keys()])
132
+ settle(id, { kind: "cancelled" });
133
+ };
134
+ const nextId = () => {
135
+ const id = asId("item", `${turnId}.${fold.nextId}`);
136
+ fold = { ...fold, nextId: fold.nextId + 1 };
137
+ return id;
138
+ };
139
+ const untilLeaseLapses = async (call) => {
140
+ const lease = lapseAfter(leaseMs);
141
+ try {
142
+ for (let attempt = 0;; attempt += 1) {
143
+ try {
144
+ return await Promise.race([call(), lease.lapsed]);
145
+ }
146
+ catch (error) {
147
+ if (error instanceof ActionError || error instanceof LeaseLapsed)
148
+ throw error;
149
+ await Promise.race([
150
+ sleep(Math.min(APPEND_RETRY_BASE_MS * 2 ** attempt, APPEND_RETRY_MAX_MS)),
151
+ lease.lapsed,
152
+ ]);
153
+ }
154
+ }
155
+ }
156
+ finally {
157
+ lease.cancel();
158
+ }
159
+ };
160
+ const send = async (items) => {
161
+ const renewed = await untilLeaseLapses(() => calls.call("hostAppend", { turnId, items }));
162
+ lastWrite = deps.now();
163
+ proxy?.renew(renewed.token);
164
+ logEvent("host_append", { turnId, itemIds: items.map((item) => item.id) });
165
+ };
166
+ const write = (items) => {
167
+ const batch = [...items];
168
+ appending = appending.then(async () => {
169
+ if (lost)
170
+ return;
171
+ try {
172
+ if (batch.length === 0) {
173
+ await send([]);
174
+ return;
175
+ }
176
+ for (let at = 0; at < batch.length; at += MAX_APPEND_ITEMS) {
177
+ await send(batch.slice(at, at + MAX_APPEND_ITEMS));
178
+ }
179
+ }
180
+ catch {
181
+ markLost();
182
+ }
183
+ });
184
+ return appending;
185
+ };
186
+ const event = (name, detail, itemId) => write([
187
+ {
188
+ id: nextId(),
189
+ kind: "event",
190
+ body: {
191
+ event: name,
192
+ ...(detail ? { detail: detail.slice(0, 500) } : {}),
193
+ ...(itemId ? { itemId } : {}),
194
+ },
195
+ },
196
+ ]);
197
+ const finish = async (outcome) => {
198
+ await appending;
199
+ proxy?.end();
200
+ if (lost)
201
+ return { state: "failed", reason: "host_lost" };
202
+ try {
203
+ await untilLeaseLapses(() => calls.call("hostFinish", { turnId, outcome }));
204
+ }
205
+ catch (error) {
206
+ if (error instanceof LeaseLapsed)
207
+ return { state: "failed", reason: "host_lost" };
208
+ if (!(error instanceof ActionError && error.code === "forbidden"))
209
+ throw error;
210
+ }
211
+ return outcome;
212
+ };
213
+ const fail = async (reason, name, detail) => {
214
+ await event(name, detail);
215
+ return finish({ state: "failed", reason });
216
+ };
217
+ const verifyPrompt = async (signed, pinned, requirePresence) => {
218
+ const outcome = await verifyStatement(signed, pinned, {
219
+ requirePresence,
220
+ now: deps.now(),
221
+ maxAgeMs: promptMaxAgeMs(),
222
+ });
223
+ logEvent("statement_verified", {
224
+ kind: "prompt",
225
+ deviceId: pinned.deviceId,
226
+ outcome,
227
+ sha256: await hex(statementBytes(signed.statement)),
228
+ });
229
+ return outcome;
230
+ };
231
+ const refuse = async (reason, name, detail) => ({ outcome: await fail(reason, name, detail) });
232
+ /** A vouch counts when a device pinned here signed it with presence, for exactly these keys. */
233
+ const vouchedFor = async (keys) => {
234
+ if (!keys.vouches?.length)
235
+ return undefined;
236
+ const commitment = await deviceCommitment(origin, keys.id, keys.signingKey, keys.presenceKey?.publicKey);
237
+ for (const vouch of keys.vouches) {
238
+ if (vouch.commitment !== commitment)
239
+ continue;
240
+ const voucher = await deps.trust.get(origin, vouch.voucher);
241
+ if (!voucher?.presenceKey)
242
+ continue;
243
+ const outcome = await verifyStatement({
244
+ statement: vouchStatement(origin, vouch.voucher, keys.id, commitment, vouch.at),
245
+ deviceId: voucher.deviceId,
246
+ signature: vouch.signature,
247
+ }, voucher, { requirePresence: true, now: deps.now(), maxAgeMs: VOUCH_MAX_AGE_MS });
248
+ logEvent("vouch_verified", { deviceId: keys.id, voucher: vouch.voucher, outcome });
249
+ if (outcome !== "valid")
250
+ continue;
251
+ return {
252
+ deviceId: keys.id,
253
+ signingKey: keys.signingKey,
254
+ presenceKey: keys.presenceKey,
255
+ name: keys.name,
256
+ platform: keys.platform,
257
+ approvedAt: deps.now(),
258
+ via: "vouched",
259
+ voucher: vouch.voucher,
260
+ };
261
+ }
262
+ return undefined;
263
+ };
264
+ const pinVouched = async (device) => {
265
+ await deps.trust.pin(origin, device);
266
+ logEvent("device_trusted", { deviceId: device.deviceId, via: "vouched" });
267
+ deps.prompter.deviceLinked?.({
268
+ appName: grant.appName,
269
+ device: device.name ?? "A device",
270
+ });
271
+ };
272
+ /** Moves the device's last presence check forward; reported so its devices know when the next is due. */
273
+ const confirmedPresence = async (device, at) => {
274
+ if (at <= (device.presenceAt ?? device.approvedAt))
275
+ return;
276
+ await deps.trust.pin(origin, { ...device, presenceAt: at });
277
+ };
278
+ const unlinked = `Link this device to use your agents on ${computerName()}: open the Agent sidebar on it.`;
279
+ const admit = async () => {
280
+ const signed = await signedPrompt(origin, claimed.prompt);
281
+ if (!signed) {
282
+ return refuse("device_rejected", "turn_failed", `Update ${grant.appName} to send messages to ${computerName()}: this version does not sign them.`);
283
+ }
284
+ const known = await deps.trust.get(origin, signed.deviceId);
285
+ const keys = claimed.device;
286
+ if (known && !keys) {
287
+ await deps.trust.forget(origin, known.deviceId);
288
+ return refuse("device_rejected", "device_rejected", "This device is no longer registered.");
289
+ }
290
+ if (!keys || keys.id !== signed.deviceId) {
291
+ return refuse("signature_invalid", "turn_failed", "The account has no such device.");
292
+ }
293
+ if (known && fingerprint(known) !== fingerprint(keys)) {
294
+ return refuse("signature_invalid", "turn_failed", "This device's keys changed.");
295
+ }
296
+ const pinned = known ?? {
297
+ deviceId: keys.id,
298
+ signingKey: keys.signingKey,
299
+ presenceKey: keys.presenceKey,
300
+ };
301
+ const due = known && presenceDueAt(deps.policy, known);
302
+ const outcome = await verifyPrompt(signed, pinned, due !== undefined && signed.statement.at > due);
303
+ if (outcome === "stale") {
304
+ return { outcome: await finish({ state: "failed", reason: "prompt_expired" }) };
305
+ }
306
+ if (outcome === "presence_required") {
307
+ return refuse("device_rejected", "turn_failed", `Confirm it's you to keep using your agents on ${computerName()} from this device: send again and use Face ID or Touch ID.`);
308
+ }
309
+ if (outcome !== "valid") {
310
+ return refuse("signature_invalid", "turn_failed", "This prompt's signature did not check out.");
311
+ }
312
+ if (known) {
313
+ if (signed.signature.presence === "user") {
314
+ await confirmedPresence(known, signed.statement.at);
315
+ }
316
+ return { device: known };
317
+ }
318
+ const proof = claimed.prompt.body.signed?.pairing;
319
+ const secret = proof ? await deps.pairing?.secret() : undefined;
320
+ if (proof &&
321
+ secret &&
322
+ (await checkPairingProof(secret, proof, keys.id, keys.signingKey))) {
323
+ const device = {
324
+ ...pinned,
325
+ name: keys.name,
326
+ platform: keys.platform,
327
+ approvedAt: deps.now(),
328
+ via: "pairing",
329
+ };
330
+ await deps.trust.pin(origin, device);
331
+ await deps.pairing?.forget();
332
+ logEvent("device_trusted", { deviceId: keys.id, via: "pairing" });
333
+ return { device };
334
+ }
335
+ const vouched = await vouchedFor(keys);
336
+ if (vouched) {
337
+ await pinVouched(vouched);
338
+ return { device: vouched };
339
+ }
340
+ return refuse("device_rejected", "device_rejected", unlinked);
341
+ };
342
+ const proceed = async (device) => {
343
+ const profile = deps.profiles.find((candidate) => candidate.id === agentId);
344
+ if (!profile) {
345
+ return fail("agent_unavailable", "turn_failed", `This computer has no agent "${agentId}".`);
346
+ }
347
+ let live = pool.session(grant, threadId, agentId);
348
+ let connection = pool.process(origin, agentId);
349
+ const fresh = live === undefined;
350
+ if (live) {
351
+ proxy = live.proxy;
352
+ proxy.begin(claimed.token);
353
+ }
354
+ else {
355
+ const switched = deps.profiles.some((other) => other.id !== agentId && pool.session(grant, threadId, other.id) !== undefined);
356
+ if (switched)
357
+ await event("agent_switched", profile.name);
358
+ try {
359
+ connection = await pool.launch(origin, agentId, async (signal) => {
360
+ await mkdir(grant.workspace, { recursive: true, mode: 0o700 });
361
+ return deps.launcher.launch(profile, {
362
+ cwd: grant.workspace,
363
+ env: await agentEnvironment(),
364
+ signal,
365
+ });
366
+ });
367
+ proxy = deps.proxy.open(appServer, origin);
368
+ proxy.begin(claimed.token);
369
+ const session = await connection.openSession({
370
+ cwd: grant.workspace,
371
+ mcpServers: [proxy.server],
372
+ resume: await pool.resumable(grant, threadId, agentId),
373
+ pinAskMode: true,
374
+ strictAsk: deps.policy.answers !== "off",
375
+ });
376
+ live = { session, proxy };
377
+ pool.remember(grant, threadId, agentId, live);
378
+ }
379
+ catch (error) {
380
+ proxy?.dispose();
381
+ proxy = undefined;
382
+ if (error instanceof SessionRefused && error.leftOpen) {
383
+ await pool.drop(origin, agentId);
384
+ }
385
+ const message = error instanceof Error ? error.message : String(error);
386
+ return fail("agent_unavailable", "turn_failed", message);
387
+ }
388
+ }
389
+ const { session } = live;
390
+ const exited = connection.exited.then((exit) => {
391
+ throw new AgentExited(exit);
392
+ });
393
+ exited.catch(() => undefined);
394
+ const text = claimed.prompt.body.text;
395
+ const blocks = [
396
+ {
397
+ kind: "text",
398
+ text: fresh && !session.resumed ? `${claimed.context}\n\n${text}` : text,
399
+ },
400
+ ];
401
+ const fromThisComputer = grant.dev === true || device?.via === "pairing";
402
+ if (!fromThisComputer) {
403
+ deps.prompter.turnStarted({
404
+ appName: grant.appName,
405
+ device: device?.name ?? claimed.device?.name ?? "the app",
406
+ agent: profile.name,
407
+ preview: text.slice(0, 80),
408
+ });
409
+ }
410
+ const turnOver = new AbortController();
411
+ const cancel = () => {
412
+ if (cancelled)
413
+ return;
414
+ cancelled = true;
415
+ void session.cancel();
416
+ settleAll();
417
+ turnOver.abort();
418
+ };
419
+ const ask = async (question) => {
420
+ if (cancelled)
421
+ return { kind: "cancelled" };
422
+ if (question.kind === "permission" && isAppTool(question, appServer)) {
423
+ const allow = question.options.find((option) => option.effect === "allow_once");
424
+ if (allow)
425
+ return { kind: "selected", optionId: allow.id };
426
+ }
427
+ const wanted = approvalFor(deps.policy, question);
428
+ const approval = wanted === "presence" && device && !device.presenceKey ? "computer" : wanted;
429
+ const id = nextId();
430
+ const dialog = new AbortController();
431
+ const answered = new Promise((resolve) => pending.set(id, { ask: question, approval, dialog, resolve }));
432
+ await write([{ id, kind: "ask", body: askBody(question, approval) }]);
433
+ if (approval === "computer") {
434
+ const choices = question.kind === "permission" ? question.options : QUESTION_CHOICES;
435
+ void deps.prompter
436
+ .answerHere({
437
+ appName: grant.appName,
438
+ agent: profile.name,
439
+ question: question.kind === "permission" ? question.title : question.message,
440
+ choices,
441
+ signal: dialog.signal,
442
+ })
443
+ .then((choice) => {
444
+ const waiting = pending.get(id);
445
+ if (!waiting)
446
+ return;
447
+ const answer = choice === undefined ? undefined : answerOf(waiting, { askId: id, choice });
448
+ if (choice !== undefined && answer) {
449
+ void write([
450
+ {
451
+ id: nextId(),
452
+ kind: "event",
453
+ body: { event: "answered_on_computer", itemId: id, choice },
454
+ },
455
+ ]);
456
+ }
457
+ settle(id, answer ?? { kind: "cancelled" });
458
+ });
459
+ }
460
+ return answered;
461
+ };
462
+ const rejected = (reason) => ({ kind: "rejected", reason });
463
+ const verdictOf = async (waiting, item, devices) => {
464
+ if (waiting.approval === "computer") {
465
+ return rejected("This question is answered on the computer.");
466
+ }
467
+ if (deps.policy.deviceApproval !== "local" && waiting.approval !== "presence") {
468
+ return { kind: "accepted" };
469
+ }
470
+ const { signed, askId, choice, values } = item.body;
471
+ const pinned = signed ? await deps.trust.get(origin, signed.deviceId) : undefined;
472
+ const listed = signed
473
+ ? devices.find((keys) => keys.id === signed.deviceId)
474
+ : undefined;
475
+ const known = pinned ??
476
+ (listed && {
477
+ deviceId: listed.id,
478
+ signingKey: listed.signingKey,
479
+ presenceKey: listed.presenceKey,
480
+ });
481
+ if (!signed || !known) {
482
+ logEvent("statement_verified", {
483
+ kind: "answer",
484
+ deviceId: signed?.deviceId,
485
+ outcome: "invalid",
486
+ });
487
+ return rejected("This device cannot answer here.");
488
+ }
489
+ const statement = await answerStatement(origin, { threadId: item.threadId, askId, choice, ...(values ? { values } : {}) }, signed.at);
490
+ const outcome = await verifyStatement({ statement, deviceId: signed.deviceId, signature: signed.signature }, known, {
491
+ requirePresence: waiting.approval === "presence",
492
+ now: deps.now(),
493
+ maxAgeMs: PROMPT_MAX_AGE_MS,
494
+ });
495
+ logEvent("statement_verified", {
496
+ kind: "answer",
497
+ deviceId: signed.deviceId,
498
+ outcome,
499
+ sha256: await hex(statementBytes(statement)),
500
+ });
501
+ if (outcome === "presence_required") {
502
+ return rejected("This answer needs a presence check.");
503
+ }
504
+ if (outcome !== "valid")
505
+ return rejected("This answer's signature did not check out.");
506
+ if (pinned && signed.signature.presence === "user") {
507
+ await confirmedPresence(pinned, signed.at);
508
+ }
509
+ return pinned || !listed
510
+ ? { kind: "accepted" }
511
+ : { kind: "unpinned", device: listed };
512
+ };
513
+ const deliver = (waiting, item, verdict) => {
514
+ const answer = verdict.kind === "accepted" ? answerOf(waiting, item.body) : undefined;
515
+ if (!answer) {
516
+ void event("answer_rejected", verdict.kind === "rejected"
517
+ ? verdict.reason
518
+ : "That is not one of the choices.", item.id);
519
+ return;
520
+ }
521
+ settle(item.body.askId, answer);
522
+ };
523
+ const admitAnswer = async (waiting, item, devices) => {
524
+ const verdict = await verdictOf(waiting, item, devices);
525
+ if (verdict.kind !== "unpinned") {
526
+ deliver(waiting, item, verdict);
527
+ return;
528
+ }
529
+ const vouched = await vouchedFor(verdict.device);
530
+ if (!vouched) {
531
+ deliver(waiting, item, rejected(unlinked));
532
+ return;
533
+ }
534
+ await pinVouched(vouched);
535
+ deliver(waiting, item, { kind: "accepted" });
536
+ };
537
+ let afterSeq = claimed.prompt.seq;
538
+ const signalsOver = new AbortController();
539
+ const signalsEnded = new Promise((resolve) => signalsOver.signal.addEventListener("abort", () => resolve("ended"), {
540
+ once: true,
541
+ }));
542
+ const signals = async () => {
543
+ for (;;) {
544
+ if ((await Promise.race([sleep(SIGNAL_POLL_MS), signalsEnded])) === "ended")
545
+ return;
546
+ let batches;
547
+ try {
548
+ const fetched = await Promise.race([
549
+ calls.call("hostSignals", { turns: [{ turnId, afterSeq }] }),
550
+ signalsEnded,
551
+ ]);
552
+ if (fetched === "ended")
553
+ return;
554
+ batches = fetched;
555
+ }
556
+ catch {
557
+ continue;
558
+ }
559
+ for (const batch of batches) {
560
+ for (const item of batch.items) {
561
+ if (signalsOver.signal.aborted)
562
+ return;
563
+ afterSeq = Math.max(afterSeq, item.seq);
564
+ if (item.kind === "cancel") {
565
+ cancel();
566
+ continue;
567
+ }
568
+ const waiting = pending.get(item.body.askId);
569
+ if (waiting)
570
+ await admitAnswer(waiting, item, batch.devices ?? []);
571
+ }
572
+ }
573
+ }
574
+ };
575
+ let stop;
576
+ let failure;
577
+ const prompted = session.prompt(blocks, {
578
+ update: (update) => {
579
+ const step = foldUpdate(fold, update, appServer);
580
+ fold = step.fold;
581
+ if (step.frames.length > 0)
582
+ link.api.stream({ turnId, frames: step.frames });
583
+ if (step.closed.length > 0)
584
+ void write(step.closed);
585
+ },
586
+ ask,
587
+ });
588
+ prompted.catch(() => undefined);
589
+ const signalsRan = signals().catch(() => undefined);
590
+ try {
591
+ stop = await Promise.race([prompted, exited, lostPromise]);
592
+ if (stop === "lost") {
593
+ signalsOver.abort();
594
+ proxy?.end();
595
+ const settled = await settlesWithin(Promise.allSettled([session.cancel(), prompted]), CANCEL_SETTLE_MS);
596
+ if (!settled)
597
+ await pool.drop(origin, agentId);
598
+ }
599
+ }
600
+ catch (error) {
601
+ failure =
602
+ error instanceof AgentExited
603
+ ? error
604
+ : await Promise.race([
605
+ exited.catch((exit) => exit),
606
+ sleep(1_000).then(() => error instanceof Error ? error : new Error(String(error))),
607
+ ]);
608
+ }
609
+ finally {
610
+ signalsOver.abort();
611
+ settleAll();
612
+ turnOver.abort();
613
+ }
614
+ await signalsRan;
615
+ const stopped = failure !== undefined || cancelled || stop === "cancelled";
616
+ const final = finishFold(fold, stopped);
617
+ fold = final.fold;
618
+ void write(final.closed);
619
+ if (failure) {
620
+ await pool.drop(origin, agentId);
621
+ const detail = failure instanceof AgentExited ? exitDetail(failure.exit) : failure.message;
622
+ return fail("agent_exited", "agent_exited", detail);
623
+ }
624
+ if (lost) {
625
+ proxy?.end();
626
+ return { state: "failed", reason: "host_lost" };
627
+ }
628
+ return finish(stopped
629
+ ? { state: "cancelled" }
630
+ : { state: "done", stopReason: stop });
631
+ };
632
+ const renewEveryMs = Math.max(1_000, (claimed.leaseUntil - deps.now()) / 2);
633
+ const renewal = setInterval(() => {
634
+ if (deps.now() - lastWrite >= renewEveryMs)
635
+ void write([]);
636
+ }, renewEveryMs / 4);
637
+ try {
638
+ if (deps.policy.deviceApproval === "local") {
639
+ const admission = await admit();
640
+ if ("outcome" in admission)
641
+ return admission.outcome;
642
+ return await proceed(admission.device);
643
+ }
644
+ return await proceed(undefined);
645
+ }
646
+ finally {
647
+ clearInterval(renewal);
648
+ }
649
+ }
650
+ //# sourceMappingURL=turn.js.map