@cydm/happy-elves 0.1.0-beta.70 → 0.1.0-beta.71
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/README.md +1 -1
- package/apps/cli/dist/commands/account.js +8 -2
- package/apps/cli/dist/commands/config.js +1 -1
- package/apps/cli/dist/commands/daemon.js +180 -25
- package/apps/cli/dist/commands/gateway.js +230 -7
- package/apps/cli/dist/commands/lib/args.js +6 -0
- package/apps/cli/dist/commands/lib/bootstrap-config.js +8 -8
- package/apps/cli/dist/commands/lib/bootstrap-daemon.d.ts +13 -2
- package/apps/cli/dist/commands/lib/bootstrap-daemon.js +99 -4
- package/apps/cli/dist/commands/lib/bootstrap-join.d.ts +1 -0
- package/apps/cli/dist/commands/lib/bootstrap-join.js +199 -0
- package/apps/cli/dist/commands/lib/bootstrap-output.d.ts +2 -5
- package/apps/cli/dist/commands/lib/bootstrap-output.js +6 -9
- package/apps/cli/dist/commands/lib/bootstrap.js +20 -15
- package/apps/cli/dist/commands/lib/config.d.ts +1 -0
- package/apps/cli/dist/commands/lib/config.js +3 -0
- package/apps/cli/dist/commands/lib/daemon-enrollment-intent.d.ts +47 -0
- package/apps/cli/dist/commands/lib/daemon-enrollment-intent.js +257 -0
- package/apps/cli/dist/commands/lib/doctor.js +10 -10
- package/apps/cli/dist/commands/lib/exit.js +8 -8
- package/apps/cli/dist/commands/lib/index.d.ts +4 -2
- package/apps/cli/dist/commands/lib/index.js +4 -2
- package/apps/cli/dist/commands/lib/local-daemon.d.ts +0 -1
- package/apps/cli/dist/commands/lib/local-daemon.js +0 -3
- package/apps/cli/dist/commands/lib/orchestrator.d.ts +3 -1
- package/apps/cli/dist/commands/lib/orchestrator.js +43 -5
- package/apps/cli/dist/commands/lib/scope.d.ts +11 -2
- package/apps/cli/dist/commands/lib/scope.js +27 -1
- package/apps/cli/dist/commands/lib/status.js +2 -2
- package/apps/cli/dist/commands/lib/usage.js +63 -35
- package/apps/cli/dist/commands/lib/workspace.js +2 -2
- package/apps/cli/dist/commands/loop.js +79 -18
- package/apps/cli/dist/commands/machine.js +10 -3
- package/apps/cli/dist/commands/memory.js +90 -1
- package/apps/cli/dist/commands/orchestrator.d.ts +1 -0
- package/apps/cli/dist/commands/orchestrator.js +45 -10
- package/apps/cli/dist/commands/remote.js +53 -14
- package/apps/cli/dist/commands/session.d.ts +1 -0
- package/apps/cli/dist/commands/session.js +60 -22
- package/apps/cli/dist/commands/skill.d.ts +8 -0
- package/apps/cli/dist/commands/skill.js +54 -9
- package/apps/cli/dist/skills/registry.d.ts +1 -1
- package/apps/cli/dist/skills/registry.js +12 -1
- package/apps/daemon/dist/gateway/commands.js +2 -0
- package/apps/daemon/dist/gateway/runtime.js +13 -3
- package/apps/daemon/dist/lifecycle/artifacts.d.ts +44 -0
- package/apps/daemon/dist/lifecycle/artifacts.js +588 -0
- package/apps/daemon/dist/lifecycle/capability.d.ts +25 -0
- package/apps/daemon/dist/lifecycle/capability.js +290 -0
- package/apps/daemon/dist/lifecycle/detached-helper-bundle.mjs +18505 -0
- package/apps/daemon/dist/lifecycle/detached-helper-cli.d.ts +1 -0
- package/apps/daemon/dist/lifecycle/detached-helper-cli.js +11 -0
- package/apps/daemon/dist/lifecycle/errors.d.ts +3 -0
- package/apps/daemon/dist/lifecycle/errors.js +10 -0
- package/apps/daemon/dist/lifecycle/helper.d.ts +39 -0
- package/apps/daemon/dist/lifecycle/helper.js +403 -0
- package/apps/daemon/dist/lifecycle/index.d.ts +7 -0
- package/apps/daemon/dist/lifecycle/index.js +6 -0
- package/apps/daemon/dist/lifecycle/journal.d.ts +21 -0
- package/apps/daemon/dist/lifecycle/journal.js +389 -0
- package/apps/daemon/dist/lifecycle/npm.d.ts +37 -0
- package/apps/daemon/dist/lifecycle/npm.js +164 -0
- package/apps/daemon/dist/lifecycle/operation.d.ts +19 -0
- package/apps/daemon/dist/lifecycle/operation.js +175 -0
- package/apps/daemon/dist/lifecycle/relay.d.ts +5 -0
- package/apps/daemon/dist/lifecycle/relay.js +72 -0
- package/apps/daemon/dist/lifecycle/reporter.d.ts +29 -0
- package/apps/daemon/dist/lifecycle/reporter.js +402 -0
- package/apps/daemon/dist/lifecycle/restart.d.ts +127 -0
- package/apps/daemon/dist/lifecycle/restart.js +650 -0
- package/apps/daemon/dist/lifecycle/runtime.d.ts +36 -0
- package/apps/daemon/dist/lifecycle/runtime.js +210 -0
- package/apps/daemon/dist/lifecycle/types.d.ts +110 -0
- package/apps/daemon/dist/lifecycle/types.js +3 -0
- package/apps/daemon/dist/paths.d.ts +4 -0
- package/apps/daemon/dist/paths.js +4 -0
- package/apps/daemon/dist/relay/connection.js +37 -0
- package/apps/daemon/dist/relay/register.d.ts +3 -0
- package/apps/daemon/dist/relay/register.js +49 -1
- package/apps/daemon/dist/relay/send.d.ts +3 -1
- package/apps/daemon/dist/relay/send.js +5 -2
- package/apps/daemon/dist/runtime.js +5 -2
- package/apps/daemon/dist/session/lifecycle.d.ts +1 -1
- package/apps/daemon/dist/session/lifecycle.js +107 -46
- package/apps/daemon/dist/session/primitives.js +37 -3
- package/apps/daemon/dist/session/prompt.js +39 -3
- package/apps/daemon/dist/start.js +10 -0
- package/apps/daemon/dist/turn-coordinator.d.ts +8 -0
- package/apps/daemon/dist/turn-coordinator.js +28 -1
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/account-access-revocation.d.ts +39 -0
- package/apps/relay/dist/account-access-revocation.js +67 -0
- package/apps/relay/dist/account-http-routes.d.ts +3 -0
- package/apps/relay/dist/account-http-routes.js +650 -0
- package/apps/relay/dist/account-projections.d.ts +40 -0
- package/apps/relay/dist/account-projections.js +96 -0
- package/apps/relay/dist/controller-handlers.d.ts +3 -1
- package/apps/relay/dist/controller-handlers.js +33 -6
- package/apps/relay/dist/db.d.ts +6 -0
- package/apps/relay/dist/db.js +137 -25
- package/apps/relay/dist/enrollment-repository.d.ts +61 -0
- package/apps/relay/dist/enrollment-repository.js +113 -0
- package/apps/relay/dist/http-routes.d.ts +1 -1
- package/apps/relay/dist/http-routes.js +280 -72
- package/apps/relay/dist/http-schemas.d.ts +33 -10
- package/apps/relay/dist/http-schemas.js +9 -10
- package/apps/relay/dist/index.js +28 -8
- package/apps/relay/dist/machine-command-result-handlers.d.ts +21 -1
- package/apps/relay/dist/machine-command-result-handlers.js +363 -17
- package/apps/relay/dist/relay-context.d.ts +5 -3
- package/apps/relay/dist/relay-context.js +56 -36
- package/apps/relay/dist/retention.d.ts +6 -0
- package/apps/relay/dist/retention.js +26 -2
- package/apps/relay/dist/scope.js +7 -0
- package/apps/relay/dist/session-projection-reducer.js +19 -15
- package/apps/relay/dist/types.d.ts +20 -12
- package/apps/relay/dist/websocket.js +9 -0
- package/build-identity.json +3 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/packages/client/dist/account-management.d.ts +64 -0
- package/packages/client/dist/account-management.js +486 -0
- package/packages/client/dist/account.d.ts +7 -2
- package/packages/client/dist/account.js +55 -30
- package/packages/client/dist/client.d.ts +36 -3
- package/packages/client/dist/client.js +379 -11
- package/packages/client/dist/http.d.ts +1 -1
- package/packages/client/dist/index.d.ts +5 -2
- package/packages/client/dist/index.js +2 -0
- package/packages/client/dist/live-types.d.ts +16 -0
- package/packages/client/dist/live.d.ts +5 -0
- package/packages/client/dist/live.js +110 -8
- package/packages/client/dist/parsers.d.ts +1 -3
- package/packages/client/dist/parsers.js +1 -36
- package/packages/client/dist/transport.js +16 -0
- package/packages/client/dist/types.d.ts +66 -2
- package/packages/runtime-cli/dist/backend-session-lease.d.ts +40 -0
- package/packages/runtime-cli/dist/backend-session-lease.js +561 -0
- package/packages/runtime-cli/dist/claude-history.d.ts +1 -1
- package/packages/runtime-cli/dist/claude-history.js +23 -22
- package/packages/runtime-cli/dist/claude-session-store.d.ts +0 -1
- package/packages/runtime-cli/dist/claude-session-store.js +1 -7
- package/packages/runtime-cli/dist/claude-turn.d.ts +4 -0
- package/packages/runtime-cli/dist/claude-turn.js +98 -6
- package/packages/runtime-cli/dist/codex-app-server.d.ts +8 -1
- package/packages/runtime-cli/dist/codex-app-server.js +10 -1
- package/packages/runtime-cli/dist/codex-json-rpc.d.ts +5 -0
- package/packages/runtime-cli/dist/codex-json-rpc.js +84 -5
- package/packages/runtime-cli/dist/codex-lifecycle.js +16 -3
- package/packages/runtime-cli/dist/index.js +98 -51
- package/packages/runtime-cli/dist/process-identity.d.ts +15 -0
- package/packages/runtime-cli/dist/process-identity.js +102 -0
- package/packages/runtime-cli/dist/session-store.d.ts +27 -0
- package/packages/runtime-cli/dist/session-store.js +213 -17
- package/packages/shared/dist/account.d.ts +184 -0
- package/packages/shared/dist/account.js +205 -0
- package/packages/shared/dist/crypto.d.ts +27 -1
- package/packages/shared/dist/crypto.js +112 -4
- package/packages/shared/dist/daemon-lifecycle.d.ts +85 -0
- package/packages/shared/dist/daemon-lifecycle.js +66 -0
- package/packages/shared/dist/index.d.ts +2 -0
- package/packages/shared/dist/index.js +2 -0
- package/packages/shared/dist/node/gateway-store.d.ts +3 -1
- package/packages/shared/dist/node/gateway-store.js +7 -0
- package/packages/shared/dist/protocol-schemas.d.ts +80 -26
- package/packages/shared/dist/protocol-schemas.js +4 -0
- package/packages/shared/dist/protocol-types.d.ts +35 -0
- package/packages/shared/dist/protocol.d.ts +33 -2
- package/packages/shared/dist/protocol.js +36 -0
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Public beta CLI for Happy Elves.
|
|
|
8
8
|
happy-elves start
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Reconnect this Device to an existing Account with a one-time invite from Settings → Devices → Add Device (the CLI command name is retained for compatibility):
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
happy-elves remote join-controller --url "<invite-url>" --recover-controller --json
|
|
@@ -261,7 +261,7 @@ export async function handleAccount({ domain, action, flags }) {
|
|
|
261
261
|
response = await fetch(`${relayUrl}/api/pairing/start`, {
|
|
262
262
|
method: "POST",
|
|
263
263
|
headers: { "content-type": "application/json" },
|
|
264
|
-
body: JSON.stringify({ deviceId, deviceName }),
|
|
264
|
+
body: JSON.stringify({ deviceId, deviceName, surface: "cli" }),
|
|
265
265
|
signal: AbortSignal.timeout(accountCreateRelayTimeoutMs),
|
|
266
266
|
});
|
|
267
267
|
}
|
|
@@ -292,7 +292,13 @@ export async function handleAccount({ domain, action, flags }) {
|
|
|
292
292
|
expiresAt: paired.expiresAt,
|
|
293
293
|
};
|
|
294
294
|
});
|
|
295
|
-
|
|
295
|
+
if (flags["reveal-secrets"] === true) {
|
|
296
|
+
ok("account.create", created);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const { controllerToken: _controllerToken, accountSecret: _accountSecret, ...safeCreated } = created;
|
|
300
|
+
ok("account.create", safeCreated);
|
|
301
|
+
}
|
|
296
302
|
return true;
|
|
297
303
|
}
|
|
298
304
|
if (domain === "account" && action === "reset") {
|
|
@@ -24,7 +24,7 @@ export async function handleConfig({ domain, action, flags }) {
|
|
|
24
24
|
const redacted = redactedControllerConfig(config);
|
|
25
25
|
console.log(`Config: ${configPath}`);
|
|
26
26
|
console.log(`Relay: ${redacted.relayUrl}`);
|
|
27
|
-
console.log(`
|
|
27
|
+
console.log(`Device credential: ${redacted.hasControllerToken ? "configured" : "missing"}`);
|
|
28
28
|
console.log(`Account secret: ${redacted.hasAccountSecret ? "configured" : "missing"}`);
|
|
29
29
|
return true;
|
|
30
30
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { daemonLifecycleRequestIdV1Schema, } from "../../../../packages/shared/dist/index.js";
|
|
5
|
+
import { assertLocalDaemonRuntimeMatchesConfig, assertLocalDaemonVerified, canonicalRelayUrl, cliErrorFromFetch, CliError, compactText, ControllerClient, configDir, configPath, daemonPidPath, deriveOrchestration, diagnoseInstallation, emitDoctorResult, ensureDaemonRunning, idleSessionStatuses, localDaemonStatus, ok, parsePairingClaimResponse, randomId, readConfig, readDaemonConfig, readDaemonProjectionBaseline, readLocalAuditLog, readOptionalControllerConfig, readRelayJson, redactedDaemonConfig, requirePositional, requireRelayUrl, requireString, showMachine, startDaemonTimeoutMs, stopLocalDaemon, stopSpawnedDaemon, throwRelayHttpError, waitForDaemonReadiness, waitForDaemonSelfReadiness, wantsJson, withAccountConfigTransaction } from "./lib/index.js";
|
|
5
6
|
function positiveLimit(flags, fallback) {
|
|
6
7
|
const value = flags.limit;
|
|
7
8
|
if (value === undefined)
|
|
@@ -28,6 +29,114 @@ async function boundedMap(values, concurrency, map) {
|
|
|
28
29
|
function overviewUnsupported(error) {
|
|
29
30
|
return error.code === "OVERVIEW_UNSUPPORTED";
|
|
30
31
|
}
|
|
32
|
+
function assertControllerCanObserveDaemon(controllerConfig, daemonConfig) {
|
|
33
|
+
if (canonicalRelayUrl(controllerConfig.relayUrl, "Controller config") !==
|
|
34
|
+
canonicalRelayUrl(daemonConfig.relayUrl, "Daemon config") ||
|
|
35
|
+
!pairSecretsEqual(controllerConfig.accountSecret, daemonConfig.accountSecret)) {
|
|
36
|
+
throw new CliError("Controller and daemon configurations belong to different workspaces. Local daemon readiness cannot be verified and no process was changed.", "CONFIG_SPLIT_BRAIN");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function waitForLocalDaemonReadiness(client, config, baseline, daemon, requireFreshProjection) {
|
|
40
|
+
try {
|
|
41
|
+
if (!client) {
|
|
42
|
+
const local = await localDaemonStatus();
|
|
43
|
+
assertLocalDaemonVerified(local);
|
|
44
|
+
if (!local.running) {
|
|
45
|
+
throw new CliError("Daemon exited before its local runtime identity became ready. Run happy-elves daemon doctor and happy-elves daemon logs.", "DAEMON_START_FAILED");
|
|
46
|
+
}
|
|
47
|
+
await assertLocalDaemonRuntimeMatchesConfig(config);
|
|
48
|
+
await waitForDaemonSelfReadiness(config, daemon.instanceGeneration, { timeoutMs: startDaemonTimeoutMs });
|
|
49
|
+
return { local, relayAuthenticated: "verified" };
|
|
50
|
+
}
|
|
51
|
+
const readiness = await waitForDaemonReadiness(client, config.machineId, {
|
|
52
|
+
baseline,
|
|
53
|
+
expectedInstanceGeneration: daemon.instanceGeneration,
|
|
54
|
+
requireFreshProjection,
|
|
55
|
+
timeoutMs: startDaemonTimeoutMs,
|
|
56
|
+
});
|
|
57
|
+
if (readiness.phase === "process-exited" || readiness.phase === "owner-changed") {
|
|
58
|
+
throw new CliError("Daemon exited before it authenticated with the relay. Run happy-elves daemon doctor and happy-elves daemon logs.", "DAEMON_START_FAILED");
|
|
59
|
+
}
|
|
60
|
+
if (readiness.phase !== "relay-authenticated" || !readiness.machine?.online) {
|
|
61
|
+
const relayDetail = readiness.lastRelayErrorCode ? ` Last relay error: ${readiness.lastRelayErrorCode}.` : "";
|
|
62
|
+
throw new CliError(`Daemon is running, but it did not authenticate with the relay before the startup deadline.${relayDetail} Run happy-elves daemon doctor and happy-elves daemon logs.`, "DAEMON_ONLINE_TIMEOUT");
|
|
63
|
+
}
|
|
64
|
+
return { ...readiness, relayAuthenticated: "verified" };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (daemon.startedHandle) {
|
|
68
|
+
try {
|
|
69
|
+
await stopSpawnedDaemon(daemon.startedHandle);
|
|
70
|
+
}
|
|
71
|
+
catch (cleanupError) {
|
|
72
|
+
throw new CliError(`Daemon readiness failed and its owned child could not be cleaned up: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`, "DAEMON_START_CLEANUP_FAILED");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function remoteDaemonLifecycle(flags, action) {
|
|
79
|
+
const machineId = requireString(flags, "machine");
|
|
80
|
+
if (flags["request-id"] === true)
|
|
81
|
+
throw new CliError("Missing --request-id value", "MISSING_ARGUMENT");
|
|
82
|
+
const explicitRequestId = typeof flags["request-id"] === "string" ? flags["request-id"] : undefined;
|
|
83
|
+
const requestId = explicitRequestId ?? randomId("life");
|
|
84
|
+
if (!daemonLifecycleRequestIdV1Schema.safeParse(requestId).success) {
|
|
85
|
+
throw new CliError("Invalid --request-id", "INVALID_ARGUMENT");
|
|
86
|
+
}
|
|
87
|
+
const config = await readConfig(flags);
|
|
88
|
+
const client = new ControllerClient(config);
|
|
89
|
+
if (explicitRequestId !== undefined) {
|
|
90
|
+
const retained = await client.queryDaemonLifecycleOperation(machineId, requestId);
|
|
91
|
+
if (retained) {
|
|
92
|
+
const semanticsMatch = retained.daemonId === machineId &&
|
|
93
|
+
retained.action === action &&
|
|
94
|
+
(action === "upgrade" ? retained.channel === "beta" : retained.channel === undefined);
|
|
95
|
+
if (!semanticsMatch) {
|
|
96
|
+
throw new CliError(`Lifecycle request id is already bound to another operation: ${requestId}`, "REQUEST_ID_CONFLICT");
|
|
97
|
+
}
|
|
98
|
+
ok(`daemon.${action}`, { operation: retained }, { requestId, machineId });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const snapshot = await client.snapshot();
|
|
103
|
+
const matches = snapshot.machines.filter((machine) => machine.id === machineId);
|
|
104
|
+
const machine = matches[0];
|
|
105
|
+
if (!machine)
|
|
106
|
+
throw new CliError(`Daemon not found: ${machineId}`, "MACHINE_NOT_FOUND");
|
|
107
|
+
if (matches.length !== 1) {
|
|
108
|
+
throw new CliError(`Relay returned duplicate Daemon identities: ${machineId}`, "RELAY_RESPONSE_INVALID");
|
|
109
|
+
}
|
|
110
|
+
if (!machine.online)
|
|
111
|
+
throw new CliError(`Daemon is offline: ${machineId}`, "MACHINE_OFFLINE");
|
|
112
|
+
const capability = machine.capabilities.daemonLifecycleV1;
|
|
113
|
+
const expectedInstanceGeneration = machine.capabilities.daemonInstanceGeneration;
|
|
114
|
+
const supported = capability !== undefined &&
|
|
115
|
+
expectedInstanceGeneration !== undefined &&
|
|
116
|
+
capability[action] &&
|
|
117
|
+
(action !== "upgrade" || (capability.installKind === "npm-global" && capability.channel === "beta"));
|
|
118
|
+
if (!supported) {
|
|
119
|
+
throw new CliError(`Daemon does not support remote ${action}: ${machineId}`, "DAEMON_LIFECYCLE_UNSUPPORTED");
|
|
120
|
+
}
|
|
121
|
+
const request = action === "upgrade"
|
|
122
|
+
? {
|
|
123
|
+
version: 1,
|
|
124
|
+
requestId,
|
|
125
|
+
daemonId: machineId,
|
|
126
|
+
expectedInstanceGeneration,
|
|
127
|
+
action,
|
|
128
|
+
channel: "beta",
|
|
129
|
+
}
|
|
130
|
+
: {
|
|
131
|
+
version: 1,
|
|
132
|
+
requestId,
|
|
133
|
+
daemonId: machineId,
|
|
134
|
+
expectedInstanceGeneration,
|
|
135
|
+
action,
|
|
136
|
+
};
|
|
137
|
+
const operation = await client.startDaemonLifecycleOperation(request);
|
|
138
|
+
ok(`daemon.${action}`, { operation }, { requestId, machineId });
|
|
139
|
+
}
|
|
31
140
|
async function daemonOverview(client, input) {
|
|
32
141
|
try {
|
|
33
142
|
const sessions = [];
|
|
@@ -157,8 +266,8 @@ export async function handleDaemon({ domain, action, flags }) {
|
|
|
157
266
|
console.log(`Config: ${path.join(configDir, "daemon.json")}`);
|
|
158
267
|
console.log(`Relay: ${redacted.relayUrl}`);
|
|
159
268
|
console.log(`Account: ${redacted.accountId}`);
|
|
160
|
-
console.log(`
|
|
161
|
-
console.log(`
|
|
269
|
+
console.log(`Daemon: ${redacted.machineName} (${redacted.machineId})`);
|
|
270
|
+
console.log(`Daemon credential: ${redacted.hasMachineToken ? "configured" : "missing"}`);
|
|
162
271
|
console.log(`Account secret: ${redacted.hasAccountSecret ? "configured" : "missing"}`);
|
|
163
272
|
return true;
|
|
164
273
|
}
|
|
@@ -200,20 +309,25 @@ export async function handleDaemon({ domain, action, flags }) {
|
|
|
200
309
|
return true;
|
|
201
310
|
}
|
|
202
311
|
if (domain === "daemon" && action === "start") {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
await
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
|
|
312
|
+
assertLocalDaemonVerified(await localDaemonStatus());
|
|
313
|
+
const config = await readDaemonConfig();
|
|
314
|
+
assertDaemonRelayOverrideMatches(flags, config);
|
|
315
|
+
const controllerConfig = await readOptionalControllerConfig();
|
|
316
|
+
if (controllerConfig)
|
|
317
|
+
assertControllerCanObserveDaemon(controllerConfig, config);
|
|
318
|
+
const client = controllerConfig ? new ControllerClient(controllerConfig) : undefined;
|
|
319
|
+
const baseline = client
|
|
320
|
+
? await readDaemonProjectionBaseline(client, config.machineId, { timeoutMs: startDaemonTimeoutMs })
|
|
321
|
+
: undefined;
|
|
322
|
+
const daemon = await ensureDaemonRunning(config);
|
|
323
|
+
const readiness = await waitForLocalDaemonReadiness(client, config, baseline, daemon, daemon.started);
|
|
324
|
+
const data = {
|
|
325
|
+
...readiness.local,
|
|
326
|
+
started: daemon.started,
|
|
327
|
+
machineId: readiness.machine?.id ?? config.machineId,
|
|
328
|
+
relayAuthenticated: readiness.relayAuthenticated,
|
|
329
|
+
...(readiness.relayAuthenticated === "verified" ? { online: true } : {}),
|
|
330
|
+
};
|
|
217
331
|
if (wantsJson(flags))
|
|
218
332
|
ok("daemon.start", data);
|
|
219
333
|
else
|
|
@@ -224,12 +338,43 @@ export async function handleDaemon({ domain, action, flags }) {
|
|
|
224
338
|
ok("daemon.stop", await stopLocalDaemon());
|
|
225
339
|
return true;
|
|
226
340
|
}
|
|
341
|
+
if (domain === "daemon" && action === "restart" && flags.machine === true) {
|
|
342
|
+
throw new CliError("Missing --machine value", "MISSING_ARGUMENT");
|
|
343
|
+
}
|
|
344
|
+
if (domain === "daemon" && action === "restart" && typeof flags.machine === "string") {
|
|
345
|
+
await remoteDaemonLifecycle(flags, "restart");
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
if (domain === "daemon" && action === "upgrade") {
|
|
349
|
+
await remoteDaemonLifecycle(flags, "upgrade");
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
227
352
|
if (domain === "daemon" && action === "restart") {
|
|
228
|
-
await
|
|
353
|
+
assertLocalDaemonVerified(await localDaemonStatus());
|
|
354
|
+
const config = await readDaemonConfig();
|
|
355
|
+
assertDaemonRelayOverrideMatches(flags, config);
|
|
356
|
+
const controllerConfig = await readOptionalControllerConfig();
|
|
357
|
+
if (controllerConfig)
|
|
358
|
+
assertControllerCanObserveDaemon(controllerConfig, config);
|
|
359
|
+
const client = controllerConfig ? new ControllerClient(controllerConfig) : undefined;
|
|
360
|
+
const baseline = client
|
|
361
|
+
? await readDaemonProjectionBaseline(client, config.machineId, { timeoutMs: startDaemonTimeoutMs })
|
|
362
|
+
: undefined;
|
|
229
363
|
const stop = await stopLocalDaemon();
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
364
|
+
if (stop.running || stop.alive) {
|
|
365
|
+
throw new CliError("Daemon did not stop before the restart deadline; no replacement process was started.", "DAEMON_STOP_FAILED");
|
|
366
|
+
}
|
|
367
|
+
const daemon = await ensureDaemonRunning(config);
|
|
368
|
+
const readiness = await waitForLocalDaemonReadiness(client, config, baseline, daemon, true);
|
|
369
|
+
ok("daemon.restart", {
|
|
370
|
+
stop,
|
|
371
|
+
startedPid: daemon.startedPid ?? readiness.local.pid,
|
|
372
|
+
pidPath: daemonPidPath,
|
|
373
|
+
local: readiness.local,
|
|
374
|
+
machineId: readiness.machine?.id ?? config.machineId,
|
|
375
|
+
relayAuthenticated: readiness.relayAuthenticated,
|
|
376
|
+
...(readiness.relayAuthenticated === "verified" ? { online: true } : {}),
|
|
377
|
+
});
|
|
233
378
|
return true;
|
|
234
379
|
}
|
|
235
380
|
if (domain === "daemon" && action === "status" && flags.local === true) {
|
|
@@ -264,7 +409,7 @@ export async function handleDaemon({ domain, action, flags }) {
|
|
|
264
409
|
const overview = await daemonOverview(client, { machineId: machineFilter, includeSessions, all, limit, cursor });
|
|
265
410
|
const machines = overview.machines;
|
|
266
411
|
if (machineFilter && machines.length === 0)
|
|
267
|
-
throw new CliError("
|
|
412
|
+
throw new CliError("Daemon not found", "MACHINE_NOT_FOUND");
|
|
268
413
|
const projectedMachines = includeSessions
|
|
269
414
|
? await Promise.all((await client.snapshot()).machines
|
|
270
415
|
.filter((machine) => machines.some((summary) => summary.id === machine.id))
|
|
@@ -322,9 +467,9 @@ export async function handleDaemon({ domain, action, flags }) {
|
|
|
322
467
|
},
|
|
323
468
|
};
|
|
324
469
|
if (!wantsJson(flags)) {
|
|
325
|
-
console.log(`
|
|
470
|
+
console.log(`Account: ${data.relayReachable ? "connected" : "unavailable"}`);
|
|
326
471
|
console.log(data.local.running ? `Local daemon: running${data.local.pid ? ` pid ${data.local.pid}` : ""}` : "Local daemon: stopped");
|
|
327
|
-
console.log(`
|
|
472
|
+
console.log(`Daemons: ${data.summary.onlineMachineCount}/${data.summary.machineCount} online`);
|
|
328
473
|
console.log(`Sessions: ${data.summary.sessionCount} total, ${data.summary.runningSessionCount} running`);
|
|
329
474
|
for (const machine of data.machines) {
|
|
330
475
|
console.log(`- ${machine.name} (${machine.machineId}) ${machine.online ? "online" : "offline"}`);
|
|
@@ -333,7 +478,7 @@ export async function handleDaemon({ domain, action, flags }) {
|
|
|
333
478
|
}
|
|
334
479
|
if (includeSessions) {
|
|
335
480
|
for (const session of projectedSessions) {
|
|
336
|
-
console.log(`- ${session.sessionId} ${session.status} ${session.orchestration.state} ${session.agent}
|
|
481
|
+
console.log(`- ${session.sessionId} ${session.status} ${session.orchestration.state} ${session.agent} daemon:${session.machineId}`);
|
|
337
482
|
}
|
|
338
483
|
console.log(`Page: ${overview.page.returned} of ${overview.page.total} sessions${overview.page.hasMore ? "; more available" : "; complete"}`);
|
|
339
484
|
if (overview.page.nextCursor)
|
|
@@ -358,6 +503,16 @@ function writeHumanDaemonStartOutput(data) {
|
|
|
358
503
|
else {
|
|
359
504
|
console.log(`Daemon is already running${data.pid ? ` (pid ${data.pid})` : ""}.`);
|
|
360
505
|
}
|
|
506
|
+
if (data.relayAuthenticated === "unverified") {
|
|
507
|
+
console.log("Local daemon identity is ready; relay authentication was not verified because this host has no matching Device credential.");
|
|
508
|
+
}
|
|
361
509
|
console.log("This command manages only the paired background daemon; it does not open a browser page.");
|
|
362
510
|
console.log("For setup or browser onboarding, run: happy-elves start");
|
|
363
511
|
}
|
|
512
|
+
function assertDaemonRelayOverrideMatches(flags, config) {
|
|
513
|
+
if (typeof flags.relay !== "string")
|
|
514
|
+
return;
|
|
515
|
+
if (canonicalRelayUrl(flags.relay) === canonicalRelayUrl(config.relayUrl))
|
|
516
|
+
return;
|
|
517
|
+
throw new CliError("daemon start/restart cannot change the enrolled Account. Use a one-time Re-enroll invite from the Account UI.", "CONFIG_SPLIT_BRAIN");
|
|
518
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { ControllerClient } from "../../../../packages/client/dist/index.js";
|
|
5
|
+
import { assertLocalDaemonVerified, CliError, configDir, daemonPidPath, localDaemonStatus, ok, parseCsvFlag, randomId, readConfig, requirePositional, requireString, wantsJson, } from "./lib/index.js";
|
|
6
|
+
import { canonicalizeGatewayIdentityInputV1, } from "../../../../packages/shared/dist/index.js";
|
|
5
7
|
import { gatewayStateFootprint, gatewayStateFootprintIsEmpty, gatewaySourceStateDigestV1, inspectGatewayStoreArtifacts, readGatewayStateFile, readGatewayStoreFile, repairGatewayStoreArtifacts, updateGatewayStoreFile, updateGatewayStoresFile, } from "../../../../packages/shared/dist/node/gateway-store.js";
|
|
6
8
|
const defaultGatewayInboundMode = "mention";
|
|
7
9
|
const defaultGatewayProcessingReaction = {
|
|
8
10
|
enabled: true,
|
|
9
11
|
emojiType: "Typing",
|
|
10
12
|
};
|
|
13
|
+
const defaultNewGatewayPermissionMode = "approve-reads";
|
|
11
14
|
const gatewayConfigPath = path.join(configDir, "gateways.json");
|
|
12
15
|
const gatewayStatePath = path.join(configDir, "gateway-state.json");
|
|
13
16
|
const telegramApiBase = "https://api.telegram.org";
|
|
@@ -15,6 +18,12 @@ const telegramGetMeTimeoutMs = 10_000;
|
|
|
15
18
|
export async function handleGateway({ domain, action, positional, flags }) {
|
|
16
19
|
if (domain !== "gateway")
|
|
17
20
|
return false;
|
|
21
|
+
if (flags.machine !== undefined) {
|
|
22
|
+
if (typeof flags.machine !== "string" || !flags.machine.trim()) {
|
|
23
|
+
throw new CliError("Missing value for --machine", "MISSING_ARGUMENT");
|
|
24
|
+
}
|
|
25
|
+
return await handleRemoteGateway(action, positional, flags, flags.machine.trim());
|
|
26
|
+
}
|
|
18
27
|
if (action === "store" && positional[0] === "inspect") {
|
|
19
28
|
const inspection = await inspectGatewayStoreArtifacts({
|
|
20
29
|
configPath: gatewayConfigPath,
|
|
@@ -60,7 +69,7 @@ export async function handleGateway({ domain, action, positional, flags }) {
|
|
|
60
69
|
}
|
|
61
70
|
for (const gateway of store.gateways) {
|
|
62
71
|
const destination = gateway.defaultDestination ? ` chat:${gateway.defaultDestination.platformChatId}` : "";
|
|
63
|
-
console.log(`${gateway.id} ${gateway.enabled ? "enabled" : "disabled"} ${gateway.platform} inbound:${gateway.inboundMode} agent:${gateway.defaultAgent}${destination}`);
|
|
72
|
+
console.log(`${gateway.id} ${gateway.enabled ? "enabled" : "disabled"} ${gateway.platform} inbound:${gateway.inboundMode} permission:${gateway.permissionMode ?? "approve-all"} agent:${gateway.defaultAgent}${destination}`);
|
|
64
73
|
}
|
|
65
74
|
return true;
|
|
66
75
|
}
|
|
@@ -88,6 +97,7 @@ export async function handleGateway({ domain, action, positional, flags }) {
|
|
|
88
97
|
inboundMode: inboundModeFlag(flags, defaultGatewayInboundMode),
|
|
89
98
|
processingReaction: processingReactionFromFlags(flags, defaultGatewayProcessingReaction),
|
|
90
99
|
allowAllUsers: flags["allow-all-users"] === true,
|
|
100
|
+
permissionMode: permissionModeFlag(flags, defaultNewGatewayPermissionMode),
|
|
91
101
|
identities: [],
|
|
92
102
|
createdAt: now,
|
|
93
103
|
updatedAt: now,
|
|
@@ -120,6 +130,7 @@ export async function handleGateway({ domain, action, positional, flags }) {
|
|
|
120
130
|
console.log(`inbound: ${gateway.inboundMode}`);
|
|
121
131
|
console.log(`reaction: ${gateway.processingReaction.enabled ? gateway.processingReaction.emojiType : "disabled"}`);
|
|
122
132
|
console.log(`default: ${gateway.defaultAgent} ${gateway.defaultCwd}`);
|
|
133
|
+
console.log(`permission: ${gateway.permissionMode ?? "approve-all"}`);
|
|
123
134
|
if (gateway.defaultDestination)
|
|
124
135
|
console.log(`default chat: ${gateway.defaultDestination.platformChatId}`);
|
|
125
136
|
console.log(`identities: ${gateway.identities.length}`);
|
|
@@ -309,6 +320,7 @@ export async function handleGateway({ domain, action, positional, flags }) {
|
|
|
309
320
|
defaultCwd: process.cwd(),
|
|
310
321
|
inboundMode: defaultGatewayInboundMode,
|
|
311
322
|
processingReaction: { ...defaultGatewayProcessingReaction },
|
|
323
|
+
permissionMode: defaultNewGatewayPermissionMode,
|
|
312
324
|
identities: [],
|
|
313
325
|
createdAt: now,
|
|
314
326
|
}),
|
|
@@ -356,17 +368,27 @@ export async function handleGateway({ domain, action, positional, flags }) {
|
|
|
356
368
|
const platformUserId = stringFlag(flags, "platform-user") ?? stringFlag(flags, "user");
|
|
357
369
|
if (!platformUserId)
|
|
358
370
|
throw new CliError("Missing --platform-user <id>", "MISSING_ARGUMENT");
|
|
359
|
-
const allowedActions = parseCsvFlag(flags.actions) ?? ["session.create", "session.run"];
|
|
360
371
|
const allowedSessions = parseCsvFlag(flags.sessions);
|
|
372
|
+
const allowedActions = parseCsvFlag(flags.actions) ?? (allowedSessions?.length ? ["session.run"] : ["session.create", "session.run"]);
|
|
373
|
+
let canonicalIdentity;
|
|
374
|
+
try {
|
|
375
|
+
canonicalIdentity = canonicalizeGatewayIdentityInputV1({
|
|
376
|
+
platformUserId,
|
|
377
|
+
...(stringFlag(flags, "display-name") ? { displayName: stringFlag(flags, "display-name") } : {}),
|
|
378
|
+
allowedActions,
|
|
379
|
+
...(allowedSessions ? { allowedSessions } : {}),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
throw new CliError(error instanceof Error ? error.message : "Gateway identity is invalid", "GATEWAY_IDENTITY_INVALID");
|
|
384
|
+
}
|
|
361
385
|
const result = await updateGatewayStoreFile(gatewayConfigPath, (store) => {
|
|
362
386
|
const gateway = requireGateway(store, gatewayId);
|
|
363
387
|
const existingGrant = gateway.identities.find((identity) => identity.platformUserId === platformUserId);
|
|
364
388
|
const grant = {
|
|
365
389
|
...existingGrant,
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
allowedActions,
|
|
369
|
-
allowedSessions,
|
|
390
|
+
...canonicalIdentity,
|
|
391
|
+
allowedSessions: canonicalIdentity.allowedSessions,
|
|
370
392
|
};
|
|
371
393
|
const updated = {
|
|
372
394
|
...gateway,
|
|
@@ -488,6 +510,197 @@ export async function handleGateway({ domain, action, positional, flags }) {
|
|
|
488
510
|
}
|
|
489
511
|
return false;
|
|
490
512
|
}
|
|
513
|
+
async function handleRemoteGateway(action, positional, flags, machineId) {
|
|
514
|
+
const client = new ControllerClient(await readConfig(flags));
|
|
515
|
+
if (action === "list") {
|
|
516
|
+
const result = await client.listGateways(machineId);
|
|
517
|
+
if (!wantsJson(flags)) {
|
|
518
|
+
if (result.gateways.length === 0)
|
|
519
|
+
console.log(`No gateways on Daemon ${machineId}.`);
|
|
520
|
+
for (const gateway of result.gateways)
|
|
521
|
+
printRemoteGateway(gateway);
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
ok("gateway.list", result, { machineId });
|
|
525
|
+
}
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
if (action === "status") {
|
|
529
|
+
const gatewayId = requirePositional(positional[0], "gatewayId");
|
|
530
|
+
const gateway = await client.getGateway(machineId, gatewayId);
|
|
531
|
+
if (!wantsJson(flags))
|
|
532
|
+
printRemoteGateway(gateway, true);
|
|
533
|
+
else
|
|
534
|
+
ok("gateway.status", { machineId, gateway }, { machineId });
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
if (action === "create") {
|
|
538
|
+
const platform = platformFlag(flags);
|
|
539
|
+
const id = stringFlag(flags, "id") ?? randomId("gw");
|
|
540
|
+
const defaultChatId = stringFlag(flags, "default-chat-id");
|
|
541
|
+
const gateway = await client.createGateway(machineId, {
|
|
542
|
+
id,
|
|
543
|
+
platform,
|
|
544
|
+
name: stringFlag(flags, "name") ?? `${platform} gateway`,
|
|
545
|
+
defaultAgent: stringFlag(flags, "default-agent") ?? stringFlag(flags, "agent") ?? "codex",
|
|
546
|
+
defaultCwd: stringFlag(flags, "default-cwd") ?? stringFlag(flags, "cwd") ?? process.cwd(),
|
|
547
|
+
...(defaultChatId ? { defaultDestination: { platformChatId: defaultChatId, contextKind: "main" } } : {}),
|
|
548
|
+
inboundMode: inboundModeFlag(flags, defaultGatewayInboundMode),
|
|
549
|
+
processingReaction: processingReactionFromFlags(flags, defaultGatewayProcessingReaction),
|
|
550
|
+
allowAllUsers: allowAllUsersFlag(flags, false),
|
|
551
|
+
permissionMode: permissionModeFlag(flags, defaultNewGatewayPermissionMode),
|
|
552
|
+
}, { ...(stringFlag(flags, "request-id") ? { requestId: stringFlag(flags, "request-id") } : {}) });
|
|
553
|
+
if (!wantsJson(flags))
|
|
554
|
+
console.log(`Created gateway ${gateway.id} on Daemon ${machineId}.`);
|
|
555
|
+
else
|
|
556
|
+
ok("gateway.create", { machineId, gateway }, { machineId });
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
if (action === "config" && positional[0] === "set") {
|
|
560
|
+
const gatewayId = requirePositional(positional[1], "gatewayId");
|
|
561
|
+
const current = await client.getGateway(machineId, gatewayId);
|
|
562
|
+
const defaultChatId = stringFlag(flags, "default-chat-id");
|
|
563
|
+
if (flags["clear-default-chat"] === true && defaultChatId) {
|
|
564
|
+
throw new CliError("Use only one of --default-chat-id or --clear-default-chat", "INVALID_ARGUMENT");
|
|
565
|
+
}
|
|
566
|
+
const gateway = await client.updateGatewayConfig(machineId, gatewayId, {
|
|
567
|
+
name: stringFlag(flags, "name") ?? current.name,
|
|
568
|
+
enabled: enabledFlag(flags, current.enabled),
|
|
569
|
+
inboundMode: inboundModeFlag(flags, current.inboundMode),
|
|
570
|
+
processingReaction: processingReactionFromFlags(flags, current.processingReaction),
|
|
571
|
+
defaultAgent: stringFlag(flags, "default-agent") ?? stringFlag(flags, "agent") ?? current.defaultAgent,
|
|
572
|
+
defaultCwd: stringFlag(flags, "default-cwd") ?? stringFlag(flags, "cwd") ?? current.defaultCwd,
|
|
573
|
+
allowAllUsers: allowAllUsersFlag(flags, current.allowAllUsers),
|
|
574
|
+
permissionMode: permissionModeFlag(flags, current.permissionMode ?? "approve-all"),
|
|
575
|
+
...(flags["clear-default-chat"] === true
|
|
576
|
+
? { defaultDestination: null }
|
|
577
|
+
: defaultChatId
|
|
578
|
+
? { defaultDestination: { platformChatId: defaultChatId, contextKind: "main" } }
|
|
579
|
+
: current.defaultDestination
|
|
580
|
+
? { defaultDestination: current.defaultDestination }
|
|
581
|
+
: {}),
|
|
582
|
+
});
|
|
583
|
+
if (!wantsJson(flags))
|
|
584
|
+
console.log(`Updated gateway ${gatewayId} on Daemon ${machineId}.`);
|
|
585
|
+
else
|
|
586
|
+
ok("gateway.config.set", { machineId, gateway }, { machineId });
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
if (action === "secret" && positional[0] === "set") {
|
|
590
|
+
const gatewayId = requirePositional(positional[1], "gatewayId");
|
|
591
|
+
const platform = platformFlag(flags);
|
|
592
|
+
const secret = platform === "telegram"
|
|
593
|
+
? { platform, botToken: botTokenFlag(flags) }
|
|
594
|
+
: platform === "feishu"
|
|
595
|
+
? { platform, appId: requireString(flags, "app-id"), appSecret: appSecretFlag(flags), domain: domainFlag(flags) }
|
|
596
|
+
: { platform };
|
|
597
|
+
const gateway = await client.setGatewaySecret(machineId, gatewayId, secret);
|
|
598
|
+
if (!wantsJson(flags))
|
|
599
|
+
console.log(`Saved gateway secret for ${gatewayId} on Daemon ${machineId}.`);
|
|
600
|
+
else
|
|
601
|
+
ok("gateway.secret.set", { machineId, gatewayId, platform, hasSecret: gateway.hasSecret }, { machineId });
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
if (action === "identity" && positional[0] === "allow") {
|
|
605
|
+
const gatewayId = requirePositional(positional[1], "gatewayId");
|
|
606
|
+
const platformUserId = stringFlag(flags, "platform-user") ?? stringFlag(flags, "user");
|
|
607
|
+
if (!platformUserId)
|
|
608
|
+
throw new CliError("Missing --platform-user <id>", "MISSING_ARGUMENT");
|
|
609
|
+
const allowedSessions = parseCsvFlag(flags.sessions);
|
|
610
|
+
const allowedActions = parseCsvFlag(flags.actions) ?? (allowedSessions?.length ? ["session.run"] : ["session.create", "session.run"]);
|
|
611
|
+
let identity;
|
|
612
|
+
try {
|
|
613
|
+
identity = canonicalizeGatewayIdentityInputV1({
|
|
614
|
+
platformUserId,
|
|
615
|
+
...(stringFlag(flags, "display-name") ? { displayName: stringFlag(flags, "display-name") } : {}),
|
|
616
|
+
allowedActions,
|
|
617
|
+
...(allowedSessions ? { allowedSessions } : {}),
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
throw new CliError(error instanceof Error ? error.message : "Gateway identity is invalid", "GATEWAY_IDENTITY_INVALID");
|
|
622
|
+
}
|
|
623
|
+
const gateway = await client.upsertGatewayIdentity(machineId, gatewayId, identity);
|
|
624
|
+
if (!wantsJson(flags))
|
|
625
|
+
console.log(`Allowed ${platformUserId} on gateway ${gatewayId} (Daemon ${machineId}).`);
|
|
626
|
+
else
|
|
627
|
+
ok("gateway.identity.allow", { machineId, gateway, identity }, { machineId });
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
if (action === "identity" && positional[0] === "remove") {
|
|
631
|
+
const gatewayId = requirePositional(positional[1], "gatewayId");
|
|
632
|
+
const platformUserId = stringFlag(flags, "platform-user") ?? stringFlag(flags, "user");
|
|
633
|
+
if (!platformUserId)
|
|
634
|
+
throw new CliError("Missing --platform-user <id>", "MISSING_ARGUMENT");
|
|
635
|
+
const gateway = await client.removeGatewayIdentity(machineId, gatewayId, platformUserId);
|
|
636
|
+
if (!wantsJson(flags))
|
|
637
|
+
console.log(`Removed ${platformUserId} from gateway ${gatewayId} (Daemon ${machineId}).`);
|
|
638
|
+
else
|
|
639
|
+
ok("gateway.identity.remove", { machineId, gateway, removedPlatformUserId: platformUserId }, { machineId });
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
if (action === "binding" && positional[0] === "list") {
|
|
643
|
+
const gatewayId = requirePositional(positional[1], "gatewayId");
|
|
644
|
+
const result = await client.listGatewayBindings(machineId, gatewayId);
|
|
645
|
+
if (!wantsJson(flags)) {
|
|
646
|
+
if (result.bindings.length === 0)
|
|
647
|
+
console.log(`No bindings for gateway ${gatewayId} on Daemon ${machineId}.`);
|
|
648
|
+
for (const binding of result.bindings) {
|
|
649
|
+
const thread = binding.contextKind === "thread" ? ` thread:${binding.contextKey ?? ""}` : "";
|
|
650
|
+
console.log(`${binding.sessionId} chat:${binding.platformChatId}${thread} agent:${binding.agent} cwd:${binding.cwd}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
else
|
|
654
|
+
ok("gateway.binding.list", { machineId, ...result }, { machineId });
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
if (action === "binding" && positional[0] === "clear") {
|
|
658
|
+
const gatewayId = requirePositional(positional[1], "gatewayId");
|
|
659
|
+
const result = await client.clearGatewayBinding(machineId, gatewayId, {
|
|
660
|
+
...(stringFlag(flags, "chat-id") ? { chatId: stringFlag(flags, "chat-id") } : {}),
|
|
661
|
+
...(stringFlag(flags, "thread-id") ? { threadId: stringFlag(flags, "thread-id") } : {}),
|
|
662
|
+
...(stringFlag(flags, "session") ? { sessionId: stringFlag(flags, "session") } : {}),
|
|
663
|
+
});
|
|
664
|
+
if (!wantsJson(flags))
|
|
665
|
+
console.log(`Cleared ${result.removedBindings} binding(s) and ${result.removedQueuedInbound} queued message(s) on Daemon ${machineId}.`);
|
|
666
|
+
else
|
|
667
|
+
ok("gateway.binding.clear", { machineId, ...result }, { machineId });
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
if (action === "source" && positional[0] === "inspect") {
|
|
671
|
+
const gatewayId = requirePositional(positional[1], "gatewayId");
|
|
672
|
+
const result = await client.inspectGatewaySource(machineId, gatewayId, { verifyCandidate: true });
|
|
673
|
+
if (!wantsJson(flags)) {
|
|
674
|
+
console.log(`Gateway ${gatewayId} source: ${result.status}`);
|
|
675
|
+
console.log(`state digest: ${result.stateDigest}`);
|
|
676
|
+
}
|
|
677
|
+
else
|
|
678
|
+
ok("gateway.source.inspect", { machineId, ...result }, { machineId });
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
if (action === "store" && positional[0] === "inspect") {
|
|
682
|
+
const result = await client.inspectGatewayStore(machineId);
|
|
683
|
+
if (!wantsJson(flags)) {
|
|
684
|
+
console.log(`Gateway store on Daemon ${machineId}: ${result.repairRequired ? "repair required" : "healthy"}`);
|
|
685
|
+
console.log(`artifact digest: ${result.artifactDigest}`);
|
|
686
|
+
}
|
|
687
|
+
else
|
|
688
|
+
ok("gateway.store.inspect", { machineId, ...result }, { machineId });
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
throw new CliError("This Gateway maintenance action is local-only. Remove --machine and run it on the owning Daemon, or use a supported encrypted management action.", "GATEWAY_REMOTE_ACTION_UNSUPPORTED");
|
|
692
|
+
}
|
|
693
|
+
function printRemoteGateway(gateway, verbose = false) {
|
|
694
|
+
console.log(`${gateway.id} ${gateway.enabled ? "enabled" : "disabled"} ${gateway.platform} ${gateway.runtimeState}`);
|
|
695
|
+
if (!verbose)
|
|
696
|
+
return;
|
|
697
|
+
console.log(`secret: ${gateway.hasSecret ? "set" : "missing"}`);
|
|
698
|
+
console.log(`inbound: ${gateway.inboundMode}`);
|
|
699
|
+
console.log(`default: ${gateway.defaultAgent} ${gateway.defaultCwd}`);
|
|
700
|
+
console.log(`permission: ${gateway.permissionMode ?? "approve-all"}`);
|
|
701
|
+
console.log(`identities: ${gateway.identities.length}`);
|
|
702
|
+
console.log(`bindings: ${gateway.counts.bindings} queued: ${gateway.counts.queue}`);
|
|
703
|
+
}
|
|
491
704
|
async function readGatewayStore() {
|
|
492
705
|
return await readGatewayStoreFile(gatewayConfigPath);
|
|
493
706
|
}
|
|
@@ -796,6 +1009,7 @@ function applyGatewayConfigFlags(gateway, flags) {
|
|
|
796
1009
|
defaultAgent: stringFlag(flags, "default-agent") ?? stringFlag(flags, "agent") ?? gateway.defaultAgent,
|
|
797
1010
|
defaultCwd: stringFlag(flags, "default-cwd") ?? stringFlag(flags, "cwd") ?? gateway.defaultCwd,
|
|
798
1011
|
allowAllUsers: allowAllUsersFlag(flags, gateway.allowAllUsers === true),
|
|
1012
|
+
permissionMode: permissionModeFlag(flags, gateway.permissionMode ?? "approve-all"),
|
|
799
1013
|
updatedAt: Date.now(),
|
|
800
1014
|
};
|
|
801
1015
|
const defaultChatId = stringFlag(flags, "default-chat-id");
|
|
@@ -852,6 +1066,7 @@ function publicGateway(gateway) {
|
|
|
852
1066
|
emojiType: gateway.processingReaction.emojiType,
|
|
853
1067
|
},
|
|
854
1068
|
allowAllUsers: gateway.allowAllUsers === true,
|
|
1069
|
+
permissionMode: gateway.permissionMode ?? "approve-all",
|
|
855
1070
|
...(gateway.sourceNamespaceVersion === 1 ? { sourceNamespaceVersion: 1 } : {}),
|
|
856
1071
|
...(gateway.appId ? { appId: gateway.appId } : {}),
|
|
857
1072
|
...(gateway.domain ? { domain: gateway.domain } : {}),
|
|
@@ -936,6 +1151,14 @@ function inboundModeFlag(flags, fallback) {
|
|
|
936
1151
|
}
|
|
937
1152
|
return aliases[0] ?? fallback;
|
|
938
1153
|
}
|
|
1154
|
+
function permissionModeFlag(flags, fallback) {
|
|
1155
|
+
const value = flags["permission-mode"];
|
|
1156
|
+
if (value === undefined || value === false)
|
|
1157
|
+
return fallback;
|
|
1158
|
+
if (value === "approve-all" || value === "approve-reads" || value === "deny-all")
|
|
1159
|
+
return value;
|
|
1160
|
+
throw new CliError("Invalid --permission-mode <approve-all|approve-reads|deny-all>", "INVALID_ARGUMENT");
|
|
1161
|
+
}
|
|
939
1162
|
function processingReactionFromFlags(flags, fallback) {
|
|
940
1163
|
if (flags["reaction-enabled"] === true && flags["reaction-disabled"] === true) {
|
|
941
1164
|
throw new CliError("Use only one of --reaction-enabled or --reaction-disabled", "INVALID_ARGUMENT");
|