@phnx-labs/agents-cli 1.20.74 → 1.20.77

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 (231) hide show
  1. package/CHANGELOG.md +1055 -0
  2. package/README.md +36 -10
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.js +21 -3
  5. package/dist/commands/attach.d.ts +10 -0
  6. package/dist/commands/attach.js +41 -0
  7. package/dist/commands/computer.js +3 -3
  8. package/dist/commands/detach-core.d.ts +46 -0
  9. package/dist/commands/detach-core.js +61 -0
  10. package/dist/commands/detach.d.ts +5 -0
  11. package/dist/commands/detach.js +171 -0
  12. package/dist/commands/doctor.js +18 -0
  13. package/dist/commands/exec.js +83 -13
  14. package/dist/commands/feed.d.ts +7 -1
  15. package/dist/commands/feed.js +47 -4
  16. package/dist/commands/go.d.ts +9 -2
  17. package/dist/commands/go.js +16 -6
  18. package/dist/commands/harness.d.ts +14 -0
  19. package/dist/commands/harness.js +145 -0
  20. package/dist/commands/import.js +38 -8
  21. package/dist/commands/inspect.js +5 -2
  22. package/dist/commands/profiles.d.ts +20 -0
  23. package/dist/commands/profiles.js +68 -21
  24. package/dist/commands/repo.js +14 -0
  25. package/dist/commands/routines.js +286 -15
  26. package/dist/commands/secrets.js +102 -37
  27. package/dist/commands/sessions-browser.js +12 -2
  28. package/dist/commands/sessions-export.js +19 -5
  29. package/dist/commands/sessions-migrate.d.ts +29 -0
  30. package/dist/commands/sessions-migrate.js +596 -0
  31. package/dist/commands/sessions-picker.d.ts +14 -1
  32. package/dist/commands/sessions-picker.js +184 -16
  33. package/dist/commands/sessions.d.ts +56 -14
  34. package/dist/commands/sessions.js +322 -62
  35. package/dist/commands/setup-computer.js +2 -2
  36. package/dist/commands/ssh.d.ts +13 -0
  37. package/dist/commands/ssh.js +110 -37
  38. package/dist/commands/status.js +10 -2
  39. package/dist/commands/versions.js +15 -6
  40. package/dist/commands/view.d.ts +5 -0
  41. package/dist/commands/view.js +24 -4
  42. package/dist/commands/watchdog.d.ts +7 -2
  43. package/dist/commands/watchdog.js +73 -57
  44. package/dist/index.js +59 -7
  45. package/dist/lib/activity.d.ts +17 -6
  46. package/dist/lib/activity.js +466 -40
  47. package/dist/lib/actor.d.ts +50 -0
  48. package/dist/lib/actor.js +166 -0
  49. package/dist/lib/agent-spec/provider.js +2 -1
  50. package/dist/lib/agent-spec/resolve.js +19 -5
  51. package/dist/lib/agent-spec/types.d.ts +9 -1
  52. package/dist/lib/agents.d.ts +21 -0
  53. package/dist/lib/agents.js +70 -6
  54. package/dist/lib/cloud/codex.d.ts +2 -0
  55. package/dist/lib/cloud/codex.js +14 -3
  56. package/dist/lib/cloud/session-index.d.ts +32 -0
  57. package/dist/lib/cloud/session-index.js +58 -0
  58. package/dist/lib/cloud/store.d.ts +7 -0
  59. package/dist/lib/cloud/store.js +25 -0
  60. package/dist/lib/config-transfer.js +4 -0
  61. package/dist/lib/daemon.d.ts +10 -1
  62. package/dist/lib/daemon.js +114 -11
  63. package/dist/lib/devices/connect.d.ts +15 -1
  64. package/dist/lib/devices/connect.js +15 -1
  65. package/dist/lib/devices/fleet.d.ts +37 -1
  66. package/dist/lib/devices/fleet.js +36 -2
  67. package/dist/lib/devices/health-report.d.ts +38 -0
  68. package/dist/lib/devices/health-report.js +214 -0
  69. package/dist/lib/devices/health.js +4 -1
  70. package/dist/lib/devices/reachability.d.ts +31 -0
  71. package/dist/lib/devices/reachability.js +40 -0
  72. package/dist/lib/devices/registry.d.ts +33 -0
  73. package/dist/lib/devices/registry.js +37 -0
  74. package/dist/lib/devices/resolve-target.d.ts +15 -27
  75. package/dist/lib/devices/resolve-target.js +63 -102
  76. package/dist/lib/devices/sync.d.ts +18 -0
  77. package/dist/lib/devices/sync.js +23 -1
  78. package/dist/lib/devices/tailscale.d.ts +3 -0
  79. package/dist/lib/devices/tailscale.js +1 -0
  80. package/dist/lib/events.d.ts +1 -1
  81. package/dist/lib/exec.d.ts +82 -0
  82. package/dist/lib/exec.js +218 -6
  83. package/dist/lib/feed-post.d.ts +63 -0
  84. package/dist/lib/feed-post.js +204 -0
  85. package/dist/lib/feed.d.ts +7 -1
  86. package/dist/lib/feed.js +96 -6
  87. package/dist/lib/fleet/apply.d.ts +32 -2
  88. package/dist/lib/fleet/apply.js +97 -10
  89. package/dist/lib/fleet/types.d.ts +11 -0
  90. package/dist/lib/fs-walk.d.ts +13 -0
  91. package/dist/lib/fs-walk.js +16 -7
  92. package/dist/lib/heal.d.ts +7 -4
  93. package/dist/lib/heal.js +10 -22
  94. package/dist/lib/hooks.d.ts +1 -0
  95. package/dist/lib/hooks.js +156 -0
  96. package/dist/lib/hosts/dispatch.d.ts +16 -0
  97. package/dist/lib/hosts/dispatch.js +26 -4
  98. package/dist/lib/hosts/passthrough.js +9 -2
  99. package/dist/lib/hosts/reconnect.d.ts +78 -0
  100. package/dist/lib/hosts/reconnect.js +127 -0
  101. package/dist/lib/hosts/registry.d.ts +75 -14
  102. package/dist/lib/hosts/registry.js +205 -30
  103. package/dist/lib/hosts/remote-cmd.d.ts +11 -0
  104. package/dist/lib/hosts/remote-cmd.js +32 -8
  105. package/dist/lib/hosts/remote-session-id.d.ts +45 -0
  106. package/dist/lib/hosts/remote-session-id.js +84 -0
  107. package/dist/lib/hosts/run-target.d.ts +17 -5
  108. package/dist/lib/hosts/run-target.js +30 -10
  109. package/dist/lib/hosts/session-index.d.ts +18 -1
  110. package/dist/lib/hosts/session-index.js +40 -5
  111. package/dist/lib/hosts/session-marker.d.ts +33 -0
  112. package/dist/lib/hosts/session-marker.js +51 -0
  113. package/dist/lib/hosts/tasks.d.ts +8 -4
  114. package/dist/lib/import.d.ts +2 -0
  115. package/dist/lib/import.js +35 -2
  116. package/dist/lib/menubar/install-menubar.d.ts +29 -0
  117. package/dist/lib/menubar/install-menubar.js +59 -1
  118. package/dist/lib/menubar/notify-desktop.d.ts +44 -0
  119. package/dist/lib/menubar/notify-desktop.js +78 -0
  120. package/dist/lib/migrate.d.ts +16 -0
  121. package/dist/lib/migrate.js +36 -0
  122. package/dist/lib/models.d.ts +27 -0
  123. package/dist/lib/models.js +54 -1
  124. package/dist/lib/overdue.d.ts +6 -2
  125. package/dist/lib/overdue.js +10 -36
  126. package/dist/lib/picker.js +4 -1
  127. package/dist/lib/platform/process.d.ts +17 -0
  128. package/dist/lib/platform/process.js +70 -0
  129. package/dist/lib/profiles-presets.js +9 -7
  130. package/dist/lib/profiles.d.ts +31 -0
  131. package/dist/lib/profiles.js +70 -0
  132. package/dist/lib/pty-server.d.ts +2 -10
  133. package/dist/lib/pty-server.js +4 -38
  134. package/dist/lib/registry.d.ts +1 -1
  135. package/dist/lib/registry.js +48 -8
  136. package/dist/lib/rotate.d.ts +18 -0
  137. package/dist/lib/rotate.js +28 -0
  138. package/dist/lib/routine-notify.d.ts +76 -0
  139. package/dist/lib/routine-notify.js +190 -0
  140. package/dist/lib/routines-placement.d.ts +38 -0
  141. package/dist/lib/routines-placement.js +79 -0
  142. package/dist/lib/routines-project.d.ts +97 -0
  143. package/dist/lib/routines-project.js +349 -0
  144. package/dist/lib/routines.d.ts +94 -0
  145. package/dist/lib/routines.js +93 -9
  146. package/dist/lib/runner.d.ts +12 -2
  147. package/dist/lib/runner.js +242 -20
  148. package/dist/lib/sandbox.js +10 -0
  149. package/dist/lib/secrets/account-token.d.ts +20 -0
  150. package/dist/lib/secrets/account-token.js +64 -0
  151. package/dist/lib/secrets/agent.d.ts +74 -4
  152. package/dist/lib/secrets/agent.js +181 -107
  153. package/dist/lib/secrets/bundles.d.ts +54 -6
  154. package/dist/lib/secrets/bundles.js +230 -34
  155. package/dist/lib/secrets/index.d.ts +26 -3
  156. package/dist/lib/secrets/index.js +42 -12
  157. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  158. package/dist/lib/secrets/rc-hygiene.js +154 -0
  159. package/dist/lib/secrets/remote.d.ts +7 -4
  160. package/dist/lib/secrets/remote.js +7 -4
  161. package/dist/lib/secrets/session-store.d.ts +6 -2
  162. package/dist/lib/secrets/session-store.js +65 -15
  163. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  164. package/dist/lib/secrets/sync-commands.js +21 -0
  165. package/dist/lib/secrets/unlock-hints.d.ts +27 -0
  166. package/dist/lib/secrets/unlock-hints.js +36 -0
  167. package/dist/lib/self-update.d.ts +23 -2
  168. package/dist/lib/self-update.js +86 -5
  169. package/dist/lib/session/active.d.ts +124 -7
  170. package/dist/lib/session/active.js +390 -56
  171. package/dist/lib/session/cloud.js +2 -1
  172. package/dist/lib/session/db.d.ts +54 -0
  173. package/dist/lib/session/db.js +320 -19
  174. package/dist/lib/session/detached.d.ts +30 -0
  175. package/dist/lib/session/detached.js +92 -0
  176. package/dist/lib/session/discover.d.ts +478 -3
  177. package/dist/lib/session/discover.js +1740 -472
  178. package/dist/lib/session/fork.js +2 -1
  179. package/dist/lib/session/hook-sessions.d.ts +43 -0
  180. package/dist/lib/session/hook-sessions.js +135 -0
  181. package/dist/lib/session/linear.d.ts +6 -0
  182. package/dist/lib/session/linear.js +55 -0
  183. package/dist/lib/session/migrate-targets.d.ts +65 -0
  184. package/dist/lib/session/migrate-targets.js +94 -0
  185. package/dist/lib/session/migrations.d.ts +37 -0
  186. package/dist/lib/session/migrations.js +60 -0
  187. package/dist/lib/session/parse.d.ts +18 -0
  188. package/dist/lib/session/parse.js +141 -32
  189. package/dist/lib/session/pid-registry.d.ts +34 -2
  190. package/dist/lib/session/pid-registry.js +49 -2
  191. package/dist/lib/session/prompt.d.ts +7 -0
  192. package/dist/lib/session/prompt.js +37 -0
  193. package/dist/lib/session/provenance.d.ts +7 -0
  194. package/dist/lib/session/render.d.ts +7 -2
  195. package/dist/lib/session/render.js +40 -26
  196. package/dist/lib/session/short-id.d.ts +17 -0
  197. package/dist/lib/session/short-id.js +20 -0
  198. package/dist/lib/session/state.d.ts +4 -0
  199. package/dist/lib/session/state.js +99 -13
  200. package/dist/lib/session/types.d.ts +25 -1
  201. package/dist/lib/session/types.js +8 -0
  202. package/dist/lib/shims.d.ts +1 -1
  203. package/dist/lib/shims.js +20 -6
  204. package/dist/lib/sqlite.d.ts +3 -2
  205. package/dist/lib/sqlite.js +27 -4
  206. package/dist/lib/staleness/detectors/commands.js +1 -1
  207. package/dist/lib/staleness/detectors/workflows.js +13 -2
  208. package/dist/lib/staleness/writers/commands.js +7 -7
  209. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  210. package/dist/lib/startup/command-registry.d.ts +1 -0
  211. package/dist/lib/startup/command-registry.js +3 -0
  212. package/dist/lib/state.d.ts +8 -4
  213. package/dist/lib/state.js +21 -37
  214. package/dist/lib/teams/agents.d.ts +19 -10
  215. package/dist/lib/teams/agents.js +40 -39
  216. package/dist/lib/types.d.ts +47 -2
  217. package/dist/lib/usage.d.ts +33 -1
  218. package/dist/lib/usage.js +172 -12
  219. package/dist/lib/versions.d.ts +9 -2
  220. package/dist/lib/versions.js +37 -8
  221. package/dist/lib/watchdog/log.d.ts +43 -0
  222. package/dist/lib/watchdog/log.js +69 -0
  223. package/dist/lib/watchdog/routine.d.ts +44 -0
  224. package/dist/lib/watchdog/routine.js +69 -0
  225. package/dist/lib/watchdog/runner.d.ts +51 -7
  226. package/dist/lib/watchdog/runner.js +239 -64
  227. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  228. package/dist/lib/watchdog/watchdog.js +31 -16
  229. package/dist/lib/workflows.d.ts +16 -0
  230. package/dist/lib/workflows.js +110 -1
  231. package/package.json +1 -1
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Detect credentials exported from shell rc files.
3
+ *
4
+ * A secret exported from `~/.zshenv`, `~/.zshrc`, `~/.bashrc`, `~/.profile`, …
5
+ * is inherited by every process the login shell spawns and is readable from
6
+ * `/proc/<pid>/environ` by any same-user process — the master passphrase leak
7
+ * behind RUSH-1968. `.zshenv` in particular is sourced by *every* zsh
8
+ * invocation, including non-interactive `ssh host 'cmd'`, so the value lands in
9
+ * the environment of essentially everything the box runs.
10
+ *
11
+ * The keychain-backed store (`agents secrets`) is the sanctioned home for
12
+ * credentials; the file-store master passphrase belongs in the off-env 0600 key
13
+ * file (`~/.agents/.secrets-key/passphrase`), never a shell rc export.
14
+ *
15
+ * This scanner powers a warn-level advisory in `agents doctor`. It reads only
16
+ * the exported variable *name* and its line number — never the value — so a
17
+ * finding can be printed, logged, or shipped without leaking the secret.
18
+ */
19
+ /** Shell rc files sourced at login/startup, in the order a report lists them. */
20
+ export declare const RC_FILENAMES: readonly [".zshenv", ".zprofile", ".zshrc", ".zlogin", ".bash_profile", ".bash_login", ".bashrc", ".profile", ".kshrc"];
21
+ /**
22
+ * A single credential-shaped export found in an rc file. Carries the variable
23
+ * NAME and location only — the value is never captured.
24
+ */
25
+ export interface RcSecretFinding {
26
+ /** Basename of the rc file, e.g. `.zshenv`. */
27
+ file: string;
28
+ /** 1-based line number of the export. */
29
+ line: number;
30
+ /** The exported variable name (e.g. `AGENTS_SECRETS_PASSPHRASE`). Never the value. */
31
+ name: string;
32
+ /** The file-store master passphrase gets called out separately — it is the highest-severity case. */
33
+ isMasterPassphrase: boolean;
34
+ }
35
+ /** True if a variable name looks like it holds a credential value. */
36
+ export declare function isCredentialName(name: string): boolean;
37
+ /**
38
+ * Scan one rc file's contents for credential-shaped exports. Pure — takes the
39
+ * text, returns findings (names + line numbers only, never values). A trailing
40
+ * `# comment` is stripped before matching so a commented-out export is ignored;
41
+ * the variable name always precedes any inline `#`, so stripping never loses it.
42
+ */
43
+ export declare function scanRcExports(basename: string, content: string): RcSecretFinding[];
44
+ /**
45
+ * Scan the current user's shell rc files. Reads each file that exists under
46
+ * `homeDir` (default `os.homedir()`) and aggregates the findings. Missing or
47
+ * unreadable files are skipped silently — an advisory, not a hard requirement.
48
+ */
49
+ export declare function scanUserRcFiles(homeDir?: string): RcSecretFinding[];
50
+ /**
51
+ * Build the advisory lines for `agents doctor` from a set of findings. Returns
52
+ * `[]` when there is nothing to report. The first line is the headline; the
53
+ * rest are indented detail. Names only — never values.
54
+ */
55
+ export declare function rcSecretWarningLines(findings: RcSecretFinding[]): string[];
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Detect credentials exported from shell rc files.
3
+ *
4
+ * A secret exported from `~/.zshenv`, `~/.zshrc`, `~/.bashrc`, `~/.profile`, …
5
+ * is inherited by every process the login shell spawns and is readable from
6
+ * `/proc/<pid>/environ` by any same-user process — the master passphrase leak
7
+ * behind RUSH-1968. `.zshenv` in particular is sourced by *every* zsh
8
+ * invocation, including non-interactive `ssh host 'cmd'`, so the value lands in
9
+ * the environment of essentially everything the box runs.
10
+ *
11
+ * The keychain-backed store (`agents secrets`) is the sanctioned home for
12
+ * credentials; the file-store master passphrase belongs in the off-env 0600 key
13
+ * file (`~/.agents/.secrets-key/passphrase`), never a shell rc export.
14
+ *
15
+ * This scanner powers a warn-level advisory in `agents doctor`. It reads only
16
+ * the exported variable *name* and its line number — never the value — so a
17
+ * finding can be printed, logged, or shipped without leaking the secret.
18
+ */
19
+ import * as fs from 'fs';
20
+ import * as os from 'os';
21
+ import * as path from 'path';
22
+ /** Shell rc files sourced at login/startup, in the order a report lists them. */
23
+ export const RC_FILENAMES = [
24
+ '.zshenv',
25
+ '.zprofile',
26
+ '.zshrc',
27
+ '.zlogin',
28
+ '.bash_profile',
29
+ '.bash_login',
30
+ '.bashrc',
31
+ '.profile',
32
+ '.kshrc',
33
+ ];
34
+ /** The file-store master key. Its own resolution prefers an off-env 0600 file, so
35
+ * a shell-rc export is always wrong once the store exists (RUSH-1968). */
36
+ const MASTER_PASSPHRASE = 'AGENTS_SECRETS_PASSPHRASE';
37
+ /**
38
+ * Last `_`-delimited segment values that mark a variable as credential-shaped.
39
+ * Matched against the FINAL segment (segment equality, not substring) so
40
+ * `X_API_KEY` fires on `KEY` while `SOME_KEY_PATH` (ends `PATH`) and `MONKEY`
41
+ * (single segment, not equal to `KEY`) do not.
42
+ */
43
+ const CREDENTIAL_SEGMENTS = new Set([
44
+ 'PASSPHRASE',
45
+ 'PASSWORD',
46
+ 'PASSWD',
47
+ 'SECRET',
48
+ 'SECRETS',
49
+ 'TOKEN',
50
+ 'KEY',
51
+ 'APIKEY',
52
+ 'PAT',
53
+ 'CREDENTIAL',
54
+ 'CREDENTIALS',
55
+ ]);
56
+ /**
57
+ * Final segments that look credential-shaped but name a file path, socket, or
58
+ * config knob rather than a secret value — excluded to cut false positives.
59
+ * Applied to the whole name's final segment before the credential check.
60
+ */
61
+ const BENIGN_FINAL_SEGMENTS = new Set([
62
+ 'PATH',
63
+ 'FILE',
64
+ 'DIR',
65
+ 'SOCK',
66
+ 'SOCKET',
67
+ 'ID',
68
+ 'NAME',
69
+ 'PARALLELISM', // TOKENIZERS_PARALLELISM and friends
70
+ ]);
71
+ /** True if a variable name looks like it holds a credential value. */
72
+ export function isCredentialName(name) {
73
+ if (name === MASTER_PASSPHRASE)
74
+ return true;
75
+ const segments = name.toUpperCase().split('_');
76
+ const last = segments[segments.length - 1];
77
+ if (BENIGN_FINAL_SEGMENTS.has(last))
78
+ return false;
79
+ return CREDENTIAL_SEGMENTS.has(last);
80
+ }
81
+ // Matches an env assignment that puts a value in the environment:
82
+ // export NAME=... (sh/bash/zsh)
83
+ // typeset -x NAME=... / declare -x NAME=... (bash/zsh export-attribute form)
84
+ const EXPORT_RE = /^\s*(?:export\s+|(?:typeset|declare)\s+-[A-Za-z]*x[A-Za-z]*\s+)([A-Za-z_][A-Za-z0-9_]*)\s*=/;
85
+ /**
86
+ * Scan one rc file's contents for credential-shaped exports. Pure — takes the
87
+ * text, returns findings (names + line numbers only, never values). A trailing
88
+ * `# comment` is stripped before matching so a commented-out export is ignored;
89
+ * the variable name always precedes any inline `#`, so stripping never loses it.
90
+ */
91
+ export function scanRcExports(basename, content) {
92
+ const out = [];
93
+ const lines = content.split('\n');
94
+ for (let i = 0; i < lines.length; i++) {
95
+ const code = lines[i].replace(/#.*$/, '');
96
+ const m = EXPORT_RE.exec(code);
97
+ if (!m)
98
+ continue;
99
+ const name = m[1];
100
+ if (!isCredentialName(name))
101
+ continue;
102
+ out.push({
103
+ file: basename,
104
+ line: i + 1,
105
+ name,
106
+ isMasterPassphrase: name === MASTER_PASSPHRASE,
107
+ });
108
+ }
109
+ return out;
110
+ }
111
+ /**
112
+ * Scan the current user's shell rc files. Reads each file that exists under
113
+ * `homeDir` (default `os.homedir()`) and aggregates the findings. Missing or
114
+ * unreadable files are skipped silently — an advisory, not a hard requirement.
115
+ */
116
+ export function scanUserRcFiles(homeDir = os.homedir()) {
117
+ const out = [];
118
+ for (const name of RC_FILENAMES) {
119
+ const fp = path.join(homeDir, name);
120
+ let content;
121
+ try {
122
+ content = fs.readFileSync(fp, 'utf8');
123
+ }
124
+ catch {
125
+ continue; // absent or unreadable — nothing to report
126
+ }
127
+ out.push(...scanRcExports(name, content));
128
+ }
129
+ return out;
130
+ }
131
+ /**
132
+ * Build the advisory lines for `agents doctor` from a set of findings. Returns
133
+ * `[]` when there is nothing to report. The first line is the headline; the
134
+ * rest are indented detail. Names only — never values.
135
+ */
136
+ export function rcSecretWarningLines(findings) {
137
+ if (findings.length === 0)
138
+ return [];
139
+ const lines = [];
140
+ const master = findings.filter((f) => f.isMasterPassphrase);
141
+ const others = findings.filter((f) => !f.isMasterPassphrase);
142
+ const total = findings.length;
143
+ lines.push(`${total} credential-shaped export${total === 1 ? '' : 's'} found in shell rc files — ` +
144
+ `readable from /proc/<pid>/environ by any same-user process.`);
145
+ for (const f of master) {
146
+ lines.push(`${f.file}:${f.line} ${f.name} — the file-store master key. Move it off-env to ` +
147
+ `~/.agents/.secrets-key/passphrase (chmod 600) and delete the export.`);
148
+ }
149
+ for (const f of others) {
150
+ lines.push(`${f.file}:${f.line} ${f.name} — move to \`agents secrets\` and inject via \`agents secrets exec\`.`);
151
+ }
152
+ lines.push('Rule: no credentials in env vars or shell config. Use the keychain-backed store.');
153
+ return lines;
154
+ }
@@ -34,11 +34,14 @@ import { type SshExecResult } from '../ssh-exec.js';
34
34
  */
35
35
  export declare function isDangerousRemoteEnvKey(name: string): boolean;
36
36
  /**
37
- * Resolve a `--host` value to an ssh target string. Tries the `agents hosts`
38
- * registry first (enrolled name ssh-config alias / `user@host`); on a miss,
39
- * treats the value as a raw ssh target and validates it against injection.
37
+ * Resolve a `--host` value to an ssh target STRING for the remote-secrets path.
38
+ * Delegates to the single host/device resolver (`resolveHost`, RUSH-1967) so a
39
+ * name here dials the exact same box `run --host` does; on a miss, treats the
40
+ * value as a raw ssh target and validates it against injection. Named distinctly
41
+ * from `../devices/resolve-target.ts` (which returns richer shapes) so importing
42
+ * the wrong one can't silently change which machine you dial.
40
43
  */
41
- export declare function resolveSshTarget(nameOrAlias: string): Promise<string>;
44
+ export declare function resolveHostSshTarget(nameOrAlias: string): Promise<string>;
42
45
  /**
43
46
  * Merge `--host <single>` and `--hosts <a,b,c>` into an ordered, de-duplicated
44
47
  * list. Both flags compose; either alone works. Empty when neither is set.
@@ -59,11 +59,14 @@ function osForTarget(target, lookupName) {
59
59
  return byName ?? resolveRemoteOsSync(target.split('@').pop() ?? target);
60
60
  }
61
61
  /**
62
- * Resolve a `--host` value to an ssh target string. Tries the `agents hosts`
63
- * registry first (enrolled name ssh-config alias / `user@host`); on a miss,
64
- * treats the value as a raw ssh target and validates it against injection.
62
+ * Resolve a `--host` value to an ssh target STRING for the remote-secrets path.
63
+ * Delegates to the single host/device resolver (`resolveHost`, RUSH-1967) so a
64
+ * name here dials the exact same box `run --host` does; on a miss, treats the
65
+ * value as a raw ssh target and validates it against injection. Named distinctly
66
+ * from `../devices/resolve-target.ts` (which returns richer shapes) so importing
67
+ * the wrong one can't silently change which machine you dial.
65
68
  */
66
- export async function resolveSshTarget(nameOrAlias) {
69
+ export async function resolveHostSshTarget(nameOrAlias) {
67
70
  const host = await resolveHost(nameOrAlias);
68
71
  if (host)
69
72
  return sshTargetFor(host);
@@ -38,12 +38,14 @@ export interface SessionEntry {
38
38
  expiresAt: number;
39
39
  /** true only for `--durable` unlocks — survives SLEEP. */
40
40
  sleepPersist: boolean;
41
+ harness?: string;
41
42
  }
42
43
  /** Metadata for one held bundle, kept in the index so we can rehydrate / prune
43
44
  * without reading every blob. */
44
45
  export interface SessionIndexMeta {
45
46
  expiresAt: number;
46
47
  sleepPersist: boolean;
48
+ harness?: string;
47
49
  }
48
50
  export interface SessionIndex {
49
51
  bundles: Record<string, SessionIndexMeta>;
@@ -73,9 +75,11 @@ export declare function writeIndex(index: SessionIndex): void;
73
75
  /** Persist one unlocked bundle: write its blob no-ACL and record it in the index. */
74
76
  export declare function saveSession(name: string, entry: SessionEntry): void;
75
77
  /** Read one session blob by known name. Null when absent/expired/malformed. */
76
- export declare function loadSession(name: string, now?: number): SessionEntry | null;
78
+ export declare function loadSession(name: string, now?: number, harness?: string): SessionEntry | null;
79
+ /** Remove every persisted harness grant for one bundle. */
80
+ export declare function deleteBundleSessions(name: string): void;
77
81
  /** Delete one bundle's session blob and prune it from the index. */
78
- export declare function deleteSession(name: string): void;
82
+ export declare function deleteSession(name: string, harness?: string): void;
79
83
  /** Delete every session blob + the index (for `secrets lock --all`). */
80
84
  export declare function deleteAllSessions(): void;
81
85
  /** Rehydrate every unexpired session into `[name, entry]` pairs for the broker to
@@ -82,8 +82,8 @@ export function removeEntry(index, name) {
82
82
  function shouldPersist() {
83
83
  return process.platform === 'darwin' || isKeychainBackendOverridden();
84
84
  }
85
- function sessionBlobItem(name) {
86
- return `${SESSION_ITEM_PREFIX}${name}`;
85
+ function sessionBlobItem(name, harness) {
86
+ return `${SESSION_ITEM_PREFIX}${harness}.${name}`;
87
87
  }
88
88
  /** Read the session index by its fixed name. `{bundles:{}}` when absent/unreadable. */
89
89
  export function readIndex() {
@@ -116,24 +116,26 @@ export function saveSession(name, entry) {
116
116
  if (!shouldPersist())
117
117
  return;
118
118
  try {
119
- setKeychainToken(sessionBlobItem(name), JSON.stringify(entry), { noAcl: true });
120
- writeIndex(upsertEntry(readIndex(), name, { expiresAt: entry.expiresAt, sleepPersist: entry.sleepPersist }));
119
+ const harness = entry.harness || 'cli';
120
+ const key = `${harness}:${name}`;
121
+ setKeychainToken(sessionBlobItem(name, harness), JSON.stringify({ ...entry, harness }), { noAcl: true });
122
+ writeIndex(upsertEntry(readIndex(), key, { expiresAt: entry.expiresAt, sleepPersist: entry.sleepPersist, harness }));
121
123
  }
122
124
  catch {
123
125
  /* best-effort — persistence is an optimization */
124
126
  }
125
127
  }
126
128
  /** Read one session blob by known name. Null when absent/expired/malformed. */
127
- export function loadSession(name, now = Date.now()) {
129
+ export function loadSession(name, now = Date.now(), harness = 'cli') {
128
130
  if (!shouldPersist())
129
131
  return null;
130
132
  try {
131
- const raw = getKeychainToken(sessionBlobItem(name));
133
+ const raw = getKeychainToken(sessionBlobItem(name, harness));
132
134
  const entry = JSON.parse(raw);
133
135
  if (!entry || typeof entry !== 'object' || !entry.bundle || !entry.env)
134
136
  return null;
135
137
  if (now >= entry.expiresAt) {
136
- deleteSession(name); // drop expired on read, mirroring the broker's get handler
138
+ deleteSession(name, harness); // drop expired on read
137
139
  return null;
138
140
  }
139
141
  return entry;
@@ -142,13 +144,32 @@ export function loadSession(name, now = Date.now()) {
142
144
  return null;
143
145
  }
144
146
  }
147
+ /** Remove every persisted harness grant for one bundle. */
148
+ export function deleteBundleSessions(name) {
149
+ if (!shouldPersist())
150
+ return;
151
+ const index = readIndex();
152
+ let next = index;
153
+ for (const [key, meta] of Object.entries(index.bundles)) {
154
+ const scopedName = key.includes(':') ? key.split(':').slice(1).join(':') : key;
155
+ if (scopedName !== name)
156
+ continue;
157
+ const harness = meta.harness || 'cli';
158
+ try {
159
+ deleteKeychainToken(key.includes(':') ? sessionBlobItem(name, harness) : `${SESSION_ITEM_PREFIX}${name}`);
160
+ }
161
+ catch { /* keep going */ }
162
+ next = removeEntry(next, key);
163
+ }
164
+ writeIndex(next);
165
+ }
145
166
  /** Delete one bundle's session blob and prune it from the index. */
146
- export function deleteSession(name) {
167
+ export function deleteSession(name, harness = 'cli') {
147
168
  if (!shouldPersist())
148
169
  return;
149
170
  try {
150
- deleteKeychainToken(sessionBlobItem(name));
151
- writeIndex(removeEntry(readIndex(), name));
171
+ deleteKeychainToken(sessionBlobItem(name, harness));
172
+ writeIndex(removeEntry(readIndex(), `${harness}:${name}`));
152
173
  }
153
174
  catch {
154
175
  /* best-effort */
@@ -160,8 +181,12 @@ export function deleteAllSessions() {
160
181
  return;
161
182
  try {
162
183
  for (const name of Object.keys(readIndex().bundles)) {
184
+ const meta = readIndex().bundles[name];
185
+ const bundleName = name.includes(':') ? name.split(':').slice(1).join(':') : name;
163
186
  try {
164
- deleteKeychainToken(sessionBlobItem(name));
187
+ deleteKeychainToken(name.includes(':')
188
+ ? sessionBlobItem(bundleName, meta.harness || 'cli')
189
+ : `${SESSION_ITEM_PREFIX}${bundleName}`);
165
190
  }
166
191
  catch { /* keep going */ }
167
192
  }
@@ -180,19 +205,43 @@ export function rehydrateSessions(now = Date.now()) {
180
205
  const out = [];
181
206
  try {
182
207
  const index = readIndex();
208
+ // One-time source migration from the pre-harness layout. Move each legacy
209
+ // bundle-name index/blob to the explicit cli scope, then delete the old blob.
210
+ let migratedIndex = index;
211
+ for (const [key, meta] of Object.entries(index.bundles)) {
212
+ if (key.includes(':'))
213
+ continue;
214
+ try {
215
+ const raw = getKeychainToken(`${SESSION_ITEM_PREFIX}${key}`);
216
+ const legacy = JSON.parse(raw);
217
+ setKeychainToken(sessionBlobItem(key, 'cli'), JSON.stringify({ ...legacy, harness: 'cli' }), { noAcl: true });
218
+ deleteKeychainToken(`${SESSION_ITEM_PREFIX}${key}`);
219
+ migratedIndex = upsertEntry(removeEntry(migratedIndex, key), `cli:${key}`, {
220
+ expiresAt: meta.expiresAt,
221
+ sleepPersist: meta.sleepPersist,
222
+ harness: 'cli',
223
+ });
224
+ }
225
+ catch { /* malformed/absent legacy entry is pruned below */ }
226
+ }
227
+ writeIndex(migratedIndex);
228
+ index.bundles = migratedIndex.bundles;
183
229
  const { survivors, expiredNames } = pruneExpired(index, now);
184
230
  for (const name of expiredNames) {
231
+ const meta = index.bundles[name];
185
232
  try {
186
- deleteKeychainToken(sessionBlobItem(name));
233
+ deleteKeychainToken(sessionBlobItem(name.split(':').slice(1).join(':'), meta.harness || 'cli'));
187
234
  }
188
235
  catch { /* keep going */ }
189
236
  }
190
237
  if (expiredNames.length)
191
238
  writeIndex(survivors);
192
239
  for (const name of selectRehydratable(survivors, now)) {
193
- const entry = loadSession(name, now);
240
+ const meta = survivors.bundles[name];
241
+ const bundleName = name.split(':').slice(1).join(':');
242
+ const entry = loadSession(bundleName, now, meta.harness || 'cli');
194
243
  if (entry)
195
- out.push({ name, entry });
244
+ out.push({ name: bundleName, entry });
196
245
  }
197
246
  }
198
247
  catch {
@@ -208,8 +257,9 @@ export function pruneSessionsOnSleep() {
208
257
  try {
209
258
  const { survivors, deletedNames } = pruneOnSleep(readIndex());
210
259
  for (const name of deletedNames) {
260
+ const meta = readIndex().bundles[name];
211
261
  try {
212
- deleteKeychainToken(sessionBlobItem(name));
262
+ deleteKeychainToken(sessionBlobItem(name.split(':').slice(1).join(':'), meta.harness || 'cli'));
213
263
  }
214
264
  catch { /* keep going */ }
215
265
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The argv tokens for the synchronous secrets-broker clients: the tokens
3
+ * `agentGetSync`/`agentReachableSync`/`agentEvictSync` spawn, and the tokens
4
+ * `index.ts` dispatches on before commander.
5
+ *
6
+ * This is a LEAF module — it imports nothing, so the CLI entrypoint can bind
7
+ * these statically without dragging the secrets graph (and its `state.js` /
8
+ * `install-helper.js` / `session-store.js` imports) into every invocation. That
9
+ * is the whole reason the file exists: with the tokens in `agent.ts`, `index.ts`
10
+ * had to repeat them as string literals, and the two could silently drift —
11
+ * a drift with no visible failure mode, because the CLI answers "unknown
12
+ * command", the client reads the non-zero exit as "broker down", and every
13
+ * secret read falls back to a Touch ID prompt. Exactly the bug this subsystem
14
+ * was fixed for. Sharing one binding makes that divergence a type error rather
15
+ * than something a test has to notice.
16
+ *
17
+ * Same leaf discipline, and same motivation, as `lib/cli-entry.ts`.
18
+ */
19
+ export declare const SYNC_GET_CMD = "__secrets-get";
20
+ export declare const SYNC_PING_CMD = "__secrets-ping";
21
+ export declare const SYNC_LOCK_CMD = "__secrets-lock";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The argv tokens for the synchronous secrets-broker clients: the tokens
3
+ * `agentGetSync`/`agentReachableSync`/`agentEvictSync` spawn, and the tokens
4
+ * `index.ts` dispatches on before commander.
5
+ *
6
+ * This is a LEAF module — it imports nothing, so the CLI entrypoint can bind
7
+ * these statically without dragging the secrets graph (and its `state.js` /
8
+ * `install-helper.js` / `session-store.js` imports) into every invocation. That
9
+ * is the whole reason the file exists: with the tokens in `agent.ts`, `index.ts`
10
+ * had to repeat them as string literals, and the two could silently drift —
11
+ * a drift with no visible failure mode, because the CLI answers "unknown
12
+ * command", the client reads the non-zero exit as "broker down", and every
13
+ * secret read falls back to a Touch ID prompt. Exactly the bug this subsystem
14
+ * was fixed for. Sharing one binding makes that divergence a type error rather
15
+ * than something a test has to notice.
16
+ *
17
+ * Same leaf discipline, and same motivation, as `lib/cli-entry.ts`.
18
+ */
19
+ export const SYNC_GET_CMD = '__secrets-get';
20
+ export const SYNC_PING_CMD = '__secrets-ping';
21
+ export const SYNC_LOCK_CMD = '__secrets-lock';
@@ -0,0 +1,27 @@
1
+ /** The subset of a `secrets.get` event this heuristic reads. */
2
+ export interface SecretGetRecord {
3
+ /** Bundle name the read targeted. */
4
+ bundle?: string;
5
+ /** Where the value came from: `agent` (broker hit) / `session` (durable
6
+ * unlock) are SILENT; anything else hit the keychain and thus prompted. */
7
+ source?: string;
8
+ }
9
+ export interface PromptedBundle {
10
+ name: string;
11
+ /** Number of prompting (keychain) reads in the window. */
12
+ count: number;
13
+ }
14
+ /**
15
+ * From recent `secrets.get` records, find bundles read often enough via the
16
+ * keychain (i.e. NOT served silently by the broker or a durable session) to be
17
+ * worth unlocking once. A bundle currently `held` is already silent, so it is
18
+ * excluded; the caller further drops `never`/no-ACL bundles (which never prompt,
19
+ * so unlocking them is a no-op).
20
+ *
21
+ * @param records recent `secrets.get` events (any order)
22
+ * @param held bundle names the broker currently holds (silent reads)
23
+ * @param opts.minReads minimum prompting reads to surface a bundle (default 3)
24
+ */
25
+ export declare function frequentlyPromptedBundles(records: SecretGetRecord[], held: Set<string>, opts?: {
26
+ minReads?: number;
27
+ }): PromptedBundle[];
@@ -0,0 +1,36 @@
1
+ // Usage-based unlock hints: surface bundles you keep getting a Touch ID prompt
2
+ // for, so `agents secrets status` can suggest unlocking them once instead of
3
+ // prompting on every read. Built on the existing `secrets.get` audit events
4
+ // (events.jsonl) — no new tracking. Pure + exported so the threshold/filter
5
+ // logic is unit-testable without the events sink or the keychain.
6
+ /**
7
+ * From recent `secrets.get` records, find bundles read often enough via the
8
+ * keychain (i.e. NOT served silently by the broker or a durable session) to be
9
+ * worth unlocking once. A bundle currently `held` is already silent, so it is
10
+ * excluded; the caller further drops `never`/no-ACL bundles (which never prompt,
11
+ * so unlocking them is a no-op).
12
+ *
13
+ * @param records recent `secrets.get` events (any order)
14
+ * @param held bundle names the broker currently holds (silent reads)
15
+ * @param opts.minReads minimum prompting reads to surface a bundle (default 3)
16
+ */
17
+ export function frequentlyPromptedBundles(records, held, opts = {}) {
18
+ const minReads = opts.minReads ?? 3;
19
+ const counts = new Map();
20
+ for (const r of records) {
21
+ if (!r.bundle)
22
+ continue;
23
+ // A broker hit or a durable-session read raised no Touch ID sheet — only a
24
+ // read that fell through to the keychain did. Count those; they are the ones
25
+ // an unlock would silence.
26
+ if (r.source === 'agent' || r.source === 'session')
27
+ continue;
28
+ if (held.has(r.bundle))
29
+ continue;
30
+ counts.set(r.bundle, (counts.get(r.bundle) ?? 0) + 1);
31
+ }
32
+ return [...counts.entries()]
33
+ .filter(([, n]) => n >= minReads)
34
+ .map(([name, count]) => ({ name, count }))
35
+ .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
36
+ }
@@ -55,6 +55,23 @@ export declare function saveUpdateCheck(file: string, latestVersion: string): vo
55
55
  export declare function dismissUpdateVersion(file: string, version: string): void;
56
56
  /** Whether the cached state warrants an upgrade prompt for a copy running `currentVersion`. */
57
57
  export declare function shouldPromptUpgrade(cache: UpdateCheckCache | null, currentVersion: string): boolean;
58
+ /**
59
+ * The on-disk package root of the copy that is currently running.
60
+ *
61
+ * For a plain JS install this is just `<__dirname>/..`. Under the compiled
62
+ * standalone binary (shipped since 1.20.53) it is not: Bun sets `__dirname` to
63
+ * its embedded virtual FS, so `<__dirname>/..` yields `/$bunfs` — a path that
64
+ * exists nowhere. That phantom value was reported as a second install by the
65
+ * multi-install check, and rejected by deriveGlobalPrefix as "not an
66
+ * npm-managed install", so every self-upgrade from a compiled copy failed.
67
+ *
68
+ * The physical executable is `process.execPath`, which ships inside the
69
+ * package (`<packageRoot>/dist/bin/agents`). Walk up from it to the directory
70
+ * whose package.json actually names this package rather than assuming a fixed
71
+ * depth, so a change to the dist layout surfaces as a clear throw here instead
72
+ * of a wrong prefix that npm would happily install into.
73
+ */
74
+ export declare function resolveRunningPackageRoot(dirname: string, execPath?: string): string;
58
75
  /**
59
76
  * Derive the npm global prefix that owns the install at `packageRoot`.
60
77
  *
@@ -133,8 +150,12 @@ export interface AgentsCliInstall {
133
150
  *
134
151
  * npm bin entries are symlinks that resolve to `<packageRoot>/dist/index.js`
135
152
  * (the dev install's `~/.local/bin/agents` chains through the dev prefix to
136
- * the same shape). Anything that doesn't resolve to a dist/index.js inside a
137
- * package named @phnx-labs/agents-cli is some other tool and is skipped.
153
+ * the same shape). A shim pointed at the compiled standalone binary resolves
154
+ * to `<packageRoot>/dist/bin/agents` instead; that shape counts too, otherwise
155
+ * the copy that actually runs — the compiled one is typically first on PATH —
156
+ * is invisible to this scan and its root looks like a separate install.
157
+ * Anything that resolves to neither shape inside a package named
158
+ * @phnx-labs/agents-cli is some other tool and is skipped.
138
159
  * POSIX-only: Windows npm bins are .cmd wrappers, not symlinks.
139
160
  */
140
161
  export declare function findAgentsCliInstalls(pathEnv: string): AgentsCliInstall[];