@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.
Files changed (169) hide show
  1. package/README.md +1 -1
  2. package/apps/cli/dist/commands/account.js +8 -2
  3. package/apps/cli/dist/commands/config.js +1 -1
  4. package/apps/cli/dist/commands/daemon.js +180 -25
  5. package/apps/cli/dist/commands/gateway.js +230 -7
  6. package/apps/cli/dist/commands/lib/args.js +6 -0
  7. package/apps/cli/dist/commands/lib/bootstrap-config.js +8 -8
  8. package/apps/cli/dist/commands/lib/bootstrap-daemon.d.ts +13 -2
  9. package/apps/cli/dist/commands/lib/bootstrap-daemon.js +99 -4
  10. package/apps/cli/dist/commands/lib/bootstrap-join.d.ts +1 -0
  11. package/apps/cli/dist/commands/lib/bootstrap-join.js +199 -0
  12. package/apps/cli/dist/commands/lib/bootstrap-output.d.ts +2 -5
  13. package/apps/cli/dist/commands/lib/bootstrap-output.js +6 -9
  14. package/apps/cli/dist/commands/lib/bootstrap.js +20 -15
  15. package/apps/cli/dist/commands/lib/config.d.ts +1 -0
  16. package/apps/cli/dist/commands/lib/config.js +3 -0
  17. package/apps/cli/dist/commands/lib/daemon-enrollment-intent.d.ts +47 -0
  18. package/apps/cli/dist/commands/lib/daemon-enrollment-intent.js +257 -0
  19. package/apps/cli/dist/commands/lib/doctor.js +10 -10
  20. package/apps/cli/dist/commands/lib/exit.js +8 -8
  21. package/apps/cli/dist/commands/lib/index.d.ts +4 -2
  22. package/apps/cli/dist/commands/lib/index.js +4 -2
  23. package/apps/cli/dist/commands/lib/local-daemon.d.ts +0 -1
  24. package/apps/cli/dist/commands/lib/local-daemon.js +0 -3
  25. package/apps/cli/dist/commands/lib/orchestrator.d.ts +3 -1
  26. package/apps/cli/dist/commands/lib/orchestrator.js +43 -5
  27. package/apps/cli/dist/commands/lib/scope.d.ts +11 -2
  28. package/apps/cli/dist/commands/lib/scope.js +27 -1
  29. package/apps/cli/dist/commands/lib/status.js +2 -2
  30. package/apps/cli/dist/commands/lib/usage.js +63 -35
  31. package/apps/cli/dist/commands/lib/workspace.js +2 -2
  32. package/apps/cli/dist/commands/loop.js +79 -18
  33. package/apps/cli/dist/commands/machine.js +10 -3
  34. package/apps/cli/dist/commands/memory.js +90 -1
  35. package/apps/cli/dist/commands/orchestrator.d.ts +1 -0
  36. package/apps/cli/dist/commands/orchestrator.js +45 -10
  37. package/apps/cli/dist/commands/remote.js +53 -14
  38. package/apps/cli/dist/commands/session.d.ts +1 -0
  39. package/apps/cli/dist/commands/session.js +60 -22
  40. package/apps/cli/dist/commands/skill.d.ts +8 -0
  41. package/apps/cli/dist/commands/skill.js +54 -9
  42. package/apps/cli/dist/skills/registry.d.ts +1 -1
  43. package/apps/cli/dist/skills/registry.js +12 -1
  44. package/apps/daemon/dist/gateway/commands.js +2 -0
  45. package/apps/daemon/dist/gateway/runtime.js +13 -3
  46. package/apps/daemon/dist/lifecycle/artifacts.d.ts +44 -0
  47. package/apps/daemon/dist/lifecycle/artifacts.js +588 -0
  48. package/apps/daemon/dist/lifecycle/capability.d.ts +25 -0
  49. package/apps/daemon/dist/lifecycle/capability.js +290 -0
  50. package/apps/daemon/dist/lifecycle/detached-helper-bundle.mjs +18505 -0
  51. package/apps/daemon/dist/lifecycle/detached-helper-cli.d.ts +1 -0
  52. package/apps/daemon/dist/lifecycle/detached-helper-cli.js +11 -0
  53. package/apps/daemon/dist/lifecycle/errors.d.ts +3 -0
  54. package/apps/daemon/dist/lifecycle/errors.js +10 -0
  55. package/apps/daemon/dist/lifecycle/helper.d.ts +39 -0
  56. package/apps/daemon/dist/lifecycle/helper.js +403 -0
  57. package/apps/daemon/dist/lifecycle/index.d.ts +7 -0
  58. package/apps/daemon/dist/lifecycle/index.js +6 -0
  59. package/apps/daemon/dist/lifecycle/journal.d.ts +21 -0
  60. package/apps/daemon/dist/lifecycle/journal.js +389 -0
  61. package/apps/daemon/dist/lifecycle/npm.d.ts +37 -0
  62. package/apps/daemon/dist/lifecycle/npm.js +164 -0
  63. package/apps/daemon/dist/lifecycle/operation.d.ts +19 -0
  64. package/apps/daemon/dist/lifecycle/operation.js +175 -0
  65. package/apps/daemon/dist/lifecycle/relay.d.ts +5 -0
  66. package/apps/daemon/dist/lifecycle/relay.js +72 -0
  67. package/apps/daemon/dist/lifecycle/reporter.d.ts +29 -0
  68. package/apps/daemon/dist/lifecycle/reporter.js +402 -0
  69. package/apps/daemon/dist/lifecycle/restart.d.ts +127 -0
  70. package/apps/daemon/dist/lifecycle/restart.js +650 -0
  71. package/apps/daemon/dist/lifecycle/runtime.d.ts +36 -0
  72. package/apps/daemon/dist/lifecycle/runtime.js +210 -0
  73. package/apps/daemon/dist/lifecycle/types.d.ts +110 -0
  74. package/apps/daemon/dist/lifecycle/types.js +3 -0
  75. package/apps/daemon/dist/paths.d.ts +4 -0
  76. package/apps/daemon/dist/paths.js +4 -0
  77. package/apps/daemon/dist/relay/connection.js +37 -0
  78. package/apps/daemon/dist/relay/register.d.ts +3 -0
  79. package/apps/daemon/dist/relay/register.js +49 -1
  80. package/apps/daemon/dist/relay/send.d.ts +3 -1
  81. package/apps/daemon/dist/relay/send.js +5 -2
  82. package/apps/daemon/dist/runtime.js +5 -2
  83. package/apps/daemon/dist/session/lifecycle.d.ts +1 -1
  84. package/apps/daemon/dist/session/lifecycle.js +107 -46
  85. package/apps/daemon/dist/session/primitives.js +37 -3
  86. package/apps/daemon/dist/session/prompt.js +39 -3
  87. package/apps/daemon/dist/start.js +10 -0
  88. package/apps/daemon/dist/turn-coordinator.d.ts +8 -0
  89. package/apps/daemon/dist/turn-coordinator.js +28 -1
  90. package/apps/daemon/package.json +1 -1
  91. package/apps/relay/dist/account-access-revocation.d.ts +39 -0
  92. package/apps/relay/dist/account-access-revocation.js +67 -0
  93. package/apps/relay/dist/account-http-routes.d.ts +3 -0
  94. package/apps/relay/dist/account-http-routes.js +650 -0
  95. package/apps/relay/dist/account-projections.d.ts +40 -0
  96. package/apps/relay/dist/account-projections.js +96 -0
  97. package/apps/relay/dist/controller-handlers.d.ts +3 -1
  98. package/apps/relay/dist/controller-handlers.js +33 -6
  99. package/apps/relay/dist/db.d.ts +6 -0
  100. package/apps/relay/dist/db.js +137 -25
  101. package/apps/relay/dist/enrollment-repository.d.ts +61 -0
  102. package/apps/relay/dist/enrollment-repository.js +113 -0
  103. package/apps/relay/dist/http-routes.d.ts +1 -1
  104. package/apps/relay/dist/http-routes.js +280 -72
  105. package/apps/relay/dist/http-schemas.d.ts +33 -10
  106. package/apps/relay/dist/http-schemas.js +9 -10
  107. package/apps/relay/dist/index.js +28 -8
  108. package/apps/relay/dist/machine-command-result-handlers.d.ts +21 -1
  109. package/apps/relay/dist/machine-command-result-handlers.js +363 -17
  110. package/apps/relay/dist/relay-context.d.ts +5 -3
  111. package/apps/relay/dist/relay-context.js +56 -36
  112. package/apps/relay/dist/retention.d.ts +6 -0
  113. package/apps/relay/dist/retention.js +26 -2
  114. package/apps/relay/dist/scope.js +7 -0
  115. package/apps/relay/dist/session-projection-reducer.js +19 -15
  116. package/apps/relay/dist/types.d.ts +20 -12
  117. package/apps/relay/dist/websocket.js +9 -0
  118. package/build-identity.json +3 -3
  119. package/npm-shrinkwrap.json +2 -2
  120. package/package.json +1 -1
  121. package/packages/client/dist/account-management.d.ts +64 -0
  122. package/packages/client/dist/account-management.js +486 -0
  123. package/packages/client/dist/account.d.ts +7 -2
  124. package/packages/client/dist/account.js +55 -30
  125. package/packages/client/dist/client.d.ts +36 -3
  126. package/packages/client/dist/client.js +379 -11
  127. package/packages/client/dist/http.d.ts +1 -1
  128. package/packages/client/dist/index.d.ts +5 -2
  129. package/packages/client/dist/index.js +2 -0
  130. package/packages/client/dist/live-types.d.ts +16 -0
  131. package/packages/client/dist/live.d.ts +5 -0
  132. package/packages/client/dist/live.js +110 -8
  133. package/packages/client/dist/parsers.d.ts +1 -3
  134. package/packages/client/dist/parsers.js +1 -36
  135. package/packages/client/dist/transport.js +16 -0
  136. package/packages/client/dist/types.d.ts +66 -2
  137. package/packages/runtime-cli/dist/backend-session-lease.d.ts +40 -0
  138. package/packages/runtime-cli/dist/backend-session-lease.js +561 -0
  139. package/packages/runtime-cli/dist/claude-history.d.ts +1 -1
  140. package/packages/runtime-cli/dist/claude-history.js +23 -22
  141. package/packages/runtime-cli/dist/claude-session-store.d.ts +0 -1
  142. package/packages/runtime-cli/dist/claude-session-store.js +1 -7
  143. package/packages/runtime-cli/dist/claude-turn.d.ts +4 -0
  144. package/packages/runtime-cli/dist/claude-turn.js +98 -6
  145. package/packages/runtime-cli/dist/codex-app-server.d.ts +8 -1
  146. package/packages/runtime-cli/dist/codex-app-server.js +10 -1
  147. package/packages/runtime-cli/dist/codex-json-rpc.d.ts +5 -0
  148. package/packages/runtime-cli/dist/codex-json-rpc.js +84 -5
  149. package/packages/runtime-cli/dist/codex-lifecycle.js +16 -3
  150. package/packages/runtime-cli/dist/index.js +98 -51
  151. package/packages/runtime-cli/dist/process-identity.d.ts +15 -0
  152. package/packages/runtime-cli/dist/process-identity.js +102 -0
  153. package/packages/runtime-cli/dist/session-store.d.ts +27 -0
  154. package/packages/runtime-cli/dist/session-store.js +213 -17
  155. package/packages/shared/dist/account.d.ts +184 -0
  156. package/packages/shared/dist/account.js +205 -0
  157. package/packages/shared/dist/crypto.d.ts +27 -1
  158. package/packages/shared/dist/crypto.js +112 -4
  159. package/packages/shared/dist/daemon-lifecycle.d.ts +85 -0
  160. package/packages/shared/dist/daemon-lifecycle.js +66 -0
  161. package/packages/shared/dist/index.d.ts +2 -0
  162. package/packages/shared/dist/index.js +2 -0
  163. package/packages/shared/dist/node/gateway-store.d.ts +3 -1
  164. package/packages/shared/dist/node/gateway-store.js +7 -0
  165. package/packages/shared/dist/protocol-schemas.d.ts +80 -26
  166. package/packages/shared/dist/protocol-schemas.js +4 -0
  167. package/packages/shared/dist/protocol-types.d.ts +35 -0
  168. package/packages/shared/dist/protocol.d.ts +33 -2
  169. 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
- Existing workspace recovery uses a one-time invite from Settings → Browsers → Add:
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
- ok("account.create", created);
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(`Controller token: ${redacted.hasControllerToken ? "configured" : "missing"}`);
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 { assertLocalDaemonVerified, canonicalRelayUrl, cliErrorFromFetch, CliError, compactText, ControllerClient, configDir, configPath, daemonPidPath, deriveOrchestration, diagnoseInstallation, emitDoctorResult, idleSessionStatuses, localDaemonStatus, ok, parsePairingClaimResponse, randomId, readConfig, readDaemonConfig, readLocalAuditLog, readRelayJson, redactedDaemonConfig, requirePositional, requireRelayUrl, requireString, showMachine, spawnDaemonStart, stopLocalDaemon, throwRelayHttpError, wantsJson, withAccountConfigTransaction } from "./lib/index.js";
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(`Machine: ${redacted.machineName} (${redacted.machineId})`);
161
- console.log(`Machine token: ${redacted.hasMachineToken ? "configured" : "missing"}`);
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
- const status = await localDaemonStatus();
204
- assertLocalDaemonVerified(status);
205
- if (status.running) {
206
- const data = { ...status, started: false };
207
- if (wantsJson(flags))
208
- ok("daemon.start", data);
209
- else
210
- writeHumanDaemonStartOutput(data);
211
- return true;
212
- }
213
- await readDaemonConfig();
214
- const startArgs = typeof flags.relay === "string" ? ["--relay", flags.relay] : [];
215
- const pid = spawnDaemonStart(startArgs);
216
- const data = { pid, pidPath: daemonPidPath, started: true };
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 readDaemonConfig();
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
- const startArgs = typeof flags.relay === "string" ? ["--relay", flags.relay] : [];
231
- const pid = spawnDaemonStart(startArgs);
232
- ok("daemon.restart", { stop, startedPid: pid, pidPath: daemonPidPath });
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("Machine not found", "MACHINE_NOT_FOUND");
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(`Relay: ${data.relayUrl} ${data.relayReachable ? "ok" : "unreachable"}`);
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(`Machines: ${data.summary.onlineMachineCount}/${data.summary.machineCount} online`);
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} machine:${session.machineId}`);
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 { assertLocalDaemonVerified, CliError, configDir, daemonPidPath, localDaemonStatus, ok, parseCsvFlag, randomId, requirePositional, requireString, wantsJson, } from "./lib/index.js";
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
- platformUserId,
367
- displayName: stringFlag(flags, "display-name"),
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");