pinomad 0.3.0 → 0.4.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.
- package/dist/{chunk-n60kkf1m.js → chunk-17p6bfvy.js} +4 -4
- package/dist/chunk-302y6by9.js +50 -0
- package/dist/{chunk-bggesvnj.js → chunk-374zqx5h.js} +15 -92
- package/dist/chunk-5785kwej.js +128 -0
- package/dist/{chunk-z4x88s0q.js → chunk-7gmkgyaz.js} +76 -3
- package/dist/chunk-837gh8nr.js +138 -0
- package/dist/{chunk-t9y2wfk5.js → chunk-akgxwg8f.js} +98 -6
- package/dist/chunk-b3f0ced1.js +147 -0
- package/dist/{chunk-syf0z68f.js → chunk-d4y99pfy.js} +2 -2
- package/dist/{chunk-3tf23vjq.js → chunk-fgx2sh7d.js} +1 -1
- package/dist/chunk-knaw1a9g.js +182 -0
- package/dist/{chunk-c155p0t0.js → chunk-myjp910s.js} +106 -7
- package/dist/{chunk-3qn8gbjb.js → chunk-n73t5xzw.js} +1 -1
- package/dist/{chunk-kxws8b83.js → chunk-prhvksh3.js} +4 -4
- package/dist/chunk-vw5yb5pd.js +1123 -0
- package/dist/chunk-xhwkx9bd.js +139 -0
- package/dist/{chunk-7fqn69ys.js → chunk-xrvmnymc.js} +2 -2
- package/dist/{chunk-tfqc1djy.js → chunk-z5pmcscx.js} +530 -242
- package/dist/pinomad.js +14 -9
- package/package.json +1 -1
- package/web/assets/index-8mNNPXPT.js +383 -0
- package/web/assets/{index-CG33GBxM.css → index-BFJJgGGo.css} +1 -1
- package/web/index.html +2 -2
- package/dist/chunk-4dj2vb8t.js +0 -284
- package/dist/chunk-fxaewke2.js +0 -113
- package/web/assets/index-CBXdQLua.js +0 -383
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
normalizeOrigin,
|
|
4
|
+
readServerBinding,
|
|
5
|
+
readServerConfig,
|
|
6
|
+
resolveRelayOrigin
|
|
7
|
+
} from "./chunk-302y6by9.js";
|
|
2
8
|
import {
|
|
3
9
|
serveWebClient
|
|
4
10
|
} from "./chunk-r6w5aqkw.js";
|
|
11
|
+
import {
|
|
12
|
+
relayWsBase,
|
|
13
|
+
startRelayLink
|
|
14
|
+
} from "./chunk-knaw1a9g.js";
|
|
5
15
|
import {
|
|
6
16
|
packagedVersion,
|
|
7
17
|
webRoot
|
|
8
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-fgx2sh7d.js";
|
|
9
19
|
import {
|
|
10
20
|
HOST_SETTING_KEYS,
|
|
11
21
|
INVALID_FRAME_CLOSE_CODE,
|
|
@@ -14,31 +24,26 @@ import {
|
|
|
14
24
|
UNAUTHORIZED_CLOSE_CODE,
|
|
15
25
|
isQueueMode,
|
|
16
26
|
isThinkingLevel,
|
|
27
|
+
organize,
|
|
17
28
|
parseClientFrame
|
|
18
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-myjp910s.js";
|
|
19
30
|
import {
|
|
20
31
|
DevicesDoc,
|
|
32
|
+
RevocationsDoc,
|
|
33
|
+
admitDevice,
|
|
21
34
|
ensureDevices,
|
|
22
|
-
isRegistered,
|
|
23
35
|
loadHostKey,
|
|
24
36
|
loadRelayKey,
|
|
25
|
-
pairingOffers
|
|
26
|
-
|
|
27
|
-
revokeDevice
|
|
28
|
-
} from "./chunk-fxaewke2.js";
|
|
37
|
+
pairingOffers
|
|
38
|
+
} from "./chunk-837gh8nr.js";
|
|
29
39
|
import {
|
|
30
|
-
RELAY_CLOSE,
|
|
31
40
|
SECURE_PROLOGUE,
|
|
32
41
|
SecureSession,
|
|
33
42
|
decodeClientHello,
|
|
34
|
-
decodeRelayToHost,
|
|
35
43
|
encodeHostHello,
|
|
36
|
-
encodeRelayFrame,
|
|
37
|
-
relayHostId,
|
|
38
44
|
respondIK,
|
|
39
|
-
signRelayChallenge,
|
|
40
45
|
toBase64Url
|
|
41
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-7gmkgyaz.js";
|
|
42
47
|
import {
|
|
43
48
|
DEFAULT_DATA_DIR
|
|
44
49
|
} from "./chunk-cjfmfyjb.js";
|
|
@@ -522,7 +527,7 @@ var todo = {
|
|
|
522
527
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
523
528
|
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
524
529
|
import { basename as basename2, join as join7, sep as sep4 } from "node:path";
|
|
525
|
-
import { BACKGROUND_CONTEXT as
|
|
530
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
|
|
526
531
|
import { createRegistry, Harness } from "@earendil-works/pi-durable";
|
|
527
532
|
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
528
533
|
|
|
@@ -1366,10 +1371,12 @@ var fileValueError = (key, value, models) => {
|
|
|
1366
1371
|
case "steeringMode":
|
|
1367
1372
|
case "followUpMode":
|
|
1368
1373
|
return isQueueMode(value) ? undefined : "malformed";
|
|
1374
|
+
case "acceptAccountDevices":
|
|
1375
|
+
return typeof value === "boolean" ? undefined : "malformed";
|
|
1369
1376
|
}
|
|
1370
1377
|
};
|
|
1371
1378
|
var sameModel = (a, b) => a === b || a !== null && b !== null && a.provider === b.provider && a.modelId === b.modelId;
|
|
1372
|
-
var sameSettings = (a, b) => a.steeringMode === b.steeringMode && a.followUpMode === b.followUpMode && a.defaultThinkingLevel === b.defaultThinkingLevel && sameModel(a.defaultModel, b.defaultModel) && sameList(a.custom, b.custom, (x, y) => x === y);
|
|
1379
|
+
var sameSettings = (a, b) => a.steeringMode === b.steeringMode && a.followUpMode === b.followUpMode && a.acceptAccountDevices === b.acceptAccountDevices && a.defaultThinkingLevel === b.defaultThinkingLevel && sameModel(a.defaultModel, b.defaultModel) && sameList(a.custom, b.custom, (x, y) => x === y);
|
|
1373
1380
|
var parse = (path, text) => {
|
|
1374
1381
|
let parsed;
|
|
1375
1382
|
try {
|
|
@@ -1412,6 +1419,7 @@ async function openHostSettings(options) {
|
|
|
1412
1419
|
defaultThinkingLevel: fileValues.defaultThinkingLevel ?? fallback.defaultThinkingLevel(),
|
|
1413
1420
|
steeringMode: fileValues.steeringMode ?? fallback.steeringMode(),
|
|
1414
1421
|
followUpMode: fileValues.followUpMode ?? fallback.followUpMode(),
|
|
1422
|
+
acceptAccountDevices: fileValues.acceptAccountDevices ?? true,
|
|
1415
1423
|
custom
|
|
1416
1424
|
};
|
|
1417
1425
|
};
|
|
@@ -1458,13 +1466,13 @@ import lockfile from "proper-lockfile";
|
|
|
1458
1466
|
import { createServer } from "node:http";
|
|
1459
1467
|
import { homedir as homedir2, networkInterfaces } from "node:os";
|
|
1460
1468
|
import { join as join6, sep as sep3 } from "node:path";
|
|
1461
|
-
import { BACKGROUND_CONTEXT as
|
|
1469
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
|
|
1462
1470
|
import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1463
1471
|
import {
|
|
1464
1472
|
AgentDoc as AgentDoc3,
|
|
1465
1473
|
LiveDoc
|
|
1466
1474
|
} from "@earendil-works/pi-durable";
|
|
1467
|
-
import { WebSocket
|
|
1475
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
1468
1476
|
import { Value as Value2 } from "typebox/value";
|
|
1469
1477
|
|
|
1470
1478
|
// apps/host/src/folders.ts
|
|
@@ -1651,6 +1659,353 @@ ${answer}`}`,
|
|
|
1651
1659
|
});
|
|
1652
1660
|
}
|
|
1653
1661
|
|
|
1662
|
+
// apps/host/src/push.ts
|
|
1663
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
|
|
1664
|
+
import { defineDoc as defineDoc6 } from "@earendil-works/pi-durable";
|
|
1665
|
+
|
|
1666
|
+
// packages/client/src/family.ts
|
|
1667
|
+
var SUBAGENT_TASK_PREFIX = /^\s*in the working directory\s+\S+,\s*/i;
|
|
1668
|
+
function displayName(summary) {
|
|
1669
|
+
if (summary === undefined)
|
|
1670
|
+
return "Conversation";
|
|
1671
|
+
if (summary.kind !== "subagent")
|
|
1672
|
+
return summary.title ?? "New conversation";
|
|
1673
|
+
if (summary.label !== undefined)
|
|
1674
|
+
return summary.label;
|
|
1675
|
+
const title = summary.title?.replace(SUBAGENT_TASK_PREFIX, "").trim();
|
|
1676
|
+
return title === undefined || title === "" ? "Subagent" : title;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// packages/client/src/run-notifications.ts
|
|
1680
|
+
var everySummary = (organized) => {
|
|
1681
|
+
const summaries = [];
|
|
1682
|
+
const visit = (node) => {
|
|
1683
|
+
summaries.push(node.summary);
|
|
1684
|
+
for (const child of node.children)
|
|
1685
|
+
visit(child);
|
|
1686
|
+
};
|
|
1687
|
+
for (const root of [...organized.chats, ...organized.projects.flatMap((entry) => entry.conversations)])
|
|
1688
|
+
visit(root);
|
|
1689
|
+
return summaries;
|
|
1690
|
+
};
|
|
1691
|
+
function diffRunNotices(previous, organized) {
|
|
1692
|
+
const notices = [];
|
|
1693
|
+
const changed = [];
|
|
1694
|
+
const baseline = new Map;
|
|
1695
|
+
for (const summary of everySummary(organized)) {
|
|
1696
|
+
baseline.set(summary.id, summary.status);
|
|
1697
|
+
if (previous === undefined)
|
|
1698
|
+
continue;
|
|
1699
|
+
const before = previous.get(summary.id);
|
|
1700
|
+
if (before !== summary.status)
|
|
1701
|
+
changed.push(summary.id);
|
|
1702
|
+
if (summary.status === "needs-answer" && before !== "needs-answer") {
|
|
1703
|
+
notices.push({ id: summary.id, title: displayName(summary), body: "Waiting for your answer" });
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
const runEnded = before === "running" || before === "needs-answer";
|
|
1707
|
+
if (runEnded && (summary.status === undefined || summary.status === "failed") && summary.kind !== "subagent") {
|
|
1708
|
+
notices.push({ id: summary.id, title: displayName(summary), body: summary.status === "failed" ? "Failed" : "Finished" });
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
if (previous !== undefined) {
|
|
1712
|
+
for (const gone of previous.keys()) {
|
|
1713
|
+
if (!baseline.has(gone))
|
|
1714
|
+
changed.push(gone);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return { notices, changed, baseline };
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// apps/host/src/push.ts
|
|
1721
|
+
var context2 = BACKGROUND_CONTEXT2;
|
|
1722
|
+
var PushDoc = defineDoc6({
|
|
1723
|
+
kind: "pinomad.push",
|
|
1724
|
+
version: 1,
|
|
1725
|
+
scope: "session",
|
|
1726
|
+
initial: () => ({ registrations: [] })
|
|
1727
|
+
});
|
|
1728
|
+
var PUSH_TOKEN_PATTERN = /^Expo(?:nent)?PushToken\[[^\]]+\]$/;
|
|
1729
|
+
var PUSH_TOKEN_MAX_LENGTH = 256;
|
|
1730
|
+
function pushTokenProblem(token) {
|
|
1731
|
+
if (token === null)
|
|
1732
|
+
return;
|
|
1733
|
+
if (token.length > PUSH_TOKEN_MAX_LENGTH) {
|
|
1734
|
+
return `Push token is too long (${PUSH_TOKEN_MAX_LENGTH} characters at most)`;
|
|
1735
|
+
}
|
|
1736
|
+
if (!PUSH_TOKEN_PATTERN.test(token))
|
|
1737
|
+
return "Not an Expo push token";
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
async function setPushToken(harness, publicKey, token, context3) {
|
|
1741
|
+
const problem = pushTokenProblem(token);
|
|
1742
|
+
if (problem !== undefined)
|
|
1743
|
+
throw new Error(problem);
|
|
1744
|
+
await harness.commit(async (tx) => {
|
|
1745
|
+
const devices = await tx.doc(DevicesDoc);
|
|
1746
|
+
if (!devices.devices.some((entry) => entry.publicKey === publicKey)) {
|
|
1747
|
+
throw new Error("Device is not paired");
|
|
1748
|
+
}
|
|
1749
|
+
const push = await tx.doc(PushDoc);
|
|
1750
|
+
push.registrations = push.registrations.filter((entry) => entry.publicKey !== publicKey);
|
|
1751
|
+
if (token !== null)
|
|
1752
|
+
push.registrations.push({ publicKey, token, updatedAt: Date.now() });
|
|
1753
|
+
}, context3);
|
|
1754
|
+
}
|
|
1755
|
+
async function revokeDevice(harness, publicKey, context3) {
|
|
1756
|
+
await harness.commit(async (tx) => {
|
|
1757
|
+
const devices = await tx.doc(DevicesDoc);
|
|
1758
|
+
devices.devices = devices.devices.filter((entry) => entry.publicKey !== publicKey);
|
|
1759
|
+
const push = await tx.doc(PushDoc);
|
|
1760
|
+
push.registrations = push.registrations.filter((entry) => entry.publicKey !== publicKey);
|
|
1761
|
+
const revocations = await tx.doc(RevocationsDoc);
|
|
1762
|
+
if (!revocations.revoked.includes(publicKey))
|
|
1763
|
+
revocations.revoked.push(publicKey);
|
|
1764
|
+
}, context3);
|
|
1765
|
+
}
|
|
1766
|
+
async function dropTokenIfCurrent(harness, token, context3) {
|
|
1767
|
+
await harness.commit(async (tx) => {
|
|
1768
|
+
const push = await tx.doc(PushDoc);
|
|
1769
|
+
push.registrations = push.registrations.filter((entry) => entry.token !== token);
|
|
1770
|
+
}, context3);
|
|
1771
|
+
}
|
|
1772
|
+
var EXPO_SEND_URL = "https://exp.host/--/api/v2/push/send";
|
|
1773
|
+
var EXPO_RECEIPTS_URL = "https://exp.host/--/api/v2/push/getReceipts";
|
|
1774
|
+
var FALLBACK_TITLE = "PiNomad conversation";
|
|
1775
|
+
var TITLE_WAIT_MS = 15000;
|
|
1776
|
+
var TTL_SECONDS = 3600;
|
|
1777
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
1778
|
+
var MAX_ATTEMPTS = 4;
|
|
1779
|
+
var RETRY_BASE_MS = 5000;
|
|
1780
|
+
var RECEIPT_DELAY_MS = 15 * 60 * 1000;
|
|
1781
|
+
var MAX_PENDING = 128;
|
|
1782
|
+
var realTimers = {
|
|
1783
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
1784
|
+
clearTimeout: (handle) => clearTimeout(handle)
|
|
1785
|
+
};
|
|
1786
|
+
var tokenHint = (token) => `…${token.slice(-6)}`;
|
|
1787
|
+
async function attachPushSender(options) {
|
|
1788
|
+
const { harness, conversations } = options;
|
|
1789
|
+
const fetcher = options.fetch ?? fetch;
|
|
1790
|
+
const timers = options.timers ?? realTimers;
|
|
1791
|
+
const indexWatch = await harness.watchDoc(IndexDoc, context2);
|
|
1792
|
+
let index = indexWatch?.value ?? undefined;
|
|
1793
|
+
let baseline;
|
|
1794
|
+
let disposed = false;
|
|
1795
|
+
const pending = new Set;
|
|
1796
|
+
const titleWaiters = new Map;
|
|
1797
|
+
const receiptTimers = new Set;
|
|
1798
|
+
const generation = new Map;
|
|
1799
|
+
const update = () => {
|
|
1800
|
+
if (index === undefined)
|
|
1801
|
+
return;
|
|
1802
|
+
const organized = organize(index, conversations.value);
|
|
1803
|
+
if (baseline === undefined) {
|
|
1804
|
+
baseline = diffRunNotices(undefined, organized).baseline;
|
|
1805
|
+
for (const id of baseline.keys())
|
|
1806
|
+
generation.set(id, 0);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
const diff = diffRunNotices(baseline, organized);
|
|
1810
|
+
baseline = diff.baseline;
|
|
1811
|
+
for (const id of diff.changed)
|
|
1812
|
+
generation.set(id, (generation.get(id) ?? 0) + 1);
|
|
1813
|
+
for (const notice of diff.notices)
|
|
1814
|
+
enqueue(notice);
|
|
1815
|
+
};
|
|
1816
|
+
const fresh = (item) => baseline?.has(item.notice.id) === true && generation.get(item.notice.id) === item.generation;
|
|
1817
|
+
const registrations = async () => {
|
|
1818
|
+
const doc = await harness.snapshot(PushDoc, context2);
|
|
1819
|
+
return new Set((doc?.registrations ?? []).map((entry) => entry.token));
|
|
1820
|
+
};
|
|
1821
|
+
const post = async (url, payload) => {
|
|
1822
|
+
const response = await fetcher(url, {
|
|
1823
|
+
method: "POST",
|
|
1824
|
+
headers: { accept: "application/json", "content-type": "application/json" },
|
|
1825
|
+
body: JSON.stringify(payload),
|
|
1826
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1827
|
+
});
|
|
1828
|
+
return { status: response.status, body: await response.json() };
|
|
1829
|
+
};
|
|
1830
|
+
const ticketOf = (body) => {
|
|
1831
|
+
const data = body?.data;
|
|
1832
|
+
const ticket = Array.isArray(data) ? data[0] : data;
|
|
1833
|
+
return ticket !== null && typeof ticket === "object" ? ticket : undefined;
|
|
1834
|
+
};
|
|
1835
|
+
const message = (item, title) => ({
|
|
1836
|
+
to: item.token,
|
|
1837
|
+
title,
|
|
1838
|
+
body: item.notice.body,
|
|
1839
|
+
sound: "default",
|
|
1840
|
+
ttl: TTL_SECONDS,
|
|
1841
|
+
data: { hostKey: options.hostKey, conversationId: item.notice.id },
|
|
1842
|
+
threadId: `${options.hostKey}:${item.notice.id}`,
|
|
1843
|
+
collapseId: `${options.hostKey}:${item.notice.id}`
|
|
1844
|
+
});
|
|
1845
|
+
const namedTitle = async (id) => {
|
|
1846
|
+
const doc = await harness.snapshot(TitleDoc, id, context2);
|
|
1847
|
+
return typeof doc?.title === "string" && doc.title !== "" ? doc.title : undefined;
|
|
1848
|
+
};
|
|
1849
|
+
const titleFor = async (notice) => {
|
|
1850
|
+
const named = await namedTitle(notice.id);
|
|
1851
|
+
if (named !== undefined)
|
|
1852
|
+
return named;
|
|
1853
|
+
if (notice.body !== "Finished" || disposed)
|
|
1854
|
+
return FALLBACK_TITLE;
|
|
1855
|
+
return await new Promise((resolve3) => {
|
|
1856
|
+
const waiters = titleWaiters.get(notice.id) ?? new Set;
|
|
1857
|
+
titleWaiters.set(notice.id, waiters);
|
|
1858
|
+
const settle = (title) => {
|
|
1859
|
+
waiters.delete(settle);
|
|
1860
|
+
if (waiters.size === 0)
|
|
1861
|
+
titleWaiters.delete(notice.id);
|
|
1862
|
+
timers.clearTimeout(timer);
|
|
1863
|
+
resolve3(title);
|
|
1864
|
+
};
|
|
1865
|
+
const timer = timers.setTimeout(() => settle(FALLBACK_TITLE), TITLE_WAIT_MS);
|
|
1866
|
+
waiters.add(settle);
|
|
1867
|
+
namedTitle(notice.id).then((title) => {
|
|
1868
|
+
if (title !== undefined)
|
|
1869
|
+
settle(title);
|
|
1870
|
+
});
|
|
1871
|
+
});
|
|
1872
|
+
};
|
|
1873
|
+
const drop = (item) => {
|
|
1874
|
+
pending.delete(item);
|
|
1875
|
+
if (item.timer !== undefined)
|
|
1876
|
+
timers.clearTimeout(item.timer);
|
|
1877
|
+
};
|
|
1878
|
+
const attempt = async (item) => {
|
|
1879
|
+
if (disposed || !pending.has(item))
|
|
1880
|
+
return;
|
|
1881
|
+
try {
|
|
1882
|
+
if (!(await registrations()).has(item.token) || !fresh(item))
|
|
1883
|
+
return drop(item);
|
|
1884
|
+
if (item.title === undefined) {
|
|
1885
|
+
item.title = await titleFor(item.notice);
|
|
1886
|
+
if (disposed || !pending.has(item) || !(await registrations()).has(item.token) || !fresh(item))
|
|
1887
|
+
return drop(item);
|
|
1888
|
+
}
|
|
1889
|
+
const { status, body } = await post(EXPO_SEND_URL, message(item, item.title));
|
|
1890
|
+
const ticket = ticketOf(body);
|
|
1891
|
+
if (ticket?.details?.error === "DeviceNotRegistered") {
|
|
1892
|
+
drop(item);
|
|
1893
|
+
console.error(`push: token ${tokenHint(item.token)} is not registered (ticket)`);
|
|
1894
|
+
await dropTokenIfCurrent(harness, item.token, context2);
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
if (status >= 200 && status < 300 && ticket?.status === "ok") {
|
|
1898
|
+
drop(item);
|
|
1899
|
+
if (typeof ticket.id === "string")
|
|
1900
|
+
scheduleReceiptCheck(item.token, ticket.id);
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
if ((status === 429 || status >= 500) && item.attempts + 1 < MAX_ATTEMPTS) {
|
|
1904
|
+
item.attempts += 1;
|
|
1905
|
+
item.timer = timers.setTimeout(() => void attempt(item), RETRY_BASE_MS * 2 ** (item.attempts - 1));
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
console.error(`push: send to ${tokenHint(item.token)} failed permanently (HTTP ${status})`);
|
|
1909
|
+
drop(item);
|
|
1910
|
+
} catch (error) {
|
|
1911
|
+
if (item.attempts + 1 < MAX_ATTEMPTS) {
|
|
1912
|
+
item.attempts += 1;
|
|
1913
|
+
item.timer = timers.setTimeout(() => void attempt(item), RETRY_BASE_MS * 2 ** (item.attempts - 1));
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
console.error(`push: send to ${tokenHint(item.token)} gave up: ${error instanceof Error ? error.message : String(error)}`);
|
|
1917
|
+
drop(item);
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
const scheduleReceiptCheck = (token, ticketId) => {
|
|
1921
|
+
const timer = timers.setTimeout(() => {
|
|
1922
|
+
receiptTimers.delete(timer);
|
|
1923
|
+
checkReceipt(token, ticketId);
|
|
1924
|
+
}, RECEIPT_DELAY_MS);
|
|
1925
|
+
receiptTimers.add(timer);
|
|
1926
|
+
while (receiptTimers.size > MAX_PENDING) {
|
|
1927
|
+
const oldest = receiptTimers.values().next().value;
|
|
1928
|
+
receiptTimers.delete(oldest);
|
|
1929
|
+
timers.clearTimeout(oldest);
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
const checkReceipt = async (token, ticketId) => {
|
|
1933
|
+
if (disposed)
|
|
1934
|
+
return;
|
|
1935
|
+
try {
|
|
1936
|
+
const { status, body } = await post(EXPO_RECEIPTS_URL, { ids: [ticketId] });
|
|
1937
|
+
const receipt = status >= 200 && status < 300 ? body?.data?.[ticketId] : undefined;
|
|
1938
|
+
if (receipt === undefined || disposed)
|
|
1939
|
+
return;
|
|
1940
|
+
if (receipt.status === "error" && receipt.details?.error === "DeviceNotRegistered") {
|
|
1941
|
+
console.error(`push: token ${tokenHint(token)} is not registered (receipt)`);
|
|
1942
|
+
await dropTokenIfCurrent(harness, token, context2);
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
if (receipt.status === "error")
|
|
1946
|
+
console.error(`push: receipt ${ticketId} reports ${receipt.details?.error ?? "error"}`);
|
|
1947
|
+
} catch (error) {
|
|
1948
|
+
console.error(`push: receipt check ${ticketId} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
const enqueue = async (notice) => {
|
|
1952
|
+
if (disposed)
|
|
1953
|
+
return;
|
|
1954
|
+
const itemGeneration = generation.get(notice.id) ?? 0;
|
|
1955
|
+
try {
|
|
1956
|
+
const tokens = await registrations();
|
|
1957
|
+
for (const token of tokens) {
|
|
1958
|
+
if (disposed)
|
|
1959
|
+
return;
|
|
1960
|
+
if (pending.size >= MAX_PENDING) {
|
|
1961
|
+
const [oldest] = pending;
|
|
1962
|
+
drop(oldest);
|
|
1963
|
+
}
|
|
1964
|
+
const item = { token, notice, generation: itemGeneration, attempts: 0 };
|
|
1965
|
+
pending.add(item);
|
|
1966
|
+
attempt(item);
|
|
1967
|
+
}
|
|
1968
|
+
} catch (error) {
|
|
1969
|
+
console.error(`push: could not read registrations: ${error instanceof Error ? error.message : String(error)}`);
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1972
|
+
const unsubscribeConversations = conversations.subscribe(() => update());
|
|
1973
|
+
const unsubscribeCommits = harness.subscribeCommits((publication) => {
|
|
1974
|
+
for (const change of publication.changes) {
|
|
1975
|
+
if (change.type !== "document" || change.record.kind !== TitleDoc.definition.kind)
|
|
1976
|
+
continue;
|
|
1977
|
+
const id = change.conversationId;
|
|
1978
|
+
if (id === undefined)
|
|
1979
|
+
continue;
|
|
1980
|
+
const title = change.value?.title;
|
|
1981
|
+
if (typeof title !== "string" || title === "")
|
|
1982
|
+
continue;
|
|
1983
|
+
const waiters = titleWaiters.get(id);
|
|
1984
|
+
titleWaiters.delete(id);
|
|
1985
|
+
for (const settle of waiters ?? [])
|
|
1986
|
+
settle(title);
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
indexWatch?.start(async (value) => {
|
|
1990
|
+
index = value ?? undefined;
|
|
1991
|
+
update();
|
|
1992
|
+
});
|
|
1993
|
+
update();
|
|
1994
|
+
return async () => {
|
|
1995
|
+
disposed = true;
|
|
1996
|
+
unsubscribeConversations();
|
|
1997
|
+
unsubscribeCommits();
|
|
1998
|
+
await indexWatch?.stop();
|
|
1999
|
+
for (const item of pending)
|
|
2000
|
+
drop(item);
|
|
2001
|
+
pending.clear();
|
|
2002
|
+
titleWaiters.clear();
|
|
2003
|
+
for (const timer of receiptTimers)
|
|
2004
|
+
timers.clearTimeout(timer);
|
|
2005
|
+
receiptTimers.clear();
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
|
|
1654
2009
|
// packages/protocol/src/tool-target.ts
|
|
1655
2010
|
var TARGET_KEYS = [
|
|
1656
2011
|
"path",
|
|
@@ -1678,154 +2033,8 @@ function toolTarget(args) {
|
|
|
1678
2033
|
return;
|
|
1679
2034
|
}
|
|
1680
2035
|
|
|
1681
|
-
// apps/host/src/relay-link.ts
|
|
1682
|
-
import { WebSocket } from "ws";
|
|
1683
|
-
function relayWsBase(origin) {
|
|
1684
|
-
const normalized = origin.replace(/\/+$/, "");
|
|
1685
|
-
if (normalized.startsWith("https://"))
|
|
1686
|
-
return `wss://${normalized.slice("https://".length)}`;
|
|
1687
|
-
if (normalized.startsWith("http://"))
|
|
1688
|
-
return `ws://${normalized.slice("http://".length)}`;
|
|
1689
|
-
throw new Error(`relay origin must be an http(s) URL: ${origin}`);
|
|
1690
|
-
}
|
|
1691
|
-
var REJECTION_REASONS = {
|
|
1692
|
-
[RELAY_CLOSE.badSignature]: () => `the relay rejected this host's registration signature — a version mismatch?`,
|
|
1693
|
-
[RELAY_CLOSE.hostNotAllowed]: (hostId) => `the relay does not allow this host; add --allow-host ${hostId} to the relay's arguments`,
|
|
1694
|
-
[RELAY_CLOSE.replaced]: () => `the relay replaced this control connection — another process registered the same hostId`
|
|
1695
|
-
};
|
|
1696
|
-
function startRelayLink(options) {
|
|
1697
|
-
const origin = options.origin.replace(/\/+$/, "");
|
|
1698
|
-
const wsBase = relayWsBase(origin);
|
|
1699
|
-
const hostId = relayHostId(options.signingKey.publicKey);
|
|
1700
|
-
const minDelay = options.reconnectDelayMs?.min ?? 1000;
|
|
1701
|
-
const maxDelay = options.reconnectDelayMs?.max ?? 30000;
|
|
1702
|
-
const maxPendingAccepts = options.maxPendingAccepts ?? 16;
|
|
1703
|
-
let state = { status: "connecting" };
|
|
1704
|
-
let stopped = false;
|
|
1705
|
-
let control;
|
|
1706
|
-
let delay = minDelay;
|
|
1707
|
-
let retryTimer;
|
|
1708
|
-
let pendingAccepts = 0;
|
|
1709
|
-
function setState(next) {
|
|
1710
|
-
const changed = state.status !== next.status || state.status === "rejected" && next.status === "rejected" && state.reason !== next.reason;
|
|
1711
|
-
state = next;
|
|
1712
|
-
if (changed)
|
|
1713
|
-
options.onStateChange?.(next);
|
|
1714
|
-
}
|
|
1715
|
-
const watched = new Set;
|
|
1716
|
-
const ponged = new WeakMap;
|
|
1717
|
-
const pingIntervalMs = options.pingIntervalMs ?? 30000;
|
|
1718
|
-
const pingTimer = setInterval(() => {
|
|
1719
|
-
for (const socket of watched) {
|
|
1720
|
-
if (socket.readyState !== WebSocket.OPEN)
|
|
1721
|
-
continue;
|
|
1722
|
-
if (ponged.get(socket) === true) {
|
|
1723
|
-
ponged.set(socket, false);
|
|
1724
|
-
socket.ping();
|
|
1725
|
-
} else {
|
|
1726
|
-
socket.terminate();
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
}, pingIntervalMs);
|
|
1730
|
-
function watch2(socket) {
|
|
1731
|
-
watched.add(socket);
|
|
1732
|
-
ponged.set(socket, true);
|
|
1733
|
-
socket.on("pong", () => ponged.set(socket, true));
|
|
1734
|
-
socket.once("close", () => watched.delete(socket));
|
|
1735
|
-
socket.on("error", () => socket.terminate());
|
|
1736
|
-
}
|
|
1737
|
-
function connectControl() {
|
|
1738
|
-
if (stopped)
|
|
1739
|
-
return;
|
|
1740
|
-
const socket = new WebSocket(`${wsBase}/host`, { handshakeTimeout: pingIntervalMs });
|
|
1741
|
-
control = socket;
|
|
1742
|
-
watch2(socket);
|
|
1743
|
-
socket.on("message", onControlMessage);
|
|
1744
|
-
socket.once("close", onControlClosed);
|
|
1745
|
-
}
|
|
1746
|
-
function reconnect(at) {
|
|
1747
|
-
if (stopped || retryTimer !== undefined)
|
|
1748
|
-
return;
|
|
1749
|
-
retryTimer = setTimeout(() => {
|
|
1750
|
-
retryTimer = undefined;
|
|
1751
|
-
connectControl();
|
|
1752
|
-
}, at);
|
|
1753
|
-
}
|
|
1754
|
-
function onControlMessage(data, isBinary) {
|
|
1755
|
-
let frame;
|
|
1756
|
-
try {
|
|
1757
|
-
if (isBinary)
|
|
1758
|
-
throw new Error("binary control frame");
|
|
1759
|
-
frame = decodeRelayToHost(data.toString());
|
|
1760
|
-
} catch {
|
|
1761
|
-
control?.close();
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
switch (frame.t) {
|
|
1765
|
-
case "challenge":
|
|
1766
|
-
control?.send(encodeRelayFrame({
|
|
1767
|
-
t: "register",
|
|
1768
|
-
hostId,
|
|
1769
|
-
signature: signRelayChallenge(options.signingKey.secretKey, { relayOrigin: origin, hostId, nonce: frame.nonce })
|
|
1770
|
-
}));
|
|
1771
|
-
break;
|
|
1772
|
-
case "registered":
|
|
1773
|
-
delay = minDelay;
|
|
1774
|
-
setState({ status: "registered" });
|
|
1775
|
-
break;
|
|
1776
|
-
case "incoming": {
|
|
1777
|
-
if (pendingAccepts >= maxPendingAccepts)
|
|
1778
|
-
break;
|
|
1779
|
-
pendingAccepts += 1;
|
|
1780
|
-
const data2 = new WebSocket(`${wsBase}/accept/${frame.connId}`, { handshakeTimeout: pingIntervalMs });
|
|
1781
|
-
watch2(data2);
|
|
1782
|
-
const release = () => {
|
|
1783
|
-
pendingAccepts -= 1;
|
|
1784
|
-
clearTimeout(ageOut);
|
|
1785
|
-
data2.off("close", release);
|
|
1786
|
-
};
|
|
1787
|
-
const ageOut = setTimeout(release, 1e4);
|
|
1788
|
-
data2.once("close", release);
|
|
1789
|
-
options.accept(data2);
|
|
1790
|
-
break;
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
function onControlClosed(code) {
|
|
1795
|
-
control = undefined;
|
|
1796
|
-
if (stopped)
|
|
1797
|
-
return;
|
|
1798
|
-
const rejection = REJECTION_REASONS[code];
|
|
1799
|
-
if (rejection !== undefined) {
|
|
1800
|
-
const reason = `relay ${origin}: ${rejection(hostId)} (${code})`;
|
|
1801
|
-
setState({ status: "rejected", reason });
|
|
1802
|
-
delay = maxDelay;
|
|
1803
|
-
reconnect(maxDelay);
|
|
1804
|
-
return;
|
|
1805
|
-
}
|
|
1806
|
-
setState({ status: "connecting" });
|
|
1807
|
-
reconnect(delay);
|
|
1808
|
-
delay = Math.min(maxDelay, delay * 2);
|
|
1809
|
-
}
|
|
1810
|
-
connectControl();
|
|
1811
|
-
return {
|
|
1812
|
-
hostId,
|
|
1813
|
-
origin,
|
|
1814
|
-
state: () => state,
|
|
1815
|
-
close() {
|
|
1816
|
-
stopped = true;
|
|
1817
|
-
if (retryTimer !== undefined)
|
|
1818
|
-
clearTimeout(retryTimer);
|
|
1819
|
-
clearInterval(pingTimer);
|
|
1820
|
-
for (const socket of watched)
|
|
1821
|
-
socket.terminate();
|
|
1822
|
-
return Promise.resolve();
|
|
1823
|
-
}
|
|
1824
|
-
};
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
2036
|
// apps/host/src/gateway.ts
|
|
1828
|
-
var
|
|
2037
|
+
var context3 = BACKGROUND_CONTEXT3;
|
|
1829
2038
|
var forkTails = new WeakMap;
|
|
1830
2039
|
function serializedFork(harness, work) {
|
|
1831
2040
|
const next = (forkTails.get(harness) ?? Promise.resolve()).then(work, work);
|
|
@@ -1845,6 +2054,13 @@ async function startGateway(options) {
|
|
|
1845
2054
|
models: options.models,
|
|
1846
2055
|
settings: options.settings
|
|
1847
2056
|
});
|
|
2057
|
+
const detachPush = options.push === undefined ? undefined : await attachPushSender({
|
|
2058
|
+
harness: options.harness,
|
|
2059
|
+
conversations,
|
|
2060
|
+
hostKey: options.push.hostKey,
|
|
2061
|
+
...options.push.fetch === undefined ? {} : { fetch: options.push.fetch },
|
|
2062
|
+
...options.push.timers === undefined ? {} : { timers: options.push.timers }
|
|
2063
|
+
});
|
|
1848
2064
|
const http = createServer((request, response) => {
|
|
1849
2065
|
serveWebClient(request, response, options.webRoot).catch(() => {
|
|
1850
2066
|
if (!response.headersSent)
|
|
@@ -1867,7 +2083,10 @@ async function startGateway(options) {
|
|
|
1867
2083
|
};
|
|
1868
2084
|
let remote;
|
|
1869
2085
|
let relay;
|
|
1870
|
-
const register = (connection, devicePublicKey) => {
|
|
2086
|
+
const register = (connection, devicePublicKey, joined) => {
|
|
2087
|
+
if (joined?.source === "account") {
|
|
2088
|
+
broadcast("info", `New device "${joined.name}" joined through your account`);
|
|
2089
|
+
}
|
|
1871
2090
|
const client = new GatewayClient(connection, options, conversations, {
|
|
1872
2091
|
devicePublicKey,
|
|
1873
2092
|
closed: (self) => {
|
|
@@ -1884,20 +2103,30 @@ async function startGateway(options) {
|
|
|
1884
2103
|
const spec = options.secure;
|
|
1885
2104
|
if (spec === undefined)
|
|
1886
2105
|
return;
|
|
1887
|
-
const
|
|
1888
|
-
const pair = `pair=${toBase64Url(spec.hostKey.publicKey)}.${secret}`;
|
|
2106
|
+
const pair = (secret2) => `pair=${toBase64Url(spec.hostKey.publicKey)}.${secret2}`;
|
|
1889
2107
|
if (relay !== undefined) {
|
|
2108
|
+
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
1890
2109
|
const target2 = `${relayWsBase(relay.origin)}/c/${relay.hostId}`;
|
|
1891
|
-
return { url: `${relay.origin}/#${pair}&url=${encodeURIComponent(target2)}`, expiresAt };
|
|
2110
|
+
return { url: `${relay.origin}/#${pair(secret2)}&url=${encodeURIComponent(target2)}`, expiresAt: expiresAt2 };
|
|
2111
|
+
}
|
|
2112
|
+
if (remote !== undefined) {
|
|
2113
|
+
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
2114
|
+
return { url: `${remote.advertiseUrl}/#${pair(secret2)}`, expiresAt: expiresAt2 };
|
|
1892
2115
|
}
|
|
1893
|
-
|
|
1894
|
-
return { url: `${remote.advertiseUrl}/#${pair}`, expiresAt };
|
|
2116
|
+
const { secret, expiresAt } = spec.offers.create("local");
|
|
1895
2117
|
const target = `ws://127.0.0.1:${port}${LOOPBACK_SECURE_PATH}`;
|
|
1896
|
-
return { url: `http://127.0.0.1:${port}/#${pair}&url=${encodeURIComponent(target)}`, expiresAt };
|
|
2118
|
+
return { url: `http://127.0.0.1:${port}/#${pair(secret)}&url=${encodeURIComponent(target)}`, expiresAt };
|
|
1897
2119
|
}
|
|
1898
2120
|
});
|
|
1899
2121
|
clients.add(client);
|
|
1900
2122
|
};
|
|
2123
|
+
const admission = {
|
|
2124
|
+
harness: options.harness,
|
|
2125
|
+
acceptAccountDevices: () => options.settings.value.acceptAccountDevices,
|
|
2126
|
+
serverBinding: async () => await readServerBinding(options.dataDir).catch(() => {
|
|
2127
|
+
return;
|
|
2128
|
+
})
|
|
2129
|
+
};
|
|
1901
2130
|
server.on("connection", (socket, request) => {
|
|
1902
2131
|
socket.on("error", () => socket.terminate());
|
|
1903
2132
|
const url = new URL(request.url ?? "/", "ws://127.0.0.1");
|
|
@@ -1907,7 +2136,7 @@ async function startGateway(options) {
|
|
|
1907
2136
|
socket.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
1908
2137
|
return;
|
|
1909
2138
|
}
|
|
1910
|
-
secureHandshake(socket, secure2,
|
|
2139
|
+
secureHandshake(socket, secure2, admission, register);
|
|
1911
2140
|
return;
|
|
1912
2141
|
}
|
|
1913
2142
|
const token = url.searchParams.get("token");
|
|
@@ -1918,14 +2147,34 @@ async function startGateway(options) {
|
|
|
1918
2147
|
}
|
|
1919
2148
|
register(tokenConnection(socket));
|
|
1920
2149
|
});
|
|
1921
|
-
remote = await startRemote(options, register);
|
|
2150
|
+
remote = await startRemote(options, admission, register);
|
|
1922
2151
|
const secure = options.secure;
|
|
2152
|
+
const detachSettingsWatch = options.settings.subscribe((settings) => {
|
|
2153
|
+
if (!settings.acceptAccountDevices)
|
|
2154
|
+
secure?.offers.revokeAccount();
|
|
2155
|
+
});
|
|
1923
2156
|
if (secure?.relay !== undefined) {
|
|
2157
|
+
const relayOrigin = secure.relay.origin;
|
|
1924
2158
|
let link;
|
|
1925
2159
|
link = startRelayLink({
|
|
1926
|
-
origin:
|
|
2160
|
+
origin: relayOrigin,
|
|
1927
2161
|
signingKey: secure.relay.signingKey,
|
|
1928
|
-
|
|
2162
|
+
noisePublicKey: secure.hostKey.publicKey,
|
|
2163
|
+
accept: (socket) => secureHandshake(socket, secure, admission, register),
|
|
2164
|
+
accountOffer: async () => {
|
|
2165
|
+
if (!options.settings.value.acceptAccountDevices)
|
|
2166
|
+
return { ok: false, reason: "disabled" };
|
|
2167
|
+
const config = await readServerConfig(options.dataDir).catch(() => {
|
|
2168
|
+
return;
|
|
2169
|
+
});
|
|
2170
|
+
if (config?.binding === undefined)
|
|
2171
|
+
return { ok: false, reason: "not_joined" };
|
|
2172
|
+
if (normalizeOrigin(config.origin) !== normalizeOrigin(relayOrigin)) {
|
|
2173
|
+
return { ok: false, reason: "rebinding" };
|
|
2174
|
+
}
|
|
2175
|
+
const { secret, expiresAt } = secure.offers.createAccount(config.binding);
|
|
2176
|
+
return { ok: true, secret, expiresAt };
|
|
2177
|
+
},
|
|
1929
2178
|
onStateChange: (state) => {
|
|
1930
2179
|
if (state.status === "registered") {
|
|
1931
2180
|
console.log(JSON.stringify({ event: "relay", status: "registered", origin: link.origin, hostId: link.hostId }));
|
|
@@ -1949,6 +2198,8 @@ async function startGateway(options) {
|
|
|
1949
2198
|
async close() {
|
|
1950
2199
|
await relay?.close();
|
|
1951
2200
|
detachAutoTitle?.();
|
|
2201
|
+
detachSettingsWatch();
|
|
2202
|
+
await detachPush?.();
|
|
1952
2203
|
conversations.dispose();
|
|
1953
2204
|
await Promise.all([...clients].map((client) => client.dispose()));
|
|
1954
2205
|
for (const socket of server.clients)
|
|
@@ -1962,9 +2213,9 @@ async function startGateway(options) {
|
|
|
1962
2213
|
function tokenConnection(socket) {
|
|
1963
2214
|
return {
|
|
1964
2215
|
get open() {
|
|
1965
|
-
return socket.readyState ===
|
|
2216
|
+
return socket.readyState === WebSocket.OPEN;
|
|
1966
2217
|
},
|
|
1967
|
-
send: (text) => socket.readyState ===
|
|
2218
|
+
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((resolve3) => socket.send(text, () => resolve3())) : Promise.resolve(),
|
|
1968
2219
|
close: (code, reason) => socket.close(code, reason),
|
|
1969
2220
|
handle(handlers) {
|
|
1970
2221
|
socket.on("message", (data) => handlers.message(String(data)));
|
|
@@ -1975,9 +2226,9 @@ function tokenConnection(socket) {
|
|
|
1975
2226
|
function secureConnection(socket, session) {
|
|
1976
2227
|
return {
|
|
1977
2228
|
get open() {
|
|
1978
|
-
return socket.readyState ===
|
|
2229
|
+
return socket.readyState === WebSocket.OPEN;
|
|
1979
2230
|
},
|
|
1980
|
-
send: (text) => socket.readyState ===
|
|
2231
|
+
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((resolve3) => {
|
|
1981
2232
|
const chunks = session.seal(text);
|
|
1982
2233
|
chunks.forEach((chunk, index) => socket.send(chunk, index === chunks.length - 1 ? () => resolve3() : undefined));
|
|
1983
2234
|
}) : Promise.resolve(),
|
|
@@ -2012,7 +2263,7 @@ function pickLanAddress(interfaces) {
|
|
|
2012
2263
|
}
|
|
2013
2264
|
return fallback;
|
|
2014
2265
|
}
|
|
2015
|
-
function secureHandshake(socket, secure,
|
|
2266
|
+
function secureHandshake(socket, secure, policy, register) {
|
|
2016
2267
|
socket.on("error", () => socket.terminate());
|
|
2017
2268
|
const responder = respondIK({ prologue: SECURE_PROLOGUE, static: secure.hostKey });
|
|
2018
2269
|
let settled = false;
|
|
@@ -2034,7 +2285,7 @@ function secureHandshake(socket, secure, harness, register) {
|
|
|
2034
2285
|
clearTimeout(timeout);
|
|
2035
2286
|
const session = new SecureSession(admitted.result);
|
|
2036
2287
|
socket.send(admitted.message);
|
|
2037
|
-
register(secureConnection(socket, session), admitted.deviceKey);
|
|
2288
|
+
register(secureConnection(socket, session), admitted.deviceKey, admitted.joined);
|
|
2038
2289
|
}).catch(() => socket.close(1011, "handshake failed"));
|
|
2039
2290
|
async function admit() {
|
|
2040
2291
|
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
@@ -2049,19 +2300,28 @@ function secureHandshake(socket, secure, harness, register) {
|
|
|
2049
2300
|
return;
|
|
2050
2301
|
}
|
|
2051
2302
|
const deviceKey = toBase64Url(remoteStatic);
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2303
|
+
const decision = await admitDevice(policy.harness, {
|
|
2304
|
+
publicKey: deviceKey,
|
|
2305
|
+
...hello.pair === undefined ? {} : { pair: hello.pair },
|
|
2306
|
+
offers: secure.offers,
|
|
2307
|
+
acceptAccountDevices: policy.acceptAccountDevices,
|
|
2308
|
+
serverBinding: policy.serverBinding
|
|
2309
|
+
}, context3);
|
|
2310
|
+
if (decision.outcome === "refused") {
|
|
2311
|
+
socket.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
|
|
2312
|
+
return;
|
|
2058
2313
|
}
|
|
2059
2314
|
const { message, result: result2 } = responder.writeMessage2(encodeHostHello({ v: 1 }));
|
|
2060
|
-
return {
|
|
2315
|
+
return {
|
|
2316
|
+
result: result2,
|
|
2317
|
+
message,
|
|
2318
|
+
deviceKey,
|
|
2319
|
+
...decision.outcome === "admitted" ? { joined: decision.entry } : {}
|
|
2320
|
+
};
|
|
2061
2321
|
}
|
|
2062
2322
|
});
|
|
2063
2323
|
}
|
|
2064
|
-
async function startRemote(options, register) {
|
|
2324
|
+
async function startRemote(options, admission, register) {
|
|
2065
2325
|
const spec = options.secure;
|
|
2066
2326
|
const remote = spec?.lan;
|
|
2067
2327
|
if (spec === undefined || remote === undefined)
|
|
@@ -2074,7 +2334,7 @@ async function startRemote(options, register) {
|
|
|
2074
2334
|
});
|
|
2075
2335
|
});
|
|
2076
2336
|
const secure = new WebSocketServer({ server: http });
|
|
2077
|
-
secure.on("connection", (socket) => secureHandshake(socket, spec,
|
|
2337
|
+
secure.on("connection", (socket) => secureHandshake(socket, spec, admission, register));
|
|
2078
2338
|
await new Promise((resolveListen, reject) => {
|
|
2079
2339
|
http.once("listening", resolveListen);
|
|
2080
2340
|
http.once("error", reject);
|
|
@@ -2115,7 +2375,7 @@ class ConversationList {
|
|
|
2115
2375
|
const summaries = [];
|
|
2116
2376
|
let cursor;
|
|
2117
2377
|
do {
|
|
2118
|
-
const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor),
|
|
2378
|
+
const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor), context3);
|
|
2119
2379
|
for (const record of page.items) {
|
|
2120
2380
|
list.#records.set(record.id, record);
|
|
2121
2381
|
const facts = await list.#load(harness, record);
|
|
@@ -2132,23 +2392,23 @@ class ConversationList {
|
|
|
2132
2392
|
async#load(harness, record) {
|
|
2133
2393
|
const history = await historyFacts(harness, record.id);
|
|
2134
2394
|
const facts = { pending: new Set, ...history };
|
|
2135
|
-
const live = await harness.snapshot(LiveDoc, record.id,
|
|
2395
|
+
const live = await harness.snapshot(LiveDoc, record.id, context3);
|
|
2136
2396
|
facts.running = live?.run !== undefined;
|
|
2137
2397
|
facts.tools = live?.tools;
|
|
2138
2398
|
for (const token of this.#questionTokens) {
|
|
2139
|
-
const value = await harness.snapshot(token, record.id,
|
|
2399
|
+
const value = await harness.snapshot(token, record.id, context3);
|
|
2140
2400
|
if (Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined)) {
|
|
2141
2401
|
facts.pending.add(token.definition.kind);
|
|
2142
2402
|
}
|
|
2143
2403
|
}
|
|
2144
2404
|
if (record.owner !== undefined) {
|
|
2145
|
-
const subagent = await harness.snapshot(SubagentDoc, record.id,
|
|
2405
|
+
const subagent = await harness.snapshot(SubagentDoc, record.id, context3);
|
|
2146
2406
|
if (typeof subagent?.label === "string" && subagent.label !== "")
|
|
2147
2407
|
facts.label = subagent.label;
|
|
2148
2408
|
if (typeof subagent?.role === "string" && subagent.role !== "")
|
|
2149
2409
|
facts.role = subagent.role;
|
|
2150
2410
|
}
|
|
2151
|
-
const named = (await harness.snapshot(TitleDoc, record.id,
|
|
2411
|
+
const named = (await harness.snapshot(TitleDoc, record.id, context3))?.title;
|
|
2152
2412
|
if (typeof named === "string" && named !== "")
|
|
2153
2413
|
facts.named = named;
|
|
2154
2414
|
return facts;
|
|
@@ -2301,12 +2561,12 @@ function titleOf(entry) {
|
|
|
2301
2561
|
return title === "" ? {} : { title };
|
|
2302
2562
|
}
|
|
2303
2563
|
async function historyFacts(harness, id) {
|
|
2304
|
-
const conversation = await harness.conversation(id,
|
|
2564
|
+
const conversation = await harness.conversation(id, context3);
|
|
2305
2565
|
let first;
|
|
2306
2566
|
let newest;
|
|
2307
2567
|
let cursor;
|
|
2308
2568
|
do {
|
|
2309
|
-
const page = await conversation.entries({}, 256, cursor,
|
|
2569
|
+
const page = await conversation.entries({}, 256, cursor, context3);
|
|
2310
2570
|
first = page.items.findLast((entry) => entry.kind === "pi.user" && entry.conversationId === id && titleOf(entry).title !== undefined) ?? first;
|
|
2311
2571
|
newest ??= page.items.find((entry) => entry.conversationId === id && (entry.kind === "pi.user" || entry.kind === "pi.assistant"));
|
|
2312
2572
|
cursor = page.next;
|
|
@@ -2450,25 +2710,25 @@ class GatewayClient {
|
|
|
2450
2710
|
return { stop: source.subscribe((value) => void send({ type: "snapshot", stream, value })) };
|
|
2451
2711
|
}
|
|
2452
2712
|
if (stream === "index") {
|
|
2453
|
-
const watch2 = await this.#options.harness.watchDoc(IndexDoc,
|
|
2713
|
+
const watch2 = await this.#options.harness.watchDoc(IndexDoc, context3);
|
|
2454
2714
|
if (watch2 === undefined)
|
|
2455
2715
|
throw new Error("Index document is missing");
|
|
2456
2716
|
return this.#forward(stream, watch2, send);
|
|
2457
2717
|
}
|
|
2458
2718
|
if (stream === "devices") {
|
|
2459
|
-
const watch2 = await this.#options.harness.watchDoc(DevicesDoc,
|
|
2719
|
+
const watch2 = await this.#options.harness.watchDoc(DevicesDoc, context3);
|
|
2460
2720
|
if (watch2 === undefined)
|
|
2461
2721
|
throw new Error("Devices document is missing");
|
|
2462
2722
|
return this.#forward(stream, watch2, send);
|
|
2463
2723
|
}
|
|
2464
2724
|
if (stream === "tasks")
|
|
2465
|
-
return this.#forward(stream, await this.#options.harness.watchTaskGraph(
|
|
2725
|
+
return this.#forward(stream, await this.#options.harness.watchTaskGraph(context3), send);
|
|
2466
2726
|
if (stream.startsWith("doc:")) {
|
|
2467
2727
|
const at = stream.lastIndexOf(":");
|
|
2468
2728
|
return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
|
|
2469
2729
|
}
|
|
2470
2730
|
const conversation = await this.#conversation(Number(stream.slice("conversation:".length)));
|
|
2471
|
-
return this.#forward(stream, await conversation.watch(
|
|
2731
|
+
return this.#forward(stream, await conversation.watch(context3), send);
|
|
2472
2732
|
}
|
|
2473
2733
|
#hostValue(stream) {
|
|
2474
2734
|
return Object.hasOwn(this.#hostValues, stream) ? this.#hostValues[stream] : undefined;
|
|
@@ -2494,7 +2754,7 @@ class GatewayClient {
|
|
|
2494
2754
|
setImmediate(() => signalCreation());
|
|
2495
2755
|
});
|
|
2496
2756
|
try {
|
|
2497
|
-
const existing = await harness.watchDoc(token, id,
|
|
2757
|
+
const existing = await harness.watchDoc(token, id, context3);
|
|
2498
2758
|
if (existing !== undefined) {
|
|
2499
2759
|
unsubscribe();
|
|
2500
2760
|
return this.#forward(stream, existing, send);
|
|
@@ -2507,7 +2767,7 @@ class GatewayClient {
|
|
|
2507
2767
|
created.then(async () => {
|
|
2508
2768
|
if (stopped)
|
|
2509
2769
|
return;
|
|
2510
|
-
const watch2 = await harness.watchDoc(token, id,
|
|
2770
|
+
const watch2 = await harness.watchDoc(token, id, context3);
|
|
2511
2771
|
if (watch2 === undefined)
|
|
2512
2772
|
return;
|
|
2513
2773
|
if (stopped)
|
|
@@ -2541,7 +2801,7 @@ class GatewayClient {
|
|
|
2541
2801
|
await subscription?.subscription?.stop();
|
|
2542
2802
|
}
|
|
2543
2803
|
async#conversation(id) {
|
|
2544
|
-
const conversation = await this.#options.harness.conversation(id,
|
|
2804
|
+
const conversation = await this.#options.harness.conversation(id, context3);
|
|
2545
2805
|
if (conversation === undefined)
|
|
2546
2806
|
throw new Error(`Conversation ${id} does not exist`);
|
|
2547
2807
|
return conversation;
|
|
@@ -2557,9 +2817,9 @@ class GatewayClient {
|
|
|
2557
2817
|
async#call(method, args) {
|
|
2558
2818
|
switch (method) {
|
|
2559
2819
|
case "addProject":
|
|
2560
|
-
return addProject(this.#options.harness, args.path,
|
|
2820
|
+
return addProject(this.#options.harness, args.path, context3);
|
|
2561
2821
|
case "removeProject":
|
|
2562
|
-
await removeProject(this.#options.harness, args.path,
|
|
2822
|
+
await removeProject(this.#options.harness, args.path, context3);
|
|
2563
2823
|
return null;
|
|
2564
2824
|
case "createConversation": {
|
|
2565
2825
|
const create = args;
|
|
@@ -2574,14 +2834,14 @@ class GatewayClient {
|
|
|
2574
2834
|
...create.checkout === undefined ? {} : { checkout: create.checkout },
|
|
2575
2835
|
...create.model === undefined ? {} : { model: create.model },
|
|
2576
2836
|
...create.thinkingLevel === undefined ? {} : { thinkingLevel: create.thinkingLevel }
|
|
2577
|
-
}, settingsDefaults(this.#options.settings.value), this.#options.models,
|
|
2837
|
+
}, settingsDefaults(this.#options.settings.value), this.#options.models, context3);
|
|
2578
2838
|
return { conversationId };
|
|
2579
2839
|
}
|
|
2580
2840
|
case "archive": {
|
|
2581
2841
|
const input = args;
|
|
2582
|
-
await archive(this.#options.harness, input.conversationId, input.archived,
|
|
2842
|
+
await archive(this.#options.harness, input.conversationId, input.archived, context3);
|
|
2583
2843
|
if (input.archived) {
|
|
2584
|
-
const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId,
|
|
2844
|
+
const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId, context3);
|
|
2585
2845
|
for (const { record, reason } of kept) {
|
|
2586
2846
|
await this.send({ type: "notice", level: "warning", message: `Kept worktree ${record.path} (branch ${record.branch}): ${reason}` });
|
|
2587
2847
|
}
|
|
@@ -2596,10 +2856,17 @@ class GatewayClient {
|
|
|
2596
2856
|
}
|
|
2597
2857
|
case "revokeDevice": {
|
|
2598
2858
|
const { publicKey } = args;
|
|
2599
|
-
await revokeDevice(this.#options.harness, publicKey,
|
|
2859
|
+
await revokeDevice(this.#options.harness, publicKey, context3);
|
|
2600
2860
|
this.#hooks.closeDevice(publicKey);
|
|
2601
2861
|
return null;
|
|
2602
2862
|
}
|
|
2863
|
+
case "setPushToken": {
|
|
2864
|
+
const device = this.devicePublicKey;
|
|
2865
|
+
if (device === undefined)
|
|
2866
|
+
throw new Error("Push tokens register over the secure channel only");
|
|
2867
|
+
await setPushToken(this.#options.harness, device, args.token, context3);
|
|
2868
|
+
return null;
|
|
2869
|
+
}
|
|
2603
2870
|
case "logout": {
|
|
2604
2871
|
const modelAuth = this.#options.modelAuth;
|
|
2605
2872
|
if (modelAuth === undefined)
|
|
@@ -2642,9 +2909,9 @@ class GatewayClient {
|
|
|
2642
2909
|
const problem = imageProblem(images ?? []);
|
|
2643
2910
|
if (problem !== undefined)
|
|
2644
2911
|
throw new Error(problem);
|
|
2645
|
-
const dismissed = await this.#dismissPendingQuestions(conversation.id,
|
|
2646
|
-
const submission = await conversation.submit({ type: "input", content: messageContent(text, images), whenBusy: dismissed ? "steer" : whenBusy, requestId },
|
|
2647
|
-
submission.wait(
|
|
2912
|
+
const dismissed = await this.#dismissPendingQuestions(conversation.id, context3);
|
|
2913
|
+
const submission = await conversation.submit({ type: "input", content: messageContent(text, images), whenBusy: dismissed ? "steer" : whenBusy, requestId }, context3);
|
|
2914
|
+
submission.wait(context3).then((settled) => {
|
|
2648
2915
|
if (settled.status === "unanswered" && settled.reason !== "aborted") {
|
|
2649
2916
|
this.send({ type: "notice", level: "error", message: `No answer: ${settled.reason}` });
|
|
2650
2917
|
}
|
|
@@ -2668,16 +2935,16 @@ class GatewayClient {
|
|
|
2668
2935
|
const doc = await tx.doc(TitleDoc, conversation.id);
|
|
2669
2936
|
doc.title = trimmed;
|
|
2670
2937
|
doc.source = "manual";
|
|
2671
|
-
},
|
|
2938
|
+
}, context3);
|
|
2672
2939
|
return null;
|
|
2673
2940
|
}
|
|
2674
2941
|
case "abort":
|
|
2675
|
-
await conversation.abort(
|
|
2942
|
+
await conversation.abort(context3);
|
|
2676
2943
|
return null;
|
|
2677
2944
|
case "compact": {
|
|
2678
2945
|
const { instructions } = args;
|
|
2679
|
-
const taskId = await conversation.compact(instructions,
|
|
2680
|
-
this.#options.harness.waitForTask(taskId,
|
|
2946
|
+
const taskId = await conversation.compact(instructions, context3);
|
|
2947
|
+
this.#options.harness.waitForTask(taskId, context3).then((receipt) => void this.send({ type: "notice", level: "info", message: `Compaction ${receipt.state.outcome.status}.` }), () => {});
|
|
2681
2948
|
return { taskId };
|
|
2682
2949
|
}
|
|
2683
2950
|
case "setModel": {
|
|
@@ -2686,7 +2953,7 @@ class GatewayClient {
|
|
|
2686
2953
|
if (model === undefined)
|
|
2687
2954
|
throw new Error(`Unknown model: ${ref.provider}/${ref.modelId}`);
|
|
2688
2955
|
const thinking = (await this.#agent(conversation.id)).thinkingLevel ?? "off";
|
|
2689
|
-
await conversation.configure({ model: ref, thinkingLevel: clampThinkingLevel3(model, thinking) },
|
|
2956
|
+
await conversation.configure({ model: ref, thinkingLevel: clampThinkingLevel3(model, thinking) }, context3);
|
|
2690
2957
|
return null;
|
|
2691
2958
|
}
|
|
2692
2959
|
case "setThinkingLevel": {
|
|
@@ -2698,7 +2965,7 @@ class GatewayClient {
|
|
|
2698
2965
|
if (!getSupportedThinkingLevels(model).includes(level)) {
|
|
2699
2966
|
throw new Error(`Thinking level ${level} is not supported by ${agent.model.provider}/${agent.model.modelId}`);
|
|
2700
2967
|
}
|
|
2701
|
-
await conversation.configure({ thinkingLevel: level },
|
|
2968
|
+
await conversation.configure({ thinkingLevel: level }, context3);
|
|
2702
2969
|
return null;
|
|
2703
2970
|
}
|
|
2704
2971
|
case "fork": {
|
|
@@ -2722,13 +2989,13 @@ class GatewayClient {
|
|
|
2722
2989
|
}
|
|
2723
2990
|
cursor = page.next;
|
|
2724
2991
|
} while (cursor !== undefined);
|
|
2725
|
-
},
|
|
2726
|
-
const agent = await conversation.agent(
|
|
2992
|
+
}, context3);
|
|
2993
|
+
const agent = await conversation.agent(context3);
|
|
2727
2994
|
const remove = agent.tools.filter((tool) => removeTools.includes(tool.name));
|
|
2728
2995
|
const fork = await conversation.fork(at, {
|
|
2729
2996
|
ownership: { kind: "ownerless" },
|
|
2730
2997
|
...remove.length === 0 ? {} : { agent: { tools: { remove } } }
|
|
2731
|
-
},
|
|
2998
|
+
}, context3);
|
|
2732
2999
|
return { conversationId: fork.id };
|
|
2733
3000
|
});
|
|
2734
3001
|
}
|
|
@@ -2764,16 +3031,16 @@ class GatewayClient {
|
|
|
2764
3031
|
}
|
|
2765
3032
|
request.resolution = { outcome: "answered", answers, at: Date.now() };
|
|
2766
3033
|
return { first: true };
|
|
2767
|
-
},
|
|
3034
|
+
}, context3);
|
|
2768
3035
|
}
|
|
2769
3036
|
case "changes": {
|
|
2770
|
-
return await this.#changes(conversation,
|
|
3037
|
+
return await this.#changes(conversation, context3);
|
|
2771
3038
|
}
|
|
2772
3039
|
}
|
|
2773
3040
|
}
|
|
2774
|
-
async#dismissPendingQuestions(conversationId,
|
|
3041
|
+
async#dismissPendingQuestions(conversationId, context4) {
|
|
2775
3042
|
const questionDocs = (this.#options.docs ?? []).filter((doc) => doc.presentation === "pinomad.question");
|
|
2776
|
-
const snapshots = await Promise.all(questionDocs.map((doc) => this.#options.harness.snapshot(doc.token, conversationId,
|
|
3043
|
+
const snapshots = await Promise.all(questionDocs.map((doc) => this.#options.harness.snapshot(doc.token, conversationId, context4)));
|
|
2777
3044
|
const pending = snapshots.some((value) => Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined));
|
|
2778
3045
|
if (!pending)
|
|
2779
3046
|
return false;
|
|
@@ -2791,18 +3058,18 @@ class GatewayClient {
|
|
|
2791
3058
|
}
|
|
2792
3059
|
}
|
|
2793
3060
|
return dismissed;
|
|
2794
|
-
},
|
|
3061
|
+
}, context4);
|
|
2795
3062
|
}
|
|
2796
3063
|
get #chatsPrefix() {
|
|
2797
3064
|
return join6(this.#options.dataDir, "chats") + sep3;
|
|
2798
3065
|
}
|
|
2799
|
-
async#changes(conversation,
|
|
2800
|
-
const cwd = (await conversation.agent(
|
|
3066
|
+
async#changes(conversation, context4) {
|
|
3067
|
+
const cwd = (await conversation.agent(context4)).cwd;
|
|
2801
3068
|
if (cwd === undefined)
|
|
2802
3069
|
return { available: false, reason: "No working directory" };
|
|
2803
3070
|
if (cwd.startsWith(this.#chatsPrefix))
|
|
2804
3071
|
return { available: false, reason: "Chat conversations have no repository" };
|
|
2805
|
-
const index = await this.#options.harness.snapshot(IndexDoc,
|
|
3072
|
+
const index = await this.#options.harness.snapshot(IndexDoc, context4);
|
|
2806
3073
|
const record = checkoutAt(index?.checkouts, cwd);
|
|
2807
3074
|
if (record !== undefined) {
|
|
2808
3075
|
if (!await worktreeExists(record.path))
|
|
@@ -2815,7 +3082,7 @@ class GatewayClient {
|
|
|
2815
3082
|
return { available: true, ...await changesOf(base.repo, base.base) };
|
|
2816
3083
|
}
|
|
2817
3084
|
async#agent(id) {
|
|
2818
|
-
return await this.#options.harness.snapshot(AgentDoc3, id,
|
|
3085
|
+
return await this.#options.harness.snapshot(AgentDoc3, id, context3) ?? {};
|
|
2819
3086
|
}
|
|
2820
3087
|
async dispose() {
|
|
2821
3088
|
if (this.#disposed)
|
|
@@ -2829,7 +3096,7 @@ class GatewayClient {
|
|
|
2829
3096
|
}
|
|
2830
3097
|
|
|
2831
3098
|
// apps/host/src/host.ts
|
|
2832
|
-
var
|
|
3099
|
+
var context4 = BACKGROUND_CONTEXT4;
|
|
2833
3100
|
async function hostToken(dataDir) {
|
|
2834
3101
|
const path = join7(dataDir, "token");
|
|
2835
3102
|
try {
|
|
@@ -2898,7 +3165,7 @@ async function openHost(options) {
|
|
|
2898
3165
|
return;
|
|
2899
3166
|
const worktrees = worktreeRoot(options.dataDir);
|
|
2900
3167
|
if (cwd === worktrees || cwd.startsWith(worktrees + sep4)) {
|
|
2901
|
-
const index = await target.read.snapshot(IndexDoc,
|
|
3168
|
+
const index = await target.read.snapshot(IndexDoc, context4);
|
|
2902
3169
|
const record = checkoutAt(index?.checkouts, cwd);
|
|
2903
3170
|
if (record !== undefined) {
|
|
2904
3171
|
await ensureWorktree(record);
|
|
@@ -2918,11 +3185,11 @@ async function openHost(options) {
|
|
|
2918
3185
|
settings: typeof options.settings === "function" ? options.settings(hostSettings) : options.settings
|
|
2919
3186
|
},
|
|
2920
3187
|
onReport: (error) => report(error)
|
|
2921
|
-
},
|
|
2922
|
-
await ensureIndex(harness,
|
|
2923
|
-
await ensureDevices(harness,
|
|
3188
|
+
}, context4);
|
|
3189
|
+
await ensureIndex(harness, context4);
|
|
3190
|
+
await ensureDevices(harness, context4);
|
|
2924
3191
|
for (const path of options.projects ?? [])
|
|
2925
|
-
await addProject(harness, path,
|
|
3192
|
+
await addProject(harness, path, context4);
|
|
2926
3193
|
const hostKey = await loadHostKey(options.dataDir);
|
|
2927
3194
|
const offers = pairingOffers(options.remote?.pairingTtlMs);
|
|
2928
3195
|
const relayKey = options.relay === undefined ? undefined : await loadRelayKey(options.dataDir);
|
|
@@ -2943,6 +3210,13 @@ async function openHost(options) {
|
|
|
2943
3210
|
...options.modelAuth === undefined ? {} : { modelAuth: options.modelAuth },
|
|
2944
3211
|
...options.webRoot === undefined ? {} : { webRoot: options.webRoot },
|
|
2945
3212
|
...options.autoTitle === undefined ? {} : { autoTitle: options.autoTitle },
|
|
3213
|
+
...options.push === undefined ? {} : {
|
|
3214
|
+
push: {
|
|
3215
|
+
hostKey: toBase64Url(hostKey.publicKey),
|
|
3216
|
+
...options.push.fetch === undefined ? {} : { fetch: options.push.fetch },
|
|
3217
|
+
...options.push.timers === undefined ? {} : { timers: options.push.timers }
|
|
3218
|
+
}
|
|
3219
|
+
},
|
|
2946
3220
|
secure: {
|
|
2947
3221
|
hostKey,
|
|
2948
3222
|
offers,
|
|
@@ -2975,19 +3249,25 @@ async function openHost(options) {
|
|
|
2975
3249
|
harness,
|
|
2976
3250
|
closed: opened.closed,
|
|
2977
3251
|
settings: hostSettings,
|
|
3252
|
+
createAccountOffer: async () => {
|
|
3253
|
+
const binding = await readServerBinding(options.dataDir).catch(() => {
|
|
3254
|
+
return;
|
|
3255
|
+
});
|
|
3256
|
+
return binding === undefined ? undefined : offers.createAccount(binding);
|
|
3257
|
+
},
|
|
2978
3258
|
...gateway.remote === undefined || hostKey === undefined ? {} : { remote: { url: gateway.remote.url, advertiseUrl: gateway.remote.advertiseUrl, hostKey: hostKey.publicKey } },
|
|
2979
3259
|
...gateway.relay === undefined ? {} : { relay: { origin: gateway.relay.origin, hostId: gateway.relay.hostId, state: () => gateway.relay.state() } },
|
|
2980
3260
|
close() {
|
|
2981
3261
|
closing ??= (async () => {
|
|
2982
3262
|
try {
|
|
2983
3263
|
await gateway.close();
|
|
2984
|
-
await opened.close(
|
|
3264
|
+
await opened.close(context4);
|
|
2985
3265
|
await mcp?.close();
|
|
2986
3266
|
agentRoles?.close();
|
|
2987
3267
|
for (const each of extensions)
|
|
2988
3268
|
await each.close?.();
|
|
2989
3269
|
options.modelAuth?.close();
|
|
2990
|
-
await Promise.all([...environments.values()].map((env) => env.cleanup(
|
|
3270
|
+
await Promise.all([...environments.values()].map((env) => env.cleanup(context4)));
|
|
2991
3271
|
} finally {
|
|
2992
3272
|
await release();
|
|
2993
3273
|
}
|
|
@@ -3001,7 +3281,7 @@ async function openHost(options) {
|
|
|
3001
3281
|
for (const each of extensions)
|
|
3002
3282
|
await each.close?.().catch(() => {});
|
|
3003
3283
|
await mcp?.close().catch(() => {});
|
|
3004
|
-
await harness?.close(
|
|
3284
|
+
await harness?.close(context4).catch(() => {});
|
|
3005
3285
|
await release().catch(() => {});
|
|
3006
3286
|
throw error;
|
|
3007
3287
|
}
|
|
@@ -3210,10 +3490,17 @@ async function main(argv) {
|
|
|
3210
3490
|
}
|
|
3211
3491
|
});
|
|
3212
3492
|
const dataDir = resolve3(values["data-dir"] ?? DEFAULT_DATA_DIR);
|
|
3493
|
+
let relayOrigin;
|
|
3494
|
+
try {
|
|
3495
|
+
relayOrigin = resolveRelayOrigin(values.relay, await readServerConfig(dataDir));
|
|
3496
|
+
} catch (error) {
|
|
3497
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3498
|
+
process.exit(1);
|
|
3499
|
+
}
|
|
3213
3500
|
const extensionsFor = (models, summaries, options2) => (host2) => [
|
|
3214
3501
|
createContext({
|
|
3215
3502
|
agentsHome: join8(homedir3(), ".agents"),
|
|
3216
|
-
checkout: (input,
|
|
3503
|
+
checkout: (input, context5) => checkoutInfo(host2.harness(), input, context5)
|
|
3217
3504
|
}),
|
|
3218
3505
|
createCoding(options2),
|
|
3219
3506
|
todo,
|
|
@@ -3240,7 +3527,7 @@ async function main(argv) {
|
|
|
3240
3527
|
...values["public-url"] === undefined ? {} : { publicUrl: values["public-url"] }
|
|
3241
3528
|
}
|
|
3242
3529
|
},
|
|
3243
|
-
...
|
|
3530
|
+
...relayOrigin === undefined ? {} : { relay: { origin: relayOrigin } }
|
|
3244
3531
|
};
|
|
3245
3532
|
function fauxOptions(responses) {
|
|
3246
3533
|
const faux = fauxProvider({ tokensPerSecond: Number(values["faux-tps"]), tokenSize: { min: 1, max: 1 } });
|
|
@@ -3286,6 +3573,7 @@ async function main(argv) {
|
|
|
3286
3573
|
modelAuth,
|
|
3287
3574
|
extensions: extensionsFor(modelRuntime, summaries, { images }),
|
|
3288
3575
|
settings: (store) => createHarnessSettings(settingsManager, store),
|
|
3576
|
+
push: {},
|
|
3289
3577
|
settingsFallback: {
|
|
3290
3578
|
defaultModel: () => {
|
|
3291
3579
|
const model2 = defaultModel(settingsManager, modelRuntime);
|