@parlor/agent-host 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +16 -2
- package/dist/acp.d.ts +165 -0
- package/dist/acp.d.ts.map +1 -0
- package/dist/acp.js +626 -0
- package/dist/acp.js.map +1 -0
- package/dist/adapter-launch.d.ts +7 -0
- package/dist/adapter-launch.d.ts.map +1 -0
- package/dist/adapter-launch.js +35 -0
- package/dist/adapter-launch.js.map +1 -0
- package/dist/agents.d.ts +32 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +132 -0
- package/dist/agents.js.map +1 -0
- package/dist/assertion.d.ts +9 -0
- package/dist/assertion.d.ts.map +1 -0
- package/dist/assertion.js +22 -0
- package/dist/assertion.js.map +1 -0
- package/dist/control.d.ts +106 -0
- package/dist/control.d.ts.map +1 -0
- package/dist/control.js +32 -0
- package/dist/control.js.map +1 -0
- package/dist/daemon.d.ts +50 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +543 -0
- package/dist/daemon.js.map +1 -0
- package/dist/dev-host.d.ts +5 -0
- package/dist/dev-host.d.ts.map +1 -0
- package/dist/dev-host.js +29 -0
- package/dist/dev-host.js.map +1 -0
- package/dist/device-list.d.ts +6 -0
- package/dist/device-list.d.ts.map +1 -0
- package/dist/device-list.js +30 -0
- package/dist/device-list.js.map +1 -0
- package/dist/embedded.d.ts +6 -0
- package/dist/embedded.d.ts.map +1 -0
- package/dist/embedded.js +3 -0
- package/dist/embedded.js.map +1 -0
- package/dist/fold.d.ts +38 -0
- package/dist/fold.d.ts.map +1 -0
- package/dist/fold.js +165 -0
- package/dist/fold.js.map +1 -0
- package/dist/grants.d.ts +54 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +157 -0
- package/dist/grants.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/json-file.d.ts +6 -0
- package/dist/json-file.d.ts.map +1 -0
- package/dist/json-file.js +35 -0
- package/dist/json-file.js.map +1 -0
- package/dist/lines.d.ts +5 -0
- package/dist/lines.d.ts.map +1 -0
- package/dist/lines.js +9 -0
- package/dist/lines.js.map +1 -0
- package/dist/link-device.d.ts +28 -0
- package/dist/link-device.d.ts.map +1 -0
- package/dist/link-device.js +40 -0
- package/dist/link-device.js.map +1 -0
- package/dist/link.d.ts +52 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +277 -0
- package/dist/link.js.map +1 -0
- package/dist/lock.d.ts +7 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +105 -0
- package/dist/lock.js.map +1 -0
- package/dist/log.d.ts +3 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +18 -0
- package/dist/log.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +520 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp-proxy.d.ts +33 -0
- package/dist/mcp-proxy.d.ts.map +1 -0
- package/dist/mcp-proxy.js +317 -0
- package/dist/mcp-proxy.js.map +1 -0
- package/dist/naming.d.ts +7 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/naming.js +42 -0
- package/dist/naming.js.map +1 -0
- package/dist/pairing.d.ts +68 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +312 -0
- package/dist/pairing.js.map +1 -0
- package/dist/paths.d.ts +19 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +23 -0
- package/dist/paths.js.map +1 -0
- package/dist/policy.d.ts +37 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +60 -0
- package/dist/policy.js.map +1 -0
- package/dist/pool.d.ts +34 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +112 -0
- package/dist/pool.js.map +1 -0
- package/dist/prompter.d.ts +33 -0
- package/dist/prompter.d.ts.map +1 -0
- package/dist/prompter.js +98 -0
- package/dist/prompter.js.map +1 -0
- package/dist/release-main.d.ts +2 -0
- package/dist/release-main.d.ts.map +1 -0
- package/dist/release-main.js +50 -0
- package/dist/release-main.js.map +1 -0
- package/dist/release.d.ts +22 -0
- package/dist/release.d.ts.map +1 -0
- package/dist/release.js +81 -0
- package/dist/release.js.map +1 -0
- package/dist/revoke.d.ts +10 -0
- package/dist/revoke.d.ts.map +1 -0
- package/dist/revoke.js +23 -0
- package/dist/revoke.js.map +1 -0
- package/dist/self.d.ts +7 -0
- package/dist/self.d.ts.map +1 -0
- package/dist/self.js +10 -0
- package/dist/self.js.map +1 -0
- package/dist/service.d.ts +97 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +205 -0
- package/dist/service.js.map +1 -0
- package/dist/shell-path.d.ts +10 -0
- package/dist/shell-path.d.ts.map +1 -0
- package/dist/shell-path.js +64 -0
- package/dist/shell-path.js.map +1 -0
- package/dist/skeleton-kit.d.ts +8 -0
- package/dist/skeleton-kit.d.ts.map +1 -0
- package/dist/skeleton-kit.js +143 -0
- package/dist/skeleton-kit.js.map +1 -0
- package/dist/spec-kit.d.ts +102 -0
- package/dist/spec-kit.d.ts.map +1 -0
- package/dist/spec-kit.js +372 -0
- package/dist/spec-kit.js.map +1 -0
- package/dist/testing/fake-agent.d.ts +65 -0
- package/dist/testing/fake-agent.d.ts.map +1 -0
- package/dist/testing/fake-agent.js +238 -0
- package/dist/testing/fake-agent.js.map +1 -0
- package/dist/trust.d.ts +20 -0
- package/dist/trust.d.ts.map +1 -0
- package/dist/trust.js +27 -0
- package/dist/trust.js.map +1 -0
- package/dist/turn.d.ts +32 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +650 -0
- package/dist/turn.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +57 -3
package/dist/daemon.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AgentLauncher } from "./acp.js";
|
|
2
|
+
import { type GrantStore, type HostKeys } from "./grants.js";
|
|
3
|
+
import type { McpProxy } from "./mcp-proxy.js";
|
|
4
|
+
import type { HostPaths } from "./paths.js";
|
|
5
|
+
import { type RemotePolicy } from "./policy.js";
|
|
6
|
+
import type { SessionPool } from "./pool.js";
|
|
7
|
+
import { type LocalPrompter } from "./prompter.js";
|
|
8
|
+
import { type HostRelease } from "./service.js";
|
|
9
|
+
import type { TrustStore } from "./trust.js";
|
|
10
|
+
export interface DaemonDeps {
|
|
11
|
+
readonly release: HostRelease;
|
|
12
|
+
readonly paths: HostPaths;
|
|
13
|
+
readonly grants: GrantStore;
|
|
14
|
+
readonly keys: HostKeys;
|
|
15
|
+
readonly trust: TrustStore;
|
|
16
|
+
readonly pool: SessionPool;
|
|
17
|
+
readonly proxy: McpProxy;
|
|
18
|
+
readonly launcher: AgentLauncher;
|
|
19
|
+
readonly prompter: LocalPrompter;
|
|
20
|
+
readonly policy: RemotePolicy;
|
|
21
|
+
/** The `service.lock` token of the `ensureService` that spawned this daemon, which holds the lock for it. */
|
|
22
|
+
readonly lockToken?: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* One per user per computer, for every app: an `AppLink` per grant, the session pool,
|
|
26
|
+
* the proxy socket and the control socket. Started only through `ensureService`.
|
|
27
|
+
*/
|
|
28
|
+
export interface HostDaemon {
|
|
29
|
+
start(): Promise<void>;
|
|
30
|
+
connect(appOrigin: string): Promise<void>;
|
|
31
|
+
disconnect(appOrigin: string): Promise<void>;
|
|
32
|
+
/** Hands links over to a newer release; running turns finish here first. */
|
|
33
|
+
handover(): Promise<void>;
|
|
34
|
+
stop(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Agents start `parlor-host mcp-proxy` by name. An entry named otherwise, such as the
|
|
38
|
+
* published `dist/main.js` that `parlor dev` runs with node, gets a shim instead of a link.
|
|
39
|
+
*/
|
|
40
|
+
export declare function putHostCommandFirstOnPath(root: string, entry?: string | undefined): Promise<void>;
|
|
41
|
+
/** The daemon a service runs: once it handed over, `drained` settles after its last turn. */
|
|
42
|
+
export interface ServiceDaemon extends HostDaemon {
|
|
43
|
+
readonly drained: Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
export declare class HostAlreadyRunning extends Error {
|
|
46
|
+
readonly name = "HostAlreadyRunning";
|
|
47
|
+
constructor();
|
|
48
|
+
}
|
|
49
|
+
export declare function createHostDaemon(deps: DaemonDeps): ServiceDaemon;
|
|
50
|
+
//# sourceMappingURL=daemon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAI9C,OAAO,EAA8B,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAKzF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAQ/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAyC,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,KAAK,aAAa,EAAa,MAAM,eAAe,CAAC;AAI9D,OAAO,EAAE,KAAK,WAAW,EAAoC,MAAM,cAAc,CAAC;AAClF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,6GAA6G;IAC7G,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,4EAA4E;IAC5E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAeD;;;GAGG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,MAAM,GAAG,SAAuB,GACtC,OAAO,CAAC,IAAI,CAAC,CAqBf;AAWD,6FAA6F;AAC7F,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,SAAkB,IAAI,wBAAwB;IAC9C,cAEC;CACF;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,aAAa,CA4fhE"}
|
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { chmod, mkdir, readlink, realpath, rm, symlink, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import { basename, delimiter, dirname, join } from "node:path";
|
|
4
|
+
import { HOST_PROTOCOL, } from "@parlor/chat";
|
|
5
|
+
import { ActionError, asId, DEV_USER_ID } from "@parlor/core";
|
|
6
|
+
import { detectAgents, loadProfiles } from "./agents.js";
|
|
7
|
+
import { connectControl } from "./control.js";
|
|
8
|
+
import { requestDevHostToken } from "./dev-host.js";
|
|
9
|
+
import { devHostKeys } from "./grants.js";
|
|
10
|
+
import { onLines } from "./lines.js";
|
|
11
|
+
import { createAppLink } from "./link.js";
|
|
12
|
+
import { withInheritedLock } from "./lock.js";
|
|
13
|
+
import { logEvent, openHostLog } from "./log.js";
|
|
14
|
+
import { computerName, platformName, workspaceOf } from "./naming.js";
|
|
15
|
+
import { pairingDeviceOf, startLocalPairing, UnrevokedGrant, } from "./pairing.js";
|
|
16
|
+
import { DEV_POLICY, loadPolicy, presenceDueAt } from "./policy.js";
|
|
17
|
+
import { printable } from "./prompter.js";
|
|
18
|
+
import { compareVersions, RELEASE_KEY, signedRelease } from "./release.js";
|
|
19
|
+
import { revokeGrant } from "./revoke.js";
|
|
20
|
+
import { selfEntry } from "./self.js";
|
|
21
|
+
import { installLoginItem, installRelease } from "./service.js";
|
|
22
|
+
import { runTurn } from "./turn.js";
|
|
23
|
+
const CONNECT_TIMEOUT_MS = 15_000;
|
|
24
|
+
const OFFLINE_REPORT_MS = 1_000;
|
|
25
|
+
function incompatibleMessage(grant, welcome) {
|
|
26
|
+
return welcome.reason === "host_too_old"
|
|
27
|
+
? `Update Parlor Host to use ${grant.appName}: it speaks host protocol ${HOST_PROTOCOL}, and ${grant.appName} needs ${welcome.protocols.min}.`
|
|
28
|
+
: `${grant.appName} is older than this Parlor Host: it speaks host protocol ${welcome.protocols.max}, and this computer needs ${HOST_PROTOCOL}.`;
|
|
29
|
+
}
|
|
30
|
+
const shellQuote = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
|
|
31
|
+
/**
|
|
32
|
+
* Agents start `parlor-host mcp-proxy` by name. An entry named otherwise, such as the
|
|
33
|
+
* published `dist/main.js` that `parlor dev` runs with node, gets a shim instead of a link.
|
|
34
|
+
*/
|
|
35
|
+
export async function putHostCommandFirstOnPath(root, entry = selfEntry()) {
|
|
36
|
+
if (!entry)
|
|
37
|
+
return;
|
|
38
|
+
const bin = join(root, "bin");
|
|
39
|
+
const link = join(bin, "parlor-host");
|
|
40
|
+
await mkdir(bin, { recursive: true, mode: 0o700 });
|
|
41
|
+
const target = await realpath(entry);
|
|
42
|
+
if (basename(entry).startsWith("parlor-host")) {
|
|
43
|
+
if ((await readlink(link).catch(() => undefined)) !== target) {
|
|
44
|
+
await rm(link, { force: true });
|
|
45
|
+
await symlink(target, link);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
await rm(link, { force: true });
|
|
50
|
+
await writeFile(link, `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(target)} "$@"\n`, { mode: 0o700 });
|
|
51
|
+
}
|
|
52
|
+
const path = (process.env.PATH ?? "").split(delimiter).filter((dir) => dir !== bin);
|
|
53
|
+
process.env.PATH = [bin, ...path].join(delimiter);
|
|
54
|
+
}
|
|
55
|
+
export class HostAlreadyRunning extends Error {
|
|
56
|
+
name = "HostAlreadyRunning";
|
|
57
|
+
constructor() {
|
|
58
|
+
super("Parlor Host is already running on this computer.");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function createHostDaemon(deps) {
|
|
62
|
+
const { paths } = deps;
|
|
63
|
+
const links = new Map();
|
|
64
|
+
const devTokens = new Map();
|
|
65
|
+
const name = computerName();
|
|
66
|
+
const platform = platformName();
|
|
67
|
+
let profiles = [];
|
|
68
|
+
let agents = [];
|
|
69
|
+
let control;
|
|
70
|
+
let startup = Promise.resolve();
|
|
71
|
+
const clients = new Set();
|
|
72
|
+
const claim = async (link) => {
|
|
73
|
+
try {
|
|
74
|
+
return await link.api.calls.call("hostClaim", {});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (!(error instanceof ActionError && error.code === "not_found")) {
|
|
78
|
+
logEvent("claim_failed", {
|
|
79
|
+
appOrigin: link.grant.appOrigin,
|
|
80
|
+
error: String(error),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const running = new Set();
|
|
87
|
+
let handedOver = false;
|
|
88
|
+
let finishing = false;
|
|
89
|
+
let finishHandover = () => undefined;
|
|
90
|
+
const drained = new Promise((resolve) => (finishHandover = resolve));
|
|
91
|
+
const run = (state, claimed) => {
|
|
92
|
+
const { link } = state;
|
|
93
|
+
logEvent("turn_claimed", { appOrigin: link.grant.appOrigin, turnId: claimed.turnId });
|
|
94
|
+
state.turns += 1;
|
|
95
|
+
const turn = (link.grant.dev ? Promise.resolve(DEV_POLICY) : loadPolicy(paths.policy, deps.policy))
|
|
96
|
+
.then((policy) => runTurn(claimed, {
|
|
97
|
+
link,
|
|
98
|
+
profiles,
|
|
99
|
+
launcher: deps.launcher,
|
|
100
|
+
pool: deps.pool,
|
|
101
|
+
proxy: deps.proxy,
|
|
102
|
+
trust: {
|
|
103
|
+
...deps.trust,
|
|
104
|
+
pin: async (appOrigin, device) => {
|
|
105
|
+
await deps.trust.pin(appOrigin, device);
|
|
106
|
+
void report(state).catch(() => undefined);
|
|
107
|
+
},
|
|
108
|
+
forget: async (appOrigin, deviceId) => {
|
|
109
|
+
await deps.trust.forget(appOrigin, deviceId);
|
|
110
|
+
void report(state).catch(() => undefined);
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
policy,
|
|
114
|
+
prompter: deps.prompter,
|
|
115
|
+
now: Date.now,
|
|
116
|
+
pairing: {
|
|
117
|
+
secret: async () => (await deps.grants.get(link.grant.appOrigin))?.pairingSecret,
|
|
118
|
+
forget: async () => {
|
|
119
|
+
const saved = await deps.grants.get(link.grant.appOrigin);
|
|
120
|
+
if (!saved?.pairingSecret)
|
|
121
|
+
return;
|
|
122
|
+
const { pairingSecret: _used, ...rest } = saved;
|
|
123
|
+
await deps.grants.save(rest);
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}))
|
|
127
|
+
.then((outcome) => logEvent("turn_finished", { turnId: claimed.turnId, outcome }), (error) => logEvent("turn_finished", { turnId: claimed.turnId, error: String(error) }))
|
|
128
|
+
.finally(() => {
|
|
129
|
+
state.turns -= 1;
|
|
130
|
+
running.delete(turn);
|
|
131
|
+
settleHandover();
|
|
132
|
+
});
|
|
133
|
+
running.add(turn);
|
|
134
|
+
};
|
|
135
|
+
const claimLoop = async (state) => {
|
|
136
|
+
do {
|
|
137
|
+
state.again = false;
|
|
138
|
+
for (let claimed = await claim(state.link); claimed; claimed = state.link.mode === "serving" ? await claim(state.link) : undefined) {
|
|
139
|
+
run(state, claimed);
|
|
140
|
+
}
|
|
141
|
+
} while (state.again && state.link.mode === "serving");
|
|
142
|
+
};
|
|
143
|
+
const wake = (state) => {
|
|
144
|
+
if (state.link.mode !== "serving")
|
|
145
|
+
return;
|
|
146
|
+
if (state.claiming) {
|
|
147
|
+
state.again = true;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
state.claiming = claimLoop(state).finally(() => {
|
|
151
|
+
state.claiming = undefined;
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
const closeLink = async (origin, reason) => {
|
|
155
|
+
const state = links.get(origin);
|
|
156
|
+
links.delete(origin);
|
|
157
|
+
await state?.link.stop();
|
|
158
|
+
await deps.pool.drop(origin);
|
|
159
|
+
if (state)
|
|
160
|
+
logEvent("link_closed", { appOrigin: origin, reason });
|
|
161
|
+
};
|
|
162
|
+
const forget = async (grant) => {
|
|
163
|
+
devTokens.delete(grant.appOrigin);
|
|
164
|
+
await deps.grants.remove(grant.appOrigin);
|
|
165
|
+
await deps.keys.remove(grant.appOrigin);
|
|
166
|
+
await deps.trust.forget(grant.appOrigin);
|
|
167
|
+
await deps.pool.forget(grant);
|
|
168
|
+
};
|
|
169
|
+
const open = (grant, keys) => {
|
|
170
|
+
const origin = grant.appOrigin;
|
|
171
|
+
let state;
|
|
172
|
+
const link = createAppLink(grant, {
|
|
173
|
+
keys,
|
|
174
|
+
hostVersion: deps.release.version,
|
|
175
|
+
hostName: name,
|
|
176
|
+
platform,
|
|
177
|
+
fetch: (input, init) => fetch(input, init),
|
|
178
|
+
onWake: () => wake(state),
|
|
179
|
+
busy: () => state.turns > 0,
|
|
180
|
+
onRevoked: () => {
|
|
181
|
+
if (links.get(origin) !== state)
|
|
182
|
+
return;
|
|
183
|
+
if (grant.dev)
|
|
184
|
+
void reattach(grant);
|
|
185
|
+
else
|
|
186
|
+
void closeLink(origin, "revoked").then(() => forget(grant));
|
|
187
|
+
},
|
|
188
|
+
onIncompatible: (refused, welcome) => {
|
|
189
|
+
state.incompatible = incompatibleMessage(refused, welcome);
|
|
190
|
+
process.stderr.write(`${printable(state.incompatible)}\n`);
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
state = {
|
|
194
|
+
link,
|
|
195
|
+
keys,
|
|
196
|
+
claiming: undefined,
|
|
197
|
+
again: false,
|
|
198
|
+
incompatible: undefined,
|
|
199
|
+
turns: 0,
|
|
200
|
+
};
|
|
201
|
+
links.set(origin, state);
|
|
202
|
+
link.start();
|
|
203
|
+
return state;
|
|
204
|
+
};
|
|
205
|
+
const pinnedDevices = async (state) => {
|
|
206
|
+
if (state.link.grant.dev)
|
|
207
|
+
return {};
|
|
208
|
+
const [pinned, policy] = await Promise.all([
|
|
209
|
+
deps.trust.list(state.link.grant.appOrigin),
|
|
210
|
+
loadPolicy(paths.policy, deps.policy),
|
|
211
|
+
]);
|
|
212
|
+
const presenceDue = {};
|
|
213
|
+
for (const device of pinned) {
|
|
214
|
+
const due = presenceDueAt(policy, device);
|
|
215
|
+
if (due !== undefined)
|
|
216
|
+
presenceDue[device.deviceId] = due;
|
|
217
|
+
}
|
|
218
|
+
return { devices: pinned.map((device) => device.deviceId), presenceDue };
|
|
219
|
+
};
|
|
220
|
+
const report = async (state) => {
|
|
221
|
+
const reported = state.link.api.calls.call("hostReport", {
|
|
222
|
+
online: true,
|
|
223
|
+
agents,
|
|
224
|
+
...(await pinnedDevices(state)),
|
|
225
|
+
});
|
|
226
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("The app did not answer in time.")), CONNECT_TIMEOUT_MS));
|
|
227
|
+
try {
|
|
228
|
+
await Promise.race([reported, timeout]);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
throw new Error(state.incompatible ?? (error instanceof Error ? error.message : String(error)), { cause: error });
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const devGrant = async (origin) => {
|
|
235
|
+
const manifest = await fetch(`${origin}/_parlor/manifest`)
|
|
236
|
+
.then((response) => response.json())
|
|
237
|
+
.catch(() => undefined);
|
|
238
|
+
return {
|
|
239
|
+
appOrigin: origin,
|
|
240
|
+
appName: manifest?.app?.name ?? new URL(origin).host,
|
|
241
|
+
grantId: asId("grant", "dev"),
|
|
242
|
+
hostId: asId("host", "dev"),
|
|
243
|
+
account: { userId: DEV_USER_ID, name: "Developer" },
|
|
244
|
+
workspace: workspaceOf(paths.workspaces, origin),
|
|
245
|
+
createdAt: Date.now(),
|
|
246
|
+
dev: true,
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
const attach = async (origin, hostToken) => {
|
|
250
|
+
await closeLink(origin, "replaced");
|
|
251
|
+
devTokens.set(origin, hostToken);
|
|
252
|
+
await report(open(await devGrant(origin), devHostKeys(hostToken)));
|
|
253
|
+
};
|
|
254
|
+
const reattach = async (grant) => {
|
|
255
|
+
await closeLink(grant.appOrigin, "dev_server_restarted");
|
|
256
|
+
if (!devTokens.has(grant.appOrigin))
|
|
257
|
+
return;
|
|
258
|
+
try {
|
|
259
|
+
const token = await requestDevHostToken(grant.appOrigin);
|
|
260
|
+
if (devTokens.has(grant.appOrigin))
|
|
261
|
+
await attach(grant.appOrigin, token);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
logEvent("dev_attach_failed", { appOrigin: grant.appOrigin, error: String(error) });
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
const status = () => ({
|
|
268
|
+
state: "running",
|
|
269
|
+
version: deps.release.version,
|
|
270
|
+
apps: [...links.keys()],
|
|
271
|
+
});
|
|
272
|
+
const grantsOf = () => [...links.values()].map(({ link: { grant } }) => ({
|
|
273
|
+
appOrigin: grant.appOrigin,
|
|
274
|
+
appName: grant.appName,
|
|
275
|
+
grantId: grant.grantId,
|
|
276
|
+
}));
|
|
277
|
+
const daemon = {
|
|
278
|
+
drained,
|
|
279
|
+
async start() {
|
|
280
|
+
let finishStartup;
|
|
281
|
+
let failStartup;
|
|
282
|
+
startup = new Promise((resolve, reject) => {
|
|
283
|
+
finishStartup = resolve;
|
|
284
|
+
failStartup = reject;
|
|
285
|
+
});
|
|
286
|
+
startup.catch(() => undefined);
|
|
287
|
+
try {
|
|
288
|
+
await mkdir(paths.root, { recursive: true, mode: 0o700 });
|
|
289
|
+
await withInheritedLock(paths.lock, deps.lockToken, async () => {
|
|
290
|
+
const answering = await connectControl(paths.control);
|
|
291
|
+
if (answering) {
|
|
292
|
+
answering.close();
|
|
293
|
+
throw new HostAlreadyRunning();
|
|
294
|
+
}
|
|
295
|
+
await mkdir(dirname(paths.control), { recursive: true, mode: 0o700 });
|
|
296
|
+
control = createServer(serve);
|
|
297
|
+
await rm(paths.control, { force: true });
|
|
298
|
+
await new Promise((resolve, reject) => {
|
|
299
|
+
control.once("error", reject);
|
|
300
|
+
control.listen(paths.control, () => resolve());
|
|
301
|
+
});
|
|
302
|
+
await chmod(paths.control, 0o600);
|
|
303
|
+
});
|
|
304
|
+
openHostLog(join(paths.root, "logs", "daemon.log"));
|
|
305
|
+
await putHostCommandFirstOnPath(paths.root);
|
|
306
|
+
await deps.proxy.listen(paths.proxy);
|
|
307
|
+
profiles = await loadProfiles(paths.agents);
|
|
308
|
+
agents = await detectAgents(profiles);
|
|
309
|
+
finishStartup();
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
failStartup(error);
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
logEvent("daemon_started", {
|
|
316
|
+
version: deps.release.version,
|
|
317
|
+
agents: agents.map((a) => a.id),
|
|
318
|
+
});
|
|
319
|
+
for (const grant of await deps.grants.list()) {
|
|
320
|
+
if (grant.declined)
|
|
321
|
+
continue;
|
|
322
|
+
void report(open(grant, deps.keys)).catch((error) => logEvent("link_failed", { appOrigin: grant.appOrigin, error: String(error) }));
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
async connect(appOrigin) {
|
|
326
|
+
const grant = await deps.grants.get(appOrigin);
|
|
327
|
+
if (!grant || grant.declined) {
|
|
328
|
+
throw new Error(`This computer is not connected to ${appOrigin}.`);
|
|
329
|
+
}
|
|
330
|
+
const current = links.get(appOrigin);
|
|
331
|
+
if (current?.link.grant.grantId === grant.grantId) {
|
|
332
|
+
await report(current);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
await closeLink(appOrigin, "replaced");
|
|
336
|
+
if (current)
|
|
337
|
+
await deps.pool.forget(current.link.grant);
|
|
338
|
+
await report(open(grant, deps.keys));
|
|
339
|
+
},
|
|
340
|
+
async disconnect(appOrigin) {
|
|
341
|
+
const state = links.get(appOrigin);
|
|
342
|
+
const grant = state?.link.grant ?? (await deps.grants.get(appOrigin));
|
|
343
|
+
if (!grant)
|
|
344
|
+
throw new Error(`This computer is not connected to ${appOrigin}.`);
|
|
345
|
+
if (state) {
|
|
346
|
+
await state.link.stop({ revoke: true });
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
await revokeGrant({
|
|
350
|
+
appOrigin,
|
|
351
|
+
appName: grant.appName,
|
|
352
|
+
assert: (request) => deps.keys.assert(appOrigin, request),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
await closeLink(appOrigin, "disconnected");
|
|
356
|
+
await forget(grant);
|
|
357
|
+
},
|
|
358
|
+
async handover() {
|
|
359
|
+
if (handedOver)
|
|
360
|
+
return;
|
|
361
|
+
handedOver = true;
|
|
362
|
+
for (const state of links.values())
|
|
363
|
+
state.link.drain();
|
|
364
|
+
await Promise.all([...links.values()].map((state) => state.claiming));
|
|
365
|
+
const closing = control;
|
|
366
|
+
control = undefined;
|
|
367
|
+
closing?.close();
|
|
368
|
+
await Promise.all([rm(paths.control, { force: true }), deps.proxy.stopListening()]);
|
|
369
|
+
logEvent("handover_started", { version: deps.release.version });
|
|
370
|
+
},
|
|
371
|
+
async stop() {
|
|
372
|
+
if (!handedOver) {
|
|
373
|
+
await Promise.all([...links.values()].map((state) => Promise.race([
|
|
374
|
+
state.link.api.calls.call("hostReport", { online: false, agents }),
|
|
375
|
+
new Promise((resolve) => setTimeout(resolve, OFFLINE_REPORT_MS)),
|
|
376
|
+
]).catch(() => undefined)));
|
|
377
|
+
}
|
|
378
|
+
for (const origin of [...links.keys()])
|
|
379
|
+
await closeLink(origin, "stopped");
|
|
380
|
+
for (const socket of clients)
|
|
381
|
+
socket.destroy();
|
|
382
|
+
const closing = control;
|
|
383
|
+
control = undefined;
|
|
384
|
+
if (closing)
|
|
385
|
+
await new Promise((resolve) => closing.close(() => resolve()));
|
|
386
|
+
if (!handedOver)
|
|
387
|
+
await rm(paths.control, { force: true });
|
|
388
|
+
await deps.proxy.close();
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
const settleHandover = () => {
|
|
392
|
+
if (!handedOver || running.size > 0 || finishing)
|
|
393
|
+
return;
|
|
394
|
+
finishing = true;
|
|
395
|
+
void (async () => {
|
|
396
|
+
for (const origin of [...links.keys()])
|
|
397
|
+
await closeLink(origin, "handed_over");
|
|
398
|
+
await Promise.all([...clients].map((socket) => new Promise((resolve) => {
|
|
399
|
+
socket.end(() => resolve());
|
|
400
|
+
setTimeout(resolve, OFFLINE_REPORT_MS).unref();
|
|
401
|
+
})));
|
|
402
|
+
logEvent("handover_finished", { version: deps.release.version });
|
|
403
|
+
finishHandover();
|
|
404
|
+
})();
|
|
405
|
+
};
|
|
406
|
+
const handOverTo = async (binary, signature) => {
|
|
407
|
+
const release = await signedRelease(binary, signature, RELEASE_KEY);
|
|
408
|
+
if (!release)
|
|
409
|
+
return { ok: false, error: "unsigned" };
|
|
410
|
+
if (compareVersions(release.version, deps.release.version) <= 0) {
|
|
411
|
+
return { ok: false, error: "downgrade" };
|
|
412
|
+
}
|
|
413
|
+
if (!(await installRelease(paths, { binary, signature }, release))) {
|
|
414
|
+
return { ok: false, error: "unsigned" };
|
|
415
|
+
}
|
|
416
|
+
await installLoginItem(paths);
|
|
417
|
+
await daemon.handover();
|
|
418
|
+
setImmediate(settleHandover);
|
|
419
|
+
return { ok: true };
|
|
420
|
+
};
|
|
421
|
+
const pairings = new Map();
|
|
422
|
+
const pendingPairing = (appOrigin) => {
|
|
423
|
+
const origin = new URL(appOrigin).origin;
|
|
424
|
+
return { origin, pairing: pairings.get(origin) };
|
|
425
|
+
};
|
|
426
|
+
const confirmPairing = async (request) => {
|
|
427
|
+
const { origin, pairing } = pendingPairing(request.appOrigin);
|
|
428
|
+
if (!pairing)
|
|
429
|
+
return { ok: false, error: "No pairing was started for this app." };
|
|
430
|
+
pairings.delete(origin);
|
|
431
|
+
let outcome;
|
|
432
|
+
try {
|
|
433
|
+
outcome = await pairing.confirm(request.connect);
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
if (!(error instanceof UnrevokedGrant))
|
|
437
|
+
throw error;
|
|
438
|
+
await deps.grants.save(error.grant);
|
|
439
|
+
return { ok: false, error: "declined_locally" };
|
|
440
|
+
}
|
|
441
|
+
if (!outcome.paired)
|
|
442
|
+
return { ok: false, error: outcome.reason };
|
|
443
|
+
const { grant } = outcome;
|
|
444
|
+
await deps.grants.save(grant);
|
|
445
|
+
const device = pairingDeviceOf(request.device);
|
|
446
|
+
if (device) {
|
|
447
|
+
await deps.trust.pin(grant.appOrigin, {
|
|
448
|
+
deviceId: asId("device", device.deviceId),
|
|
449
|
+
signingKey: device.signingKey,
|
|
450
|
+
presenceKey: device.presenceKey,
|
|
451
|
+
name: device.name,
|
|
452
|
+
platform: device.platform,
|
|
453
|
+
approvedAt: Date.now(),
|
|
454
|
+
via: "pairing",
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
await daemon.connect(grant.appOrigin);
|
|
458
|
+
return { ok: true };
|
|
459
|
+
};
|
|
460
|
+
const handle = async (request) => {
|
|
461
|
+
if (request.op !== "hello")
|
|
462
|
+
await startup;
|
|
463
|
+
switch (request.op) {
|
|
464
|
+
case "hello":
|
|
465
|
+
return { ok: true, release: deps.release };
|
|
466
|
+
case "handover":
|
|
467
|
+
return handOverTo(request.binary, request.signature);
|
|
468
|
+
case "status":
|
|
469
|
+
return { ok: true, status: status(), grants: grantsOf() };
|
|
470
|
+
case "begin_local_pairing": {
|
|
471
|
+
const { origin } = pendingPairing(request.appOrigin);
|
|
472
|
+
const existing = await deps.grants.get(origin);
|
|
473
|
+
if (existing && !existing.declined) {
|
|
474
|
+
return {
|
|
475
|
+
ok: false,
|
|
476
|
+
error: `This computer is already connected to ${existing.appName}.`,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const pairing = startLocalPairing({
|
|
480
|
+
appOrigin: origin,
|
|
481
|
+
hostName: name,
|
|
482
|
+
platform,
|
|
483
|
+
keys: deps.keys,
|
|
484
|
+
workspaceRoot: paths.workspaces,
|
|
485
|
+
});
|
|
486
|
+
pairings.set(origin, pairing);
|
|
487
|
+
const begun = await pairing.pairing;
|
|
488
|
+
logEvent("local_pairing_begun", {
|
|
489
|
+
appOrigin: origin,
|
|
490
|
+
matchingCode: begun.matchingCode,
|
|
491
|
+
});
|
|
492
|
+
return { ok: true, pairing: begun };
|
|
493
|
+
}
|
|
494
|
+
case "finish_local_pairing": {
|
|
495
|
+
const { pairing } = pendingPairing(request.appOrigin);
|
|
496
|
+
if (!pairing)
|
|
497
|
+
return { ok: false, error: "No pairing was started for this app." };
|
|
498
|
+
return { ok: true, ...(await pairing.finish(request.code)) };
|
|
499
|
+
}
|
|
500
|
+
case "confirm_local_pairing":
|
|
501
|
+
return confirmPairing(request);
|
|
502
|
+
case "connect":
|
|
503
|
+
await daemon.connect(request.appOrigin);
|
|
504
|
+
return { ok: true };
|
|
505
|
+
case "disconnect":
|
|
506
|
+
await daemon.disconnect(request.appOrigin);
|
|
507
|
+
return { ok: true };
|
|
508
|
+
case "dev_attach":
|
|
509
|
+
await attach(request.appOrigin, request.hostToken);
|
|
510
|
+
return { ok: true };
|
|
511
|
+
case "dev_detach":
|
|
512
|
+
devTokens.delete(request.appOrigin);
|
|
513
|
+
await closeLink(request.appOrigin, "detached");
|
|
514
|
+
return { ok: true };
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
function serve(socket) {
|
|
518
|
+
clients.add(socket);
|
|
519
|
+
socket.on("close", () => clients.delete(socket));
|
|
520
|
+
socket.on("error", () => socket.destroy());
|
|
521
|
+
onLines(socket, (line) => {
|
|
522
|
+
let request;
|
|
523
|
+
try {
|
|
524
|
+
request = JSON.parse(line);
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
socket.write(`${JSON.stringify({ ok: false, error: "Not JSON." })}\n`);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
void handle(request)
|
|
531
|
+
.catch((error) => ({
|
|
532
|
+
ok: false,
|
|
533
|
+
error: error instanceof Error ? error.message : String(error),
|
|
534
|
+
}))
|
|
535
|
+
.then((reply) => {
|
|
536
|
+
if (!socket.destroyed)
|
|
537
|
+
socket.write(`${JSON.stringify(reply ?? { ok: false, error: "Unknown op." })}\n`);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
return daemon;
|
|
542
|
+
}
|
|
543
|
+
//# sourceMappingURL=daemon.js.map
|