@phnx-labs/agents-cli 1.20.64 → 1.20.66

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 (188) hide show
  1. package/CHANGELOG.md +51 -3
  2. package/README.md +156 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.d.ts +12 -0
  5. package/dist/commands/apply.js +274 -0
  6. package/dist/commands/browser.js +2 -2
  7. package/dist/commands/cloud.js +32 -2
  8. package/dist/commands/doctor.js +4 -1
  9. package/dist/commands/exec.d.ts +48 -0
  10. package/dist/commands/exec.js +159 -49
  11. package/dist/commands/feed.js +25 -11
  12. package/dist/commands/hosts.js +44 -6
  13. package/dist/commands/mcp.js +55 -5
  14. package/dist/commands/monitors.d.ts +12 -0
  15. package/dist/commands/monitors.js +748 -0
  16. package/dist/commands/output.js +2 -2
  17. package/dist/commands/plugins.js +28 -7
  18. package/dist/commands/routines.js +23 -2
  19. package/dist/commands/secrets.d.ts +16 -0
  20. package/dist/commands/secrets.js +215 -64
  21. package/dist/commands/serve.js +31 -0
  22. package/dist/commands/sessions-browser.d.ts +82 -0
  23. package/dist/commands/sessions-browser.js +320 -0
  24. package/dist/commands/sessions-export.js +8 -3
  25. package/dist/commands/sessions.d.ts +17 -0
  26. package/dist/commands/sessions.js +157 -10
  27. package/dist/commands/share.d.ts +2 -0
  28. package/dist/commands/share.js +150 -0
  29. package/dist/commands/ssh.js +54 -3
  30. package/dist/commands/versions.js +7 -3
  31. package/dist/commands/view.d.ts +26 -0
  32. package/dist/commands/view.js +32 -9
  33. package/dist/commands/webhook.js +10 -2
  34. package/dist/index.js +35 -14
  35. package/dist/lib/agents.d.ts +46 -0
  36. package/dist/lib/agents.js +121 -3
  37. package/dist/lib/auto-dispatch-provider.js +7 -2
  38. package/dist/lib/auto-dispatch.d.ts +3 -0
  39. package/dist/lib/auto-dispatch.js +3 -0
  40. package/dist/lib/browser/chrome.js +2 -2
  41. package/dist/lib/cloud/antigravity.js +2 -2
  42. package/dist/lib/cloud/host.d.ts +59 -0
  43. package/dist/lib/cloud/host.js +224 -0
  44. package/dist/lib/cloud/registry.js +4 -0
  45. package/dist/lib/cloud/types.d.ts +6 -4
  46. package/dist/lib/computer-rpc.js +3 -1
  47. package/dist/lib/crabbox/cli.js +5 -1
  48. package/dist/lib/crabbox/runtimes.js +11 -2
  49. package/dist/lib/daemon.d.ts +20 -4
  50. package/dist/lib/daemon.js +62 -19
  51. package/dist/lib/devices/connect.d.ts +18 -1
  52. package/dist/lib/devices/connect.js +10 -2
  53. package/dist/lib/devices/fleet.d.ts +3 -2
  54. package/dist/lib/devices/fleet.js +9 -0
  55. package/dist/lib/devices/known-hosts.d.ts +62 -0
  56. package/dist/lib/devices/known-hosts.js +137 -0
  57. package/dist/lib/devices/registry.d.ts +15 -0
  58. package/dist/lib/devices/registry.js +9 -0
  59. package/dist/lib/exec.d.ts +19 -2
  60. package/dist/lib/exec.js +41 -13
  61. package/dist/lib/fleet/apply.d.ts +63 -0
  62. package/dist/lib/fleet/apply.js +214 -0
  63. package/dist/lib/fleet/auth-sync.d.ts +67 -0
  64. package/dist/lib/fleet/auth-sync.js +142 -0
  65. package/dist/lib/fleet/manifest.d.ts +29 -0
  66. package/dist/lib/fleet/manifest.js +127 -0
  67. package/dist/lib/fleet/types.d.ts +129 -0
  68. package/dist/lib/fleet/types.js +13 -0
  69. package/dist/lib/git.d.ts +27 -0
  70. package/dist/lib/git.js +34 -2
  71. package/dist/lib/hosts/dispatch.d.ts +29 -8
  72. package/dist/lib/hosts/dispatch.js +66 -20
  73. package/dist/lib/hosts/passthrough.js +2 -0
  74. package/dist/lib/hosts/providers/devices.d.ts +27 -0
  75. package/dist/lib/hosts/providers/devices.js +98 -0
  76. package/dist/lib/hosts/registry.d.ts +10 -16
  77. package/dist/lib/hosts/registry.js +17 -50
  78. package/dist/lib/hosts/remote-cmd.d.ts +23 -0
  79. package/dist/lib/hosts/remote-cmd.js +79 -4
  80. package/dist/lib/hosts/run-target.d.ts +84 -0
  81. package/dist/lib/hosts/run-target.js +99 -0
  82. package/dist/lib/hosts/types.d.ts +23 -5
  83. package/dist/lib/hosts/types.js +22 -4
  84. package/dist/lib/linear-autoclose.d.ts +30 -0
  85. package/dist/lib/linear-autoclose.js +22 -0
  86. package/dist/lib/mcp.d.ts +27 -1
  87. package/dist/lib/mcp.js +126 -12
  88. package/dist/lib/monitors/config.d.ts +161 -0
  89. package/dist/lib/monitors/config.js +372 -0
  90. package/dist/lib/monitors/dispatch.d.ts +28 -0
  91. package/dist/lib/monitors/dispatch.js +91 -0
  92. package/dist/lib/monitors/engine.d.ts +61 -0
  93. package/dist/lib/monitors/engine.js +205 -0
  94. package/dist/lib/monitors/sources/command.d.ts +11 -0
  95. package/dist/lib/monitors/sources/command.js +31 -0
  96. package/dist/lib/monitors/sources/device.d.ts +13 -0
  97. package/dist/lib/monitors/sources/device.js +45 -0
  98. package/dist/lib/monitors/sources/file.d.ts +14 -0
  99. package/dist/lib/monitors/sources/file.js +57 -0
  100. package/dist/lib/monitors/sources/http.d.ts +10 -0
  101. package/dist/lib/monitors/sources/http.js +34 -0
  102. package/dist/lib/monitors/sources/index.d.ts +14 -0
  103. package/dist/lib/monitors/sources/index.js +31 -0
  104. package/dist/lib/monitors/sources/poll.d.ts +9 -0
  105. package/dist/lib/monitors/sources/poll.js +9 -0
  106. package/dist/lib/monitors/sources/types.d.ts +18 -0
  107. package/dist/lib/monitors/sources/types.js +9 -0
  108. package/dist/lib/monitors/sources/webhook.d.ts +23 -0
  109. package/dist/lib/monitors/sources/webhook.js +47 -0
  110. package/dist/lib/monitors/sources/ws.d.ts +14 -0
  111. package/dist/lib/monitors/sources/ws.js +45 -0
  112. package/dist/lib/monitors/state.d.ts +69 -0
  113. package/dist/lib/monitors/state.js +144 -0
  114. package/dist/lib/picker.d.ts +53 -0
  115. package/dist/lib/picker.js +214 -1
  116. package/dist/lib/platform/exec.d.ts +16 -0
  117. package/dist/lib/platform/exec.js +17 -0
  118. package/dist/lib/plugins.d.ts +31 -1
  119. package/dist/lib/plugins.js +175 -15
  120. package/dist/lib/redact.d.ts +14 -1
  121. package/dist/lib/redact.js +47 -1
  122. package/dist/lib/remote-agents-json.js +7 -1
  123. package/dist/lib/rotate.d.ts +6 -3
  124. package/dist/lib/rotate.js +0 -1
  125. package/dist/lib/routines.d.ts +16 -0
  126. package/dist/lib/routines.js +19 -0
  127. package/dist/lib/runner.d.ts +1 -0
  128. package/dist/lib/runner.js +102 -9
  129. package/dist/lib/secrets/agent.d.ts +83 -10
  130. package/dist/lib/secrets/agent.js +237 -70
  131. package/dist/lib/secrets/bundles.d.ts +26 -0
  132. package/dist/lib/secrets/bundles.js +59 -8
  133. package/dist/lib/secrets/filestore.d.ts +9 -0
  134. package/dist/lib/secrets/filestore.js +21 -8
  135. package/dist/lib/secrets/index.d.ts +7 -0
  136. package/dist/lib/secrets/index.js +26 -6
  137. package/dist/lib/secrets/mcp.js +4 -2
  138. package/dist/lib/secrets/remote.d.ts +17 -0
  139. package/dist/lib/secrets/remote.js +40 -0
  140. package/dist/lib/self-update.d.ts +20 -0
  141. package/dist/lib/self-update.js +54 -1
  142. package/dist/lib/serve/control.d.ts +95 -0
  143. package/dist/lib/serve/control.js +260 -0
  144. package/dist/lib/serve/server.d.ts +35 -1
  145. package/dist/lib/serve/server.js +106 -76
  146. package/dist/lib/serve/stream.d.ts +43 -0
  147. package/dist/lib/serve/stream.js +116 -0
  148. package/dist/lib/serve/token.d.ts +35 -0
  149. package/dist/lib/serve/token.js +85 -0
  150. package/dist/lib/session/bundle.d.ts +14 -0
  151. package/dist/lib/session/bundle.js +12 -1
  152. package/dist/lib/session/remote-list.js +5 -1
  153. package/dist/lib/session/state.d.ts +7 -25
  154. package/dist/lib/session/state.js +16 -6
  155. package/dist/lib/session/sync/config.js +8 -2
  156. package/dist/lib/session/types.d.ts +30 -0
  157. package/dist/lib/share/capture.d.ts +29 -0
  158. package/dist/lib/share/capture.js +140 -0
  159. package/dist/lib/share/config.d.ts +35 -0
  160. package/dist/lib/share/config.js +100 -0
  161. package/dist/lib/share/og.d.ts +25 -0
  162. package/dist/lib/share/og.js +84 -0
  163. package/dist/lib/share/provision.d.ts +10 -0
  164. package/dist/lib/share/provision.js +91 -0
  165. package/dist/lib/share/publish.d.ts +57 -0
  166. package/dist/lib/share/publish.js +145 -0
  167. package/dist/lib/share/worker-template.d.ts +2 -0
  168. package/dist/lib/share/worker-template.js +82 -0
  169. package/dist/lib/shims.d.ts +13 -0
  170. package/dist/lib/shims.js +42 -2
  171. package/dist/lib/ssh-exec.d.ts +24 -0
  172. package/dist/lib/ssh-exec.js +15 -3
  173. package/dist/lib/ssh-tunnel.d.ts +19 -1
  174. package/dist/lib/ssh-tunnel.js +86 -7
  175. package/dist/lib/startup/command-registry.d.ts +3 -0
  176. package/dist/lib/startup/command-registry.js +6 -0
  177. package/dist/lib/state.d.ts +5 -0
  178. package/dist/lib/state.js +12 -0
  179. package/dist/lib/tmux/session.d.ts +7 -0
  180. package/dist/lib/tmux/session.js +3 -1
  181. package/dist/lib/triggers/webhook.d.ts +18 -0
  182. package/dist/lib/triggers/webhook.js +105 -0
  183. package/dist/lib/types.d.ts +36 -1
  184. package/dist/lib/usage.js +7 -5
  185. package/dist/lib/versions.js +14 -11
  186. package/dist/lib/workflows.d.ts +20 -0
  187. package/dist/lib/workflows.js +24 -0
  188. package/package.json +2 -1
package/dist/lib/shims.js CHANGED
@@ -16,7 +16,7 @@ import { confirm, select } from '@inquirer/prompts';
16
16
  import { IS_WINDOWS, prependToWindowsUserPath } from './platform/index.js';
17
17
  import { getShimsDir, getVersionsDir, getBackupsDir, getHistoryDir, ensureAgentsDir } from './state.js';
18
18
  export { getShimsDir };
19
- import { AGENTS, agentConfigDirName } from './agents.js';
19
+ import { AGENTS, agentConfigDirName, readAuthAccountIdentity } from './agents.js';
20
20
  /**
21
21
  * Files and directories to always skip during conflict detection and migration.
22
22
  * These are never user config that should be migrated.
@@ -1143,6 +1143,21 @@ function detectMigrationConflicts(agent, version) {
1143
1143
  * preserved so the "freshest" comparison stays stable and switches don't
1144
1144
  * ping-pong. Best-effort: a failed copy just means the user re-logs in.
1145
1145
  */
1146
+ /**
1147
+ * Best-effort account identity for the credential *directory* of a file-auth
1148
+ * agent (droid / kimi / antigravity), or null when the directory holds no
1149
+ * decodable account claim. Delegates to readAuthAccountIdentity, which decrypts
1150
+ * / decodes each agent's REAL on-disk format (droid AES-256-GCM + WorkOS JWT,
1151
+ * kimi access-token JWT, antigravity refresh-token) — the earlier plaintext
1152
+ * top-level-key scan matched NO real credential file, so the guard below never
1153
+ * engaged. Two dirs for the SAME account compare equal; DIFFERENT accounts
1154
+ * compare distinct. Used by carryForwardAuthFiles to refuse overwriting one
1155
+ * account's login with a credential that belongs to a DIFFERENT account
1156
+ * (RUSH-1764).
1157
+ */
1158
+ export function readAuthFileIdentity(agent, configDir) {
1159
+ return readAuthAccountIdentity(agent, configDir);
1160
+ }
1146
1161
  export function carryForwardAuthFiles(agent, toConfigDir) {
1147
1162
  const authFiles = AGENTS[agent].authFiles;
1148
1163
  if (!authFiles || authFiles.length === 0)
@@ -1158,12 +1173,33 @@ export function carryForwardAuthFiles(agent, toConfigDir) {
1158
1173
  catch {
1159
1174
  return; // no installed versions to source from
1160
1175
  }
1176
+ // Account identity currently installed at the destination home (null when the
1177
+ // dest is empty or its credential can't be decoded). When it IS known, only a
1178
+ // source home whose credential decodes to the SAME account is eligible to
1179
+ // overwrite it — so a newer login for a DIFFERENT account sitting in another
1180
+ // version-home can't silently replace the account the user is signed into
1181
+ // (RUSH-1764). An empty destination still seeds from the freshest source (the
1182
+ // version-switch case). Identity is a per-DIR/account property, not per-file:
1183
+ // for droid it comes from decrypting auth.v2.file with auth.v2.key, so it must
1184
+ // gate BOTH files as a unit (never carry account B's key over account A's).
1185
+ const toResolved = path.resolve(toConfigDir);
1186
+ const destIdentity = readAuthFileIdentity(agent, toConfigDir);
1187
+ const identityCache = new Map();
1188
+ const dirIdentity = (dir) => {
1189
+ const key = path.resolve(dir);
1190
+ if (!identityCache.has(key))
1191
+ identityCache.set(key, readAuthFileIdentity(agent, dir));
1192
+ return identityCache.get(key) ?? null;
1193
+ };
1161
1194
  for (const rel of authFiles) {
1162
1195
  const dest = path.join(toConfigDir, rel);
1163
1196
  const destResolved = path.resolve(dest);
1164
- // Newest existing source copy across all version homes (excluding dest).
1197
+ // Newest existing source copy across all version homes (excluding dest),
1198
+ // constrained to the destination's account identity when it is known.
1165
1199
  let newest = null;
1166
1200
  for (const dir of sourceDirs) {
1201
+ if (path.resolve(dir) === toResolved)
1202
+ continue; // never source from self
1167
1203
  const src = path.join(dir, rel);
1168
1204
  if (path.resolve(src) === destResolved)
1169
1205
  continue;
@@ -1176,6 +1212,10 @@ export function carryForwardAuthFiles(agent, toConfigDir) {
1176
1212
  }
1177
1213
  if (!st.isFile())
1178
1214
  continue;
1215
+ // Account-identity guard: never carry a different account's credential over
1216
+ // an existing login. Only enforced when the destination's identity is known.
1217
+ if (destIdentity !== null && dirIdentity(dir) !== destIdentity)
1218
+ continue;
1179
1219
  if (!newest || st.mtimeMs > newest.mtimeMs)
1180
1220
  newest = { path: src, mtimeMs: st.mtimeMs };
1181
1221
  }
@@ -29,6 +29,16 @@ export declare function shellQuote(s: string): string;
29
29
  */
30
30
  export declare const SSH_OPTS: readonly string[];
31
31
  export declare function controlOpts(): string[];
32
+ /**
33
+ * Compose an ssh connection-option prefix.
34
+ *
35
+ * `hostKeyOpts` (a caller's host-key posture, e.g. a pinned
36
+ * `StrictHostKeyChecking=yes` + managed `UserKnownHostsFile`) go FIRST, ahead of
37
+ * the `SSH_OPTS` baseline: ssh honors the *first* value it sees for each option,
38
+ * so an override placed after the baseline's `accept-new` would be silently
39
+ * ignored. Pure so the ordering contract is unit-testable. See RUSH-1767.
40
+ */
41
+ export declare function sshConnectOpts(mux: string[], hostKeyOpts?: string[]): string[];
32
42
  export interface SshExecOptions {
33
43
  /** Piped to the remote command's stdin (never interpolated into the shell). */
34
44
  input?: string;
@@ -38,6 +48,13 @@ export interface SshExecOptions {
38
48
  extraSshArgs?: string[];
39
49
  /** Reuse a persistent control socket across calls (default true; see `controlOpts`). */
40
50
  multiplex?: boolean;
51
+ /**
52
+ * Host-key `-o` options that OVERRIDE the accept-new baseline — prepended so
53
+ * ssh's first-value-wins rule takes them (see {@link sshConnectOpts}). Used to
54
+ * force strict verification against the managed known_hosts store on the
55
+ * credential-copy path (RUSH-1767).
56
+ */
57
+ hostKeyOpts?: string[];
41
58
  }
42
59
  export interface SshExecResult {
43
60
  /** Remote exit status, or null if ssh itself failed / timed out. */
@@ -79,6 +96,13 @@ export interface SshStreamOptions {
79
96
  tty?: boolean;
80
97
  /** Reuse a persistent control socket across calls (default true; see `controlOpts`). */
81
98
  multiplex?: boolean;
99
+ /**
100
+ * Host-key `-o` options that OVERRIDE the accept-new baseline — prepended so
101
+ * ssh's first-value-wins rule takes them (see {@link sshConnectOpts}). Used to
102
+ * force strict verification against the managed known_hosts store on the
103
+ * interactive credential-copy path (RUSH-1767).
104
+ */
105
+ hostKeyOpts?: string[];
82
106
  }
83
107
  /**
84
108
  * Foreground counterpart to `sshExec`: run `remoteCmd` on `target` with the
@@ -88,6 +88,18 @@ export function controlOpts() {
88
88
  '-o', 'ControlPersist=60s',
89
89
  ];
90
90
  }
91
+ /**
92
+ * Compose an ssh connection-option prefix.
93
+ *
94
+ * `hostKeyOpts` (a caller's host-key posture, e.g. a pinned
95
+ * `StrictHostKeyChecking=yes` + managed `UserKnownHostsFile`) go FIRST, ahead of
96
+ * the `SSH_OPTS` baseline: ssh honors the *first* value it sees for each option,
97
+ * so an override placed after the baseline's `accept-new` would be silently
98
+ * ignored. Pure so the ordering contract is unit-testable. See RUSH-1767.
99
+ */
100
+ export function sshConnectOpts(mux, hostKeyOpts) {
101
+ return [...(hostKeyOpts ?? []), ...SSH_OPTS, ...mux];
102
+ }
91
103
  /**
92
104
  * Run `remoteCmd` on `target` over ssh and capture stdout/stderr/exit.
93
105
  *
@@ -97,7 +109,7 @@ export function controlOpts() {
97
109
  export function sshExec(target, remoteCmd, opts = {}) {
98
110
  assertValidSshTarget(target);
99
111
  const mux = opts.multiplex === false ? [] : controlOpts();
100
- const args = [...SSH_OPTS, ...mux, ...(opts.extraSshArgs ?? []), target, remoteCmd];
112
+ const args = [...sshConnectOpts(mux, opts.hostKeyOpts), ...(opts.extraSshArgs ?? []), target, remoteCmd];
101
113
  const res = spawnSync('ssh', args, {
102
114
  input: opts.input,
103
115
  encoding: 'utf-8',
@@ -123,7 +135,7 @@ export function sshExec(target, remoteCmd, opts = {}) {
123
135
  export function sshExecRaw(target, remoteCmd, opts = {}) {
124
136
  assertValidSshTarget(target);
125
137
  const mux = opts.multiplex === false ? [] : controlOpts();
126
- const args = [...SSH_OPTS, ...mux, ...(opts.extraSshArgs ?? []), target, remoteCmd];
138
+ const args = [...sshConnectOpts(mux, opts.hostKeyOpts), ...(opts.extraSshArgs ?? []), target, remoteCmd];
127
139
  const res = spawnSync('ssh', args, {
128
140
  input: opts.input,
129
141
  // No `encoding` → spawnSync returns Buffers.
@@ -154,7 +166,7 @@ export function sshStream(target, remoteCmd, opts = {}) {
154
166
  assertValidSshTarget(target);
155
167
  const mux = opts.multiplex === false ? [] : controlOpts();
156
168
  const tty = opts.tty ? ['-tt'] : [];
157
- const args = [...SSH_OPTS, ...mux, ...tty, target, remoteCmd];
169
+ const args = [...sshConnectOpts(mux, opts.hostKeyOpts), ...tty, target, remoteCmd];
158
170
  const res = spawnSync('ssh', args, { stdio: 'inherit' });
159
171
  if (typeof res.status === 'number')
160
172
  return res.status;
@@ -49,6 +49,8 @@ export declare const REMOTE_HELPER_PORT = 8765;
49
49
  export declare const REMOTE_TASK_NAME = "AgentsComputerHelper";
50
50
  /** Basename of the cross-published exe under native/computer-win/dist. */
51
51
  export declare const WIN_HELPER_EXE = "computer-helper-win.exe";
52
+ /** Basename of the daemon's shared-secret token file under %LOCALAPPDATA%\agents. */
53
+ export declare const WIN_HELPER_TOKEN_FILE = "helper-token";
52
54
  /**
53
55
  * Locate a locally built Windows daemon exe — a repo-checkout build output from
54
56
  * `scripts/build-win.sh`, or one bundled next to the package. Local paths are
@@ -101,6 +103,16 @@ export declare function remoteStatePath(device: string): string;
101
103
  export declare function readRemoteState(device: string): RemoteTunnelState | null;
102
104
  export declare function writeRemoteState(state: RemoteTunnelState): void;
103
105
  export declare function clearRemoteState(device: string): void;
106
+ /**
107
+ * Local file holding the shared-secret token for a device's helper daemon.
108
+ * Written at `setup --host` (0600), read at `start --host` so the RPC client
109
+ * authenticates. The remote daemon reads the same value from its own
110
+ * `--token-file`; the CLI is the source of truth and never round-trips it back.
111
+ */
112
+ export declare function helperTokenPath(device: string): string;
113
+ export declare function readHelperToken(device: string): string | null;
114
+ export declare function writeHelperToken(device: string, token: string): void;
115
+ export declare function clearHelperToken(device: string): void;
104
116
  /** Resolve a registered device to its ssh pieces, or throw a clear error. */
105
117
  export declare function resolveRemoteDevice(name: string): Promise<{
106
118
  device: DeviceProfile;
@@ -123,7 +135,13 @@ export declare function buildVerifyPushScript(remotePath: string, expectedBytes:
123
135
  * survives ssh disconnect — the same rationale as the browser WMI launch. The
124
136
  * task is started immediately so the caller need not log out/in.
125
137
  */
126
- export declare function buildRegisterTaskScript(port: number, taskName: string): string;
138
+ /**
139
+ * PowerShell that writes the shared-secret token under %LOCALAPPDATA%\agents
140
+ * with an owner-only ACL (inheritance removed, read granted only to the current
141
+ * user) and echoes the resolved path so the caller can pass it to `--token-file`.
142
+ */
143
+ export declare function buildWriteTokenScript(token: string): string;
144
+ export declare function buildRegisterTaskScript(port: number, taskName: string, tokenPath: string): string;
127
145
  /** PowerShell that unregisters the task and stops any running daemon process. */
128
146
  export declare function buildUnregisterTaskScript(taskName: string): string;
129
147
  /** Convert a Windows path returned by PowerShell into the scp/SFTP path form. */
@@ -103,6 +103,8 @@ export const REMOTE_HELPER_PORT = 8765;
103
103
  export const REMOTE_TASK_NAME = 'AgentsComputerHelper';
104
104
  /** Basename of the cross-published exe under native/computer-win/dist. */
105
105
  export const WIN_HELPER_EXE = 'computer-helper-win.exe';
106
+ /** Basename of the daemon's shared-secret token file under %LOCALAPPDATA%\agents. */
107
+ export const WIN_HELPER_TOKEN_FILE = 'helper-token';
106
108
  /**
107
109
  * Locate a locally built Windows daemon exe — a repo-checkout build output from
108
110
  * `scripts/build-win.sh`, or one bundled next to the package. Local paths are
@@ -234,6 +236,37 @@ export function clearRemoteState(device) {
234
236
  /* already gone */
235
237
  }
236
238
  }
239
+ /**
240
+ * Local file holding the shared-secret token for a device's helper daemon.
241
+ * Written at `setup --host` (0600), read at `start --host` so the RPC client
242
+ * authenticates. The remote daemon reads the same value from its own
243
+ * `--token-file`; the CLI is the source of truth and never round-trips it back.
244
+ */
245
+ export function helperTokenPath(device) {
246
+ return path.join(remoteStateDir(), `${device}.token`);
247
+ }
248
+ export function readHelperToken(device) {
249
+ try {
250
+ const t = fs.readFileSync(helperTokenPath(device), 'utf-8').trim();
251
+ return t.length > 0 ? t : null;
252
+ }
253
+ catch {
254
+ return null;
255
+ }
256
+ }
257
+ export function writeHelperToken(device, token) {
258
+ const dir = remoteStateDir();
259
+ fs.mkdirSync(dir, { recursive: true });
260
+ fs.writeFileSync(helperTokenPath(device), token, { mode: 0o600 });
261
+ }
262
+ export function clearHelperToken(device) {
263
+ try {
264
+ fs.unlinkSync(helperTokenPath(device));
265
+ }
266
+ catch {
267
+ /* already gone */
268
+ }
269
+ }
237
270
  /** Resolve a registered device to its ssh pieces, or throw a clear error. */
238
271
  export async function resolveRemoteDevice(name) {
239
272
  const device = await getDevice(name);
@@ -284,10 +317,26 @@ export function buildVerifyPushScript(remotePath, expectedBytes) {
284
317
  * survives ssh disconnect — the same rationale as the browser WMI launch. The
285
318
  * task is started immediately so the caller need not log out/in.
286
319
  */
287
- export function buildRegisterTaskScript(port, taskName) {
320
+ /**
321
+ * PowerShell that writes the shared-secret token under %LOCALAPPDATA%\agents
322
+ * with an owner-only ACL (inheritance removed, read granted only to the current
323
+ * user) and echoes the resolved path so the caller can pass it to `--token-file`.
324
+ */
325
+ export function buildWriteTokenScript(token) {
326
+ return [
327
+ `$dir = Join-Path $env:LOCALAPPDATA 'agents'`,
328
+ `New-Item -ItemType Directory -Force -Path $dir | Out-Null`,
329
+ `$tok = Join-Path $dir '${WIN_HELPER_TOKEN_FILE}'`,
330
+ `Set-Content -LiteralPath $tok -Value ${psSingleQuote(token)} -NoNewline -Encoding ascii`,
331
+ `icacls $tok /inheritance:r /grant:r ("$($env:USERNAME):(R)") | Out-Null`,
332
+ `Write-Output $tok`,
333
+ ].join('; ');
334
+ }
335
+ export function buildRegisterTaskScript(port, taskName, tokenPath) {
336
+ const argument = `--port ${port} --token-file "${tokenPath}"`;
288
337
  return [
289
338
  `$exe = Join-Path (Join-Path $env:LOCALAPPDATA 'agents') '${WIN_HELPER_EXE}'`,
290
- `$action = New-ScheduledTaskAction -Execute $exe -Argument '--port ${port}'`,
339
+ `$action = New-ScheduledTaskAction -Execute $exe -Argument ${psSingleQuote(argument)}`,
291
340
  `$trigger = New-ScheduledTaskTrigger -AtLogOn`,
292
341
  `$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Highest`,
293
342
  `$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero)`,
@@ -369,13 +418,28 @@ export async function setupRemoteHelper(name) {
369
418
  if (verify.code !== 0) {
370
419
  throw new Error(`verifying helper exe on '${name}' failed (exit ${verify.code ?? 'null'}): ${verify.stderr.trim() || verify.stdout.trim()}`);
371
420
  }
372
- // Register + start the LOGON task.
373
- const reg = sshExec(target, encodePowerShell(buildRegisterTaskScript(REMOTE_HELPER_PORT, REMOTE_TASK_NAME)), {
421
+ // Provision the auth token: generate it locally, write it on the remote with
422
+ // an owner-only ACL, and register the task with --token-file. The daemon now
423
+ // refuses to start without a token, so a token-less (open-to-any-local-process)
424
+ // daemon can no longer be stood up through the CLI.
425
+ const token = generateToken();
426
+ const tokWrite = sshExec(target, encodePowerShell(buildWriteTokenScript(token)), { timeoutMs: 60_000 });
427
+ if (tokWrite.code !== 0) {
428
+ throw new Error(`writing helper token on '${name}' failed (exit ${tokWrite.code ?? 'null'}): ${tokWrite.stderr.trim() || tokWrite.stdout.trim()}`);
429
+ }
430
+ const remoteTokenPath = tokWrite.stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
431
+ if (!remoteTokenPath) {
432
+ throw new Error(`writing helper token on '${name}' did not return a destination path`);
433
+ }
434
+ // Register + start the LOGON task, pointing it at the token file.
435
+ const reg = sshExec(target, encodePowerShell(buildRegisterTaskScript(REMOTE_HELPER_PORT, REMOTE_TASK_NAME, remoteTokenPath)), {
374
436
  timeoutMs: 60_000,
375
437
  });
376
438
  if (reg.code !== 0) {
377
439
  throw new Error(`registering scheduled task on '${name}' failed (exit ${reg.code ?? 'null'}): ${reg.stderr.trim() || reg.stdout.trim()}`);
378
440
  }
441
+ // Persist the token locally so `start --host` authenticates to the daemon.
442
+ writeHelperToken(name, token);
379
443
  return { target, taskName: REMOTE_TASK_NAME };
380
444
  }
381
445
  /** Reserve a free local TCP port by binding :0 and reading the assigned port. */
@@ -403,9 +467,17 @@ export async function startRemoteTunnel(name) {
403
467
  const tunnelPid = tunnel.pid ?? 0;
404
468
  // Verify the daemon answers through the tunnel before we record it. This is
405
469
  // the real end-to-end check: tunnel up + daemon listening + RPC round-trips.
406
- const token = null; // tunnel-gated; the daemon runs token-less
470
+ // The daemon now requires a token, so the probe must authenticate with the one
471
+ // provisioned at `setup`. A missing token means setup predates auth — guide
472
+ // the user to re-run it rather than silently failing the RPC.
473
+ const token = readHelperToken(name);
407
474
  const prevTcp = process.env.COMPUTER_HELPER_TCP;
475
+ const prevTok = process.env.COMPUTER_HELPER_TOKEN;
408
476
  process.env.COMPUTER_HELPER_TCP = `127.0.0.1:${localPort}`;
477
+ if (token)
478
+ process.env.COMPUTER_HELPER_TOKEN = token;
479
+ else
480
+ delete process.env.COMPUTER_HELPER_TOKEN;
409
481
  const client = openComputerClient();
410
482
  let ok = false;
411
483
  let probeErr = '';
@@ -424,6 +496,10 @@ export async function startRemoteTunnel(name) {
424
496
  delete process.env.COMPUTER_HELPER_TCP;
425
497
  else
426
498
  process.env.COMPUTER_HELPER_TCP = prevTcp;
499
+ if (prevTok === undefined)
500
+ delete process.env.COMPUTER_HELPER_TOKEN;
501
+ else
502
+ process.env.COMPUTER_HELPER_TOKEN = prevTok;
427
503
  }
428
504
  if (!ok) {
429
505
  try {
@@ -431,8 +507,10 @@ export async function startRemoteTunnel(name) {
431
507
  process.kill(tunnelPid);
432
508
  }
433
509
  catch { /* gone */ }
434
- throw new Error(`tunnel to '${name}' opened but the daemon did not answer (${probeErr}). ` +
435
- `Is it installed? Run: agents computer setup --host ${name}`);
510
+ const hint = token
511
+ ? `Is it installed? Run: agents computer setup --host ${name}`
512
+ : `No auth token on record for '${name}' — re-run: agents computer setup --host ${name}`;
513
+ throw new Error(`tunnel to '${name}' opened but the daemon did not answer (${probeErr}). ${hint}`);
436
514
  }
437
515
  const state = {
438
516
  device: name,
@@ -473,6 +551,7 @@ export async function stopRemoteHelper(name) {
473
551
  /* device gone / offline — local teardown still succeeds */
474
552
  }
475
553
  clearRemoteState(name);
554
+ clearHelperToken(name);
476
555
  return { tunnelKilled, taskRemoved };
477
556
  }
478
557
  /**
@@ -43,6 +43,7 @@ export declare const loadImport: ModuleLoader;
43
43
  export declare const loadPackages: ModuleLoader;
44
44
  export declare const loadDaemon: ModuleLoader;
45
45
  export declare const loadRoutines: ModuleLoader;
46
+ export declare const loadMonitors: ModuleLoader;
46
47
  export declare const loadRun: ModuleLoader;
47
48
  export declare const loadDefaults: ModuleLoader;
48
49
  export declare const loadModels: ModuleLoader;
@@ -50,6 +51,7 @@ export declare const loadPrune: ModuleLoader;
50
51
  export declare const loadTrash: ModuleLoader;
51
52
  export declare const loadRestore: ModuleLoader;
52
53
  export declare const loadDoctor: ModuleLoader;
54
+ export declare const loadApply: ModuleLoader;
53
55
  export declare const loadCheck: ModuleLoader;
54
56
  export declare const loadStatus: ModuleLoader;
55
57
  export declare const loadProfiles: ModuleLoader;
@@ -89,6 +91,7 @@ export declare const loadMessage: ModuleLoader;
89
91
  export declare const loadFeed: ModuleLoader;
90
92
  export declare const loadMailboxes: ModuleLoader;
91
93
  export declare const loadServe: ModuleLoader;
94
+ export declare const loadShare: ModuleLoader;
92
95
  export declare const loadAudit: ModuleLoader;
93
96
  export declare const loadWebhook: ModuleLoader;
94
97
  export declare const loadFunnel: ModuleLoader;
@@ -21,6 +21,7 @@ export const loadImport = async () => (await import('../../commands/import.js'))
21
21
  export const loadPackages = async () => (await import('../../commands/packages.js')).registerPackagesCommands;
22
22
  export const loadDaemon = async () => (await import('../../commands/daemon.js')).registerDaemonCommands;
23
23
  export const loadRoutines = async () => (await import('../../commands/routines.js')).registerRoutinesCommands;
24
+ export const loadMonitors = async () => (await import('../../commands/monitors.js')).registerMonitorsCommands;
24
25
  export const loadRun = async () => (await import('../../commands/exec.js')).registerRunCommand;
25
26
  export const loadDefaults = async () => (await import('../../commands/defaults.js')).registerDefaultsCommands;
26
27
  export const loadModels = async () => (await import('../../commands/models.js')).registerModelsCommand;
@@ -28,6 +29,7 @@ export const loadPrune = async () => (await import('../../commands/prune.js')).r
28
29
  export const loadTrash = async () => (await import('../../commands/trash.js')).registerTrashCommands;
29
30
  export const loadRestore = async () => (await import('../../commands/trash.js')).registerRestoreCommand;
30
31
  export const loadDoctor = async () => (await import('../../commands/doctor.js')).registerDoctorCommand;
32
+ export const loadApply = async () => (await import('../../commands/apply.js')).registerApplyCommand;
31
33
  export const loadCheck = async () => (await import('../../commands/check.js')).registerCheckCommand;
32
34
  export const loadStatus = async () => (await import('../../commands/status.js')).registerStatusCommand;
33
35
  export const loadProfiles = async () => (await import('../../commands/profiles.js')).registerProfilesCommands;
@@ -67,6 +69,7 @@ export const loadMessage = async () => (await import('../../commands/message.js'
67
69
  export const loadFeed = async () => (await import('../../commands/feed.js')).registerFeedCommand;
68
70
  export const loadMailboxes = async () => (await import('../../commands/mailboxes.js')).registerMailboxesCommand;
69
71
  export const loadServe = async () => (await import('../../commands/serve.js')).registerServeCommand;
72
+ export const loadShare = async () => (await import('../../commands/share.js')).registerShareCommands;
70
73
  export const loadAudit = async () => (await import('../../commands/audit.js')).registerAuditCommands;
71
74
  export const loadWebhook = async () => (await import('../../commands/webhook.js')).registerWebhookCommand;
72
75
  export const loadFunnel = async () => (await import('../../commands/funnel.js')).registerFunnelCommand;
@@ -121,12 +124,14 @@ export const COMMAND_LOADERS = {
121
124
  install: [loadPackages],
122
125
  daemon: [loadDaemon],
123
126
  routines: [loadRoutines],
127
+ monitors: [loadMonitors],
124
128
  run: [loadRun],
125
129
  defaults: [loadDefaults],
126
130
  models: [loadModels],
127
131
  trash: [loadTrash],
128
132
  restore: [loadRestore],
129
133
  doctor: [loadDoctor],
134
+ apply: [loadApply],
130
135
  check: [loadCheck],
131
136
  status: [loadStatus],
132
137
  profiles: [loadProfiles],
@@ -175,6 +180,7 @@ export const COMMAND_LOADERS = {
175
180
  mailboxes: [loadMailboxes],
176
181
  mailbox: [loadMailboxes],
177
182
  serve: [loadServe],
183
+ share: [loadShare],
178
184
  audit: [loadAudit],
179
185
  webhook: [loadWebhook],
180
186
  funnel: [loadFunnel],
@@ -128,6 +128,11 @@ export declare function getSystemRoutinesDir(): string;
128
128
  export declare function getProjectRoutinesDir(cwd?: string): string | null;
129
129
  /** Path to routine execution logs (~/.agents/.history/runs/). */
130
130
  export declare function getRunsDir(): string;
131
+ /** Path to monitor YAML definitions (~/.agents/monitors/). */
132
+ export declare function getMonitorsDir(): string;
133
+ /** Path to the durable per-monitor state-diff store + fire history
134
+ * (~/.agents/.history/monitors/). */
135
+ export declare function getMonitorsHistoryDir(): string;
131
136
  /** Root for per-agent mailboxes (~/.agents/.history/mailbox/). */
132
137
  export declare function getMailboxRootDir(): string;
133
138
  /** Root for open-block feed records (~/.agents/.history/feed/). */
package/dist/lib/state.js CHANGED
@@ -83,12 +83,19 @@ const HISTORY_DIR = path.join(USER_AGENTS_DIR, '.history');
83
83
  const CACHE_DIR = path.join(USER_AGENTS_DIR, '.cache');
84
84
  // Top-level user dirs (config/definitions only — runtime moves into .history/.cache).
85
85
  const ROUTINES_DIR = path.join(USER_AGENTS_DIR, 'routines');
86
+ // Monitor definitions (event-triggered watchers). Sibling of ROUTINES_DIR: a
87
+ // monitor is a routine whose trigger is a watched source instead of a clock.
88
+ const MONITORS_DIR = path.join(USER_AGENTS_DIR, 'monitors');
86
89
  const TEAMS_DIR = path.join(USER_AGENTS_DIR, 'teams');
87
90
  // History bucket (durable).
88
91
  const SESSIONS_DIR = path.join(HISTORY_DIR, 'sessions');
89
92
  const SESSIONS_DB_PATH = path.join(SESSIONS_DIR, 'sessions.db');
90
93
  const VERSIONS_DIR = path.join(HISTORY_DIR, 'versions');
91
94
  const RUNS_DIR = path.join(HISTORY_DIR, 'runs');
95
+ // Durable per-monitor state-diff store + fire history (last-seen value/hash,
96
+ // fires/<id>/). Sibling of RUNS_DIR — the native diff store that replaces the
97
+ // hand-rolled markdown memory files monitors used to need.
98
+ const MONITORS_HISTORY_DIR = path.join(HISTORY_DIR, 'monitors');
92
99
  const TEAMS_AGENTS_DIR = path.join(HISTORY_DIR, 'teams', 'agents');
93
100
  const BACKUPS_DIR = path.join(HISTORY_DIR, 'backups');
94
101
  const TRASH_DIR = path.join(HISTORY_DIR, 'trash');
@@ -324,6 +331,11 @@ export function getProjectRoutinesDir(cwd = process.cwd()) {
324
331
  }
325
332
  /** Path to routine execution logs (~/.agents/.history/runs/). */
326
333
  export function getRunsDir() { return RUNS_DIR; }
334
+ /** Path to monitor YAML definitions (~/.agents/monitors/). */
335
+ export function getMonitorsDir() { return MONITORS_DIR; }
336
+ /** Path to the durable per-monitor state-diff store + fire history
337
+ * (~/.agents/.history/monitors/). */
338
+ export function getMonitorsHistoryDir() { return MONITORS_HISTORY_DIR; }
327
339
  /** Root for per-agent mailboxes (~/.agents/.history/mailbox/). */
328
340
  export function getMailboxRootDir() { return MAILBOX_DIR; }
329
341
  /** Root for open-block feed records (~/.agents/.history/feed/). */
@@ -33,6 +33,13 @@ export interface SessionMeta {
33
33
  export interface CreateSessionOptions {
34
34
  name: string;
35
35
  cmd?: string;
36
+ /**
37
+ * Redacted copy of `cmd` to persist in SessionMeta.cmd instead of the real
38
+ * command. When set, `cmd` is still executed but only `metaCmd` is written to
39
+ * disk — used to keep resolved secret values out of the informational cmd
40
+ * field (see exec.ts buildTmuxAgentCommand, RUSH-1758). Defaults to `cmd`.
41
+ */
42
+ metaCmd?: string;
36
43
  cwd?: string;
37
44
  env?: NodeJS.ProcessEnv;
38
45
  socket?: string;
@@ -106,7 +106,9 @@ export async function createSession(opts) {
106
106
  name: opts.name,
107
107
  socket,
108
108
  createdAt: Date.now(),
109
- cmd: opts.cmd,
109
+ // Persist the redacted copy when provided so resolved secret values in the
110
+ // launch command never land on disk (RUSH-1758).
111
+ cmd: opts.metaCmd ?? opts.cmd,
110
112
  cwd: opts.cwd,
111
113
  source: opts.source ?? 'cli',
112
114
  labels: opts.labels,
@@ -99,6 +99,18 @@ export interface DeliveryStore {
99
99
  markJob(id: string, jobName: string): void;
100
100
  }
101
101
  export declare function createMemoryDeliveryStore(maxEntries?: number): DeliveryStore;
102
+ /**
103
+ * A durable, disk-backed delivery store. Unlike `createMemoryDeliveryStore`,
104
+ * seen delivery ids survive a process restart and are bounded by AGE, not by a
105
+ * fixed entry count — so a captured valid delivery cannot re-fire after a
106
+ * restart or after count-based LRU eviction would have dropped it.
107
+ *
108
+ * `retentionMs` doubles as the replay-acceptance window: a delivery whose id is
109
+ * still on record (younger than the window) is rejected as a duplicate; entries
110
+ * older than the window are pruned (keeping the file bounded) since a webhook
111
+ * source will not legitimately retry a delivery that old.
112
+ */
113
+ export declare function createFileDeliveryStore(filePath: string, retentionMs?: number): DeliveryStore;
102
114
  export interface RateLimiter {
103
115
  take(key: string): boolean;
104
116
  }
@@ -117,6 +129,12 @@ export interface WebhookServerOptions {
117
129
  rateLimiter?: RateLimiter;
118
130
  rateLimitPerMinute?: number;
119
131
  maxBodyBytes?: number;
132
+ /** Per-IP ingress throttle applied BEFORE the body read (bad-sig flood guard). */
133
+ ipRateLimiter?: RateLimiter;
134
+ /** Per-source-IP requests/minute allowed through to the body read. Default 120. */
135
+ ipRateLimitPerMinute?: number;
136
+ /** Max concurrent TCP connections the receiver accepts. Default 256. */
137
+ maxConnections?: number;
120
138
  }
121
139
  /**
122
140
  * Start a localhost-bound receiver. It accepts only: