@phnx-labs/agents-cli 1.20.32 → 1.20.34

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 (110) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +28 -2
  3. package/dist/commands/commands.js +3 -3
  4. package/dist/commands/computer-actions.js +1 -0
  5. package/dist/commands/computer.d.ts +23 -0
  6. package/dist/commands/computer.js +45 -3
  7. package/dist/commands/cost.js +2 -2
  8. package/dist/commands/doctor.d.ts +10 -0
  9. package/dist/commands/doctor.js +51 -2
  10. package/dist/commands/exec.js +56 -1
  11. package/dist/commands/hooks.js +3 -3
  12. package/dist/commands/import.js +1 -1
  13. package/dist/commands/inspect.js +13 -17
  14. package/dist/commands/mcp.js +3 -3
  15. package/dist/commands/permissions.js +3 -3
  16. package/dist/commands/rules.js +3 -3
  17. package/dist/commands/secrets-migrate.js +23 -11
  18. package/dist/commands/secrets.d.ts +20 -0
  19. package/dist/commands/secrets.js +53 -1
  20. package/dist/commands/sessions.js +18 -1
  21. package/dist/commands/skills.js +3 -3
  22. package/dist/commands/status.d.ts +12 -0
  23. package/dist/commands/status.js +81 -0
  24. package/dist/commands/sync.js +2 -2
  25. package/dist/commands/teams.js +77 -18
  26. package/dist/commands/usage.js +2 -2
  27. package/dist/commands/utils.d.ts +8 -0
  28. package/dist/commands/utils.js +20 -0
  29. package/dist/commands/versions.js +4 -3
  30. package/dist/commands/view.d.ts +39 -0
  31. package/dist/commands/view.js +217 -84
  32. package/dist/commands/workflows.js +3 -3
  33. package/dist/index.js +16 -2
  34. package/dist/lib/acp/harnesses.d.ts +1 -1
  35. package/dist/lib/acp/harnesses.js +2 -2
  36. package/dist/lib/agent-spec/index.d.ts +18 -0
  37. package/dist/lib/agent-spec/index.js +35 -0
  38. package/dist/lib/agent-spec/primitives.d.ts +28 -0
  39. package/dist/lib/agent-spec/primitives.js +57 -0
  40. package/dist/lib/agent-spec/provider.d.ts +2 -0
  41. package/dist/lib/agent-spec/provider.js +9 -0
  42. package/dist/lib/agent-spec/resolve.d.ts +33 -0
  43. package/dist/lib/agent-spec/resolve.js +174 -0
  44. package/dist/lib/agent-spec/types.d.ts +57 -0
  45. package/dist/lib/agent-spec/types.js +18 -0
  46. package/dist/lib/agents.d.ts +12 -0
  47. package/dist/lib/agents.js +115 -32
  48. package/dist/lib/browser/chrome.js +20 -0
  49. package/dist/lib/browser/drivers/ssh.d.ts +19 -0
  50. package/dist/lib/browser/drivers/ssh.js +18 -3
  51. package/dist/lib/crabbox/cli.d.ts +98 -0
  52. package/dist/lib/crabbox/cli.js +218 -0
  53. package/dist/lib/crabbox/lease.d.ts +41 -0
  54. package/dist/lib/crabbox/lease.js +73 -0
  55. package/dist/lib/crabbox/runtimes.d.ts +57 -0
  56. package/dist/lib/crabbox/runtimes.js +109 -0
  57. package/dist/lib/doctor-diff.js +29 -2
  58. package/dist/lib/drift-sync.d.ts +43 -0
  59. package/dist/lib/drift-sync.js +179 -0
  60. package/dist/lib/exec.d.ts +15 -0
  61. package/dist/lib/exec.js +21 -11
  62. package/dist/lib/hosts/dispatch.d.ts +27 -10
  63. package/dist/lib/hosts/dispatch.js +55 -19
  64. package/dist/lib/hosts/option.d.ts +14 -0
  65. package/dist/lib/hosts/option.js +19 -0
  66. package/dist/lib/hosts/passthrough.d.ts +30 -0
  67. package/dist/lib/hosts/passthrough.js +141 -0
  68. package/dist/lib/hosts/remote-cmd.d.ts +36 -0
  69. package/dist/lib/hosts/remote-cmd.js +56 -0
  70. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  71. package/dist/lib/platform/winpath.d.ts +31 -2
  72. package/dist/lib/platform/winpath.js +133 -24
  73. package/dist/lib/pwsh.d.ts +11 -0
  74. package/dist/lib/pwsh.js +13 -0
  75. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  76. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  77. package/dist/lib/secrets/agent.d.ts +42 -1
  78. package/dist/lib/secrets/agent.js +89 -11
  79. package/dist/lib/secrets/bundles.js +61 -21
  80. package/dist/lib/secrets/filestore.js +31 -1
  81. package/dist/lib/secrets/index.d.ts +44 -1
  82. package/dist/lib/secrets/index.js +108 -10
  83. package/dist/lib/secrets/linux.d.ts +14 -0
  84. package/dist/lib/secrets/linux.js +21 -0
  85. package/dist/lib/secrets/windows.d.ts +74 -0
  86. package/dist/lib/secrets/windows.js +440 -0
  87. package/dist/lib/session/active.d.ts +8 -0
  88. package/dist/lib/session/active.js +18 -1
  89. package/dist/lib/session/provenance.d.ts +56 -0
  90. package/dist/lib/session/provenance.js +157 -0
  91. package/dist/lib/shims.d.ts +20 -0
  92. package/dist/lib/shims.js +53 -20
  93. package/dist/lib/ssh-exec.d.ts +22 -0
  94. package/dist/lib/ssh-exec.js +59 -2
  95. package/dist/lib/ssh-tunnel.d.ts +0 -5
  96. package/dist/lib/ssh-tunnel.js +65 -8
  97. package/dist/lib/startup/command-registry.d.ts +1 -0
  98. package/dist/lib/startup/command-registry.js +2 -0
  99. package/dist/lib/sync-status.d.ts +102 -0
  100. package/dist/lib/sync-status.js +135 -0
  101. package/dist/lib/teams/agents.d.ts +24 -0
  102. package/dist/lib/teams/agents.js +30 -1
  103. package/dist/lib/types.d.ts +20 -1
  104. package/dist/lib/usage.d.ts +30 -0
  105. package/dist/lib/usage.js +159 -2
  106. package/dist/lib/versions.d.ts +2 -4
  107. package/dist/lib/versions.js +7 -20
  108. package/package.json +2 -1
  109. package/dist/lib/agent-spec.d.ts +0 -36
  110. package/dist/lib/agent-spec.js +0 -157
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Windows secret storage via Windows Credential Manager (wincred).
3
+ *
4
+ * Primary backend: the Credential Manager `advapi32` API (CredReadW /
5
+ * CredWriteW / CredDeleteW / CredEnumerateW), reached through a static
6
+ * PowerShell script that P/Invokes the C# shim below. PowerShell (Windows
7
+ * PowerShell 5.1) ships with every supported Windows, so there is no separate
8
+ * install step. Items are stored as CRED_TYPE_GENERIC with
9
+ * CRED_PERSIST_LOCAL_MACHINE — device-local, matching the biometry-bound model
10
+ * on macOS (src/lib/secrets/index.ts).
11
+ *
12
+ * Zero injection surface: the PS script is a single STATIC constant. All
13
+ * dynamic data rides in the child ENV (target name, list prefix) or STDIN (the
14
+ * secret value) — nothing is string-interpolated into the script. The child is
15
+ * spawned with a spawnSync ARGV ARRAY (`-EncodedCommand <base64>`), never a
16
+ * shell string.
17
+ *
18
+ * Headless fallback: when Credential Manager is unreachable (no logon session —
19
+ * ERROR_NO_SUCH_LOGON_SESSION 1312 — or powershell.exe missing from PATH), we
20
+ * transparently switch to the AES-256-GCM encrypted-file store in
21
+ * ./filestore.ts, exactly like the Linux locked-collection fallback. The
22
+ * decision is cached per process; one stderr line is emitted the first time.
23
+ *
24
+ * Item names are stored VERBATIM as the credential TargetName
25
+ * (`agents-cli.bundles.<name>` / `agents-cli.secrets.<bundle>.<key>` — the
26
+ * scheme shared with the file store, see ./filestore.ts) so `list` returns item
27
+ * names directly.
28
+ */
29
+ import type { KeychainBackend } from './index.js';
30
+ export { encryptForFallback, decryptForFallback, fileBackend, type EncFile, } from './filestore.js';
31
+ /**
32
+ * CRED_MAX_CREDENTIAL_BLOB_SIZE — Credential Manager rejects a generic
33
+ * credential blob larger than 2560 bytes with an opaque CredWrite failure. We
34
+ * guard against it in `set` with a clear message. Only pathologically large
35
+ * bundle metadata could hit this; such an item should live in a file-backed
36
+ * bundle (AGENTS_SECRETS_PASSPHRASE) instead.
37
+ */
38
+ export declare const CRED_MAX_CREDENTIAL_BLOB_SIZE = 2560;
39
+ /**
40
+ * True when secret operations currently route to the encrypted-file store
41
+ * instead of Windows Credential Manager. Mirrors linux.ts:usesFileFallback so
42
+ * `listBundles()` doesn't double-count file-backed bundles under the fallback.
43
+ */
44
+ export declare function usesFileFallback(): boolean;
45
+ export declare function hasCredManToken(item: string): boolean;
46
+ export declare function getCredManToken(item: string): string;
47
+ export declare function setCredManToken(item: string, value: string): void;
48
+ export declare function deleteCredManToken(item: string): boolean;
49
+ export declare function listCredManItems(prefix: string): string[];
50
+ /**
51
+ * Parse the target names printed by the `list` op (one per line), keeping only
52
+ * those starting with `prefix` and deduping. Same contract as
53
+ * parseSecretToolItems (linux.ts). Exported for tests.
54
+ */
55
+ export declare function parseWindowsCredList(output: string, prefix: string): string[];
56
+ /**
57
+ * KeychainBackend implementation for Windows. Routes through Windows Credential
58
+ * Manager (via PowerShell P/Invoke) with a transparent encrypted-file fallback
59
+ * when the credential store is unreachable.
60
+ */
61
+ export declare const windowsBackend: KeychainBackend;
62
+ /**
63
+ * Test-only: reset module state so independent test cases don't bleed
64
+ * availability / fallback decisions across each other. Pass `forceAvailable` to
65
+ * pin the powershell-availability probe (skips the real spawn); pass `fileDir`
66
+ * to redirect the encrypted-file store to a temp dir. File-store state lives in
67
+ * ./filestore.ts and is reset there.
68
+ */
69
+ export declare function _resetForTest(opts?: {
70
+ fileDir?: string | null;
71
+ forceFileFallback?: boolean;
72
+ passphrase?: string | null;
73
+ forceAvailable?: boolean | null;
74
+ }): void;
@@ -0,0 +1,440 @@
1
+ /**
2
+ * Windows secret storage via Windows Credential Manager (wincred).
3
+ *
4
+ * Primary backend: the Credential Manager `advapi32` API (CredReadW /
5
+ * CredWriteW / CredDeleteW / CredEnumerateW), reached through a static
6
+ * PowerShell script that P/Invokes the C# shim below. PowerShell (Windows
7
+ * PowerShell 5.1) ships with every supported Windows, so there is no separate
8
+ * install step. Items are stored as CRED_TYPE_GENERIC with
9
+ * CRED_PERSIST_LOCAL_MACHINE — device-local, matching the biometry-bound model
10
+ * on macOS (src/lib/secrets/index.ts).
11
+ *
12
+ * Zero injection surface: the PS script is a single STATIC constant. All
13
+ * dynamic data rides in the child ENV (target name, list prefix) or STDIN (the
14
+ * secret value) — nothing is string-interpolated into the script. The child is
15
+ * spawned with a spawnSync ARGV ARRAY (`-EncodedCommand <base64>`), never a
16
+ * shell string.
17
+ *
18
+ * Headless fallback: when Credential Manager is unreachable (no logon session —
19
+ * ERROR_NO_SUCH_LOGON_SESSION 1312 — or powershell.exe missing from PATH), we
20
+ * transparently switch to the AES-256-GCM encrypted-file store in
21
+ * ./filestore.ts, exactly like the Linux locked-collection fallback. The
22
+ * decision is cached per process; one stderr line is emitted the first time.
23
+ *
24
+ * Item names are stored VERBATIM as the credential TargetName
25
+ * (`agents-cli.bundles.<name>` / `agents-cli.secrets.<bundle>.<key>` — the
26
+ * scheme shared with the file store, see ./filestore.ts) so `list` returns item
27
+ * names directly.
28
+ */
29
+ import { spawnSync } from 'child_process';
30
+ import { encodePwshBase64 } from '../pwsh.js';
31
+ import { fileStore, fileDir, fileStoreHasItems, machinePassphraseExists, _resetFileStoreForTest, } from './filestore.js';
32
+ // Re-exported so importers (and tests) can keep reaching these via './windows.js'.
33
+ export { encryptForFallback, decryptForFallback, fileBackend, } from './filestore.js';
34
+ const POWERSHELL = 'powershell.exe';
35
+ /**
36
+ * CRED_MAX_CREDENTIAL_BLOB_SIZE — Credential Manager rejects a generic
37
+ * credential blob larger than 2560 bytes with an opaque CredWrite failure. We
38
+ * guard against it in `set` with a clear message. Only pathologically large
39
+ * bundle metadata could hit this; such an item should live in a file-backed
40
+ * bundle (AGENTS_SECRETS_PASSPHRASE) instead.
41
+ */
42
+ export const CRED_MAX_CREDENTIAL_BLOB_SIZE = 2560;
43
+ /**
44
+ * The static PowerShell driver. Dispatches on $env:AGENTS_CRED_OP; reads the
45
+ * target name from $env:AGENTS_CRED_TARGET, the list prefix from
46
+ * $env:AGENTS_CRED_PREFIX, and (for `set`) the raw secret value from stdin.
47
+ * Nothing dynamic is interpolated into this string.
48
+ *
49
+ * Exit codes: 0 = success, 3 = clean "not found", 1 = error (message on stderr,
50
+ * carrying the Win32 code so the Node side can detect an unavailable store).
51
+ * `get` emits the blob as base64 (dodges PowerShell CRLF/encoding corruption);
52
+ * `list` prints one target name per line.
53
+ */
54
+ const PS_SCRIPT = `
55
+ $ErrorActionPreference = 'Stop'
56
+ Add-Type -TypeDefinition @'
57
+ using System;
58
+ using System.Runtime.InteropServices;
59
+ using System.Collections.Generic;
60
+
61
+ public static class AgentsCred {
62
+ const uint CRED_TYPE_GENERIC = 1;
63
+ const uint CRED_PERSIST_LOCAL_MACHINE = 2;
64
+ const int ERROR_NOT_FOUND = 1168;
65
+
66
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
67
+ struct CREDENTIAL {
68
+ public uint Flags;
69
+ public uint Type;
70
+ public IntPtr TargetName;
71
+ public IntPtr Comment;
72
+ public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
73
+ public uint CredentialBlobSize;
74
+ public IntPtr CredentialBlob;
75
+ public uint Persist;
76
+ public uint AttributeCount;
77
+ public IntPtr Attributes;
78
+ public IntPtr TargetAlias;
79
+ public IntPtr UserName;
80
+ }
81
+
82
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
83
+ static extern bool CredReadW(string target, uint type, uint flags, out IntPtr cred);
84
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
85
+ static extern bool CredWriteW(ref CREDENTIAL cred, uint flags);
86
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
87
+ static extern bool CredDeleteW(string target, uint type, uint flags);
88
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
89
+ static extern bool CredEnumerateW(string filter, uint flags, out uint count, out IntPtr creds);
90
+ [DllImport("advapi32.dll", SetLastError = false)]
91
+ static extern void CredFree(IntPtr buffer);
92
+
93
+ public static bool Has(string target) {
94
+ IntPtr p;
95
+ if (CredReadW(target, CRED_TYPE_GENERIC, 0, out p)) { CredFree(p); return true; }
96
+ int err = Marshal.GetLastWin32Error();
97
+ if (err == ERROR_NOT_FOUND) return false;
98
+ throw new Exception("CredMan error " + err);
99
+ }
100
+
101
+ public static byte[] Get(string target) {
102
+ IntPtr p;
103
+ if (!CredReadW(target, CRED_TYPE_GENERIC, 0, out p)) {
104
+ int err = Marshal.GetLastWin32Error();
105
+ if (err == ERROR_NOT_FOUND) throw new Exception("NOTFOUND");
106
+ throw new Exception("CredMan error " + err);
107
+ }
108
+ try {
109
+ CREDENTIAL cred = (CREDENTIAL)Marshal.PtrToStructure(p, typeof(CREDENTIAL));
110
+ byte[] blob = new byte[cred.CredentialBlobSize];
111
+ if (cred.CredentialBlobSize > 0)
112
+ Marshal.Copy(cred.CredentialBlob, blob, 0, (int)cred.CredentialBlobSize);
113
+ return blob;
114
+ } finally { CredFree(p); }
115
+ }
116
+
117
+ public static void Set(string target, byte[] blob) {
118
+ CREDENTIAL cred = new CREDENTIAL();
119
+ cred.Type = CRED_TYPE_GENERIC;
120
+ cred.TargetName = Marshal.StringToCoTaskMemUni(target);
121
+ cred.CredentialBlobSize = (uint)blob.Length;
122
+ cred.CredentialBlob = (blob.Length > 0) ? Marshal.AllocCoTaskMem(blob.Length) : IntPtr.Zero;
123
+ if (blob.Length > 0) Marshal.Copy(blob, 0, cred.CredentialBlob, blob.Length);
124
+ cred.Persist = CRED_PERSIST_LOCAL_MACHINE;
125
+ cred.UserName = Marshal.StringToCoTaskMemUni(Environment.UserName);
126
+ try {
127
+ if (!CredWriteW(ref cred, 0)) {
128
+ int err = Marshal.GetLastWin32Error();
129
+ throw new Exception("CredMan error " + err);
130
+ }
131
+ } finally {
132
+ Marshal.FreeCoTaskMem(cred.TargetName);
133
+ if (cred.CredentialBlob != IntPtr.Zero) Marshal.FreeCoTaskMem(cred.CredentialBlob);
134
+ Marshal.FreeCoTaskMem(cred.UserName);
135
+ }
136
+ }
137
+
138
+ public static bool Delete(string target) {
139
+ if (CredDeleteW(target, CRED_TYPE_GENERIC, 0)) return true;
140
+ int err = Marshal.GetLastWin32Error();
141
+ if (err == ERROR_NOT_FOUND) return false;
142
+ throw new Exception("CredMan error " + err);
143
+ }
144
+
145
+ public static List<string> List(string filter) {
146
+ uint count;
147
+ IntPtr credsPtr;
148
+ List<string> results = new List<string>();
149
+ string f = string.IsNullOrEmpty(filter) ? null : filter;
150
+ if (!CredEnumerateW(f, 0, out count, out credsPtr)) {
151
+ int err = Marshal.GetLastWin32Error();
152
+ if (err == ERROR_NOT_FOUND) return results;
153
+ throw new Exception("CredMan error " + err);
154
+ }
155
+ try {
156
+ for (int i = 0; i < count; i++) {
157
+ IntPtr credPtr = Marshal.ReadIntPtr(credsPtr, i * IntPtr.Size);
158
+ CREDENTIAL cred = (CREDENTIAL)Marshal.PtrToStructure(credPtr, typeof(CREDENTIAL));
159
+ if (cred.TargetName != IntPtr.Zero)
160
+ results.Add(Marshal.PtrToStringUni(cred.TargetName));
161
+ }
162
+ } finally { CredFree(credsPtr); }
163
+ return results;
164
+ }
165
+ }
166
+ '@
167
+
168
+ try {
169
+ $op = $env:AGENTS_CRED_OP
170
+ $target = $env:AGENTS_CRED_TARGET
171
+ switch ($op) {
172
+ 'has' {
173
+ if ([AgentsCred]::Has($target)) { exit 0 } else { exit 3 }
174
+ }
175
+ 'get' {
176
+ $blob = [AgentsCred]::Get($target)
177
+ [Console]::Out.Write([Convert]::ToBase64String($blob))
178
+ exit 0
179
+ }
180
+ 'set' {
181
+ $value = [Console]::In.ReadToEnd()
182
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($value)
183
+ [AgentsCred]::Set($target, $bytes)
184
+ exit 0
185
+ }
186
+ 'delete' {
187
+ if ([AgentsCred]::Delete($target)) { exit 0 } else { exit 3 }
188
+ }
189
+ 'list' {
190
+ $prefix = $env:AGENTS_CRED_PREFIX
191
+ $filter = ''
192
+ if (-not [string]::IsNullOrEmpty($prefix)) { $filter = $prefix + '*' }
193
+ foreach ($n in [AgentsCred]::List($filter)) { [Console]::Out.WriteLine($n) }
194
+ exit 0
195
+ }
196
+ default {
197
+ [Console]::Error.Write('unknown op: ' + $op)
198
+ exit 1
199
+ }
200
+ }
201
+ } catch {
202
+ $m = $_.Exception.Message
203
+ if ($m -match 'NOTFOUND') { exit 3 }
204
+ [Console]::Error.Write($m)
205
+ exit 1
206
+ }
207
+ `;
208
+ const ENCODED_SCRIPT = encodePwshBase64(PS_SCRIPT);
209
+ function runCred(op, opts) {
210
+ const env = { ...process.env, AGENTS_CRED_OP: op };
211
+ if (opts.target !== undefined)
212
+ env.AGENTS_CRED_TARGET = opts.target;
213
+ if (opts.prefix !== undefined)
214
+ env.AGENTS_CRED_PREFIX = opts.prefix;
215
+ // ARGV ARRAY — never a shell string. -EncodedCommand rides base64 of the
216
+ // UTF-16LE script with zero escaping hazards.
217
+ const result = spawnSync(POWERSHELL, ['-NoProfile', '-NonInteractive', '-EncodedCommand', ENCODED_SCRIPT], {
218
+ env,
219
+ input: opts.input,
220
+ stdio: ['pipe', 'pipe', 'pipe'],
221
+ maxBuffer: 16 * 1024 * 1024,
222
+ });
223
+ return {
224
+ status: result.status,
225
+ stdout: result.stdout?.toString() ?? '',
226
+ stderr: result.stderr?.toString() ?? '',
227
+ spawnError: !!result.error,
228
+ };
229
+ }
230
+ // ---------- powershell availability ----------
231
+ function powershellAvailable() {
232
+ const result = spawnSync(POWERSHELL, ['-NoProfile', '-NonInteractive', '-Command', 'exit 0'], {
233
+ stdio: ['ignore', 'ignore', 'ignore'],
234
+ });
235
+ return !result.error && result.status === 0;
236
+ }
237
+ let checkedAvailability = false;
238
+ let isAvailable = false;
239
+ // ---------- file fallback state ----------
240
+ let useFileFallback = false;
241
+ let warnedFallback = false;
242
+ function activateFileFallback() {
243
+ if (useFileFallback)
244
+ return;
245
+ useFileFallback = true;
246
+ if (!warnedFallback) {
247
+ warnedFallback = true;
248
+ process.stderr.write(`[agents] Windows Credential Manager unavailable, using file-based store at ${fileDir()}\n`);
249
+ }
250
+ }
251
+ /**
252
+ * Credential Manager is "unavailable" (as opposed to a plain not-found or a
253
+ * malformed item) when there's no logon session to hold credentials
254
+ * (ERROR_NO_SUCH_LOGON_SESSION 1312 — common under a service account / SSH
255
+ * session with no interactive logon) or powershell.exe can't be spawned at all.
256
+ * Those cases route to the encrypted-file fallback, exactly like the Linux
257
+ * locked-collection error.
258
+ */
259
+ function isCredManUnavailableError(r) {
260
+ if (r.spawnError)
261
+ return true; // powershell.exe not found
262
+ return /\b1312\b/.test(r.stderr) || /NO_SUCH_LOGON_SESSION/i.test(r.stderr);
263
+ }
264
+ /**
265
+ * Decide which backend a given op should use. Activates the file fallback if a
266
+ * previous run already committed to it (encrypted items on disk), or if
267
+ * powershell.exe is missing and a passphrase source exists (explicit
268
+ * AGENTS_SECRETS_PASSPHRASE, a provisioned machine-local key, or a headless
269
+ * context). The disk-items check makes the fallback persistent across the many
270
+ * short-lived `agents secrets ...` Node processes.
271
+ */
272
+ function preflight() {
273
+ if (useFileFallback)
274
+ return 'file';
275
+ if (fileStoreHasItems()) {
276
+ activateFileFallback();
277
+ return 'file';
278
+ }
279
+ if (!checkedAvailability) {
280
+ isAvailable = powershellAvailable();
281
+ checkedAvailability = true;
282
+ }
283
+ if (!isAvailable) {
284
+ if (process.env.AGENTS_SECRETS_PASSPHRASE || machinePassphraseExists() || !process.stdin.isTTY) {
285
+ activateFileFallback();
286
+ return 'file';
287
+ }
288
+ throw new Error('powershell.exe not found on PATH; cannot reach Windows Credential Manager.\n' +
289
+ 'Set AGENTS_SECRETS_PASSPHRASE to use the encrypted-file fallback.');
290
+ }
291
+ return 'credman';
292
+ }
293
+ /**
294
+ * True when secret operations currently route to the encrypted-file store
295
+ * instead of Windows Credential Manager. Mirrors linux.ts:usesFileFallback so
296
+ * `listBundles()` doesn't double-count file-backed bundles under the fallback.
297
+ */
298
+ export function usesFileFallback() {
299
+ try {
300
+ return preflight() === 'file';
301
+ }
302
+ catch {
303
+ return false;
304
+ }
305
+ }
306
+ // ---------- Credential Manager ops with fallback ----------
307
+ export function hasCredManToken(item) {
308
+ if (preflight() === 'file')
309
+ return fileStore.has(item);
310
+ const r = runCred('has', { target: item });
311
+ if (r.status === 0)
312
+ return true;
313
+ if (r.status === 3)
314
+ return false;
315
+ if (isCredManUnavailableError(r)) {
316
+ activateFileFallback();
317
+ return fileStore.has(item);
318
+ }
319
+ return false;
320
+ }
321
+ export function getCredManToken(item) {
322
+ if (preflight() === 'file')
323
+ return fileStore.get(item);
324
+ const r = runCred('get', { target: item });
325
+ if (r.status === 0) {
326
+ // stdout is base64 of the raw UTF-8 blob (dodges PowerShell encoding corruption).
327
+ return Buffer.from(r.stdout.trim(), 'base64').toString('utf8');
328
+ }
329
+ if (r.status === 3)
330
+ throw new Error(`Secret '${item}' not found in Credential Manager.`);
331
+ if (isCredManUnavailableError(r)) {
332
+ activateFileFallback();
333
+ return fileStore.get(item);
334
+ }
335
+ throw new Error(`Failed to read secret '${item}': ${r.stderr.trim() || 'unknown error'}`);
336
+ }
337
+ export function setCredManToken(item, value) {
338
+ if (!value || !value.trim())
339
+ throw new Error('Secret value is empty.');
340
+ if (preflight() === 'file') {
341
+ fileStore.set(item, value);
342
+ return;
343
+ }
344
+ const byteLen = Buffer.byteLength(value, 'utf8');
345
+ if (byteLen > CRED_MAX_CREDENTIAL_BLOB_SIZE) {
346
+ throw new Error(`Secret '${item}' is ${byteLen} bytes, exceeding the Windows Credential Manager limit of ` +
347
+ `${CRED_MAX_CREDENTIAL_BLOB_SIZE} bytes (CRED_MAX_CREDENTIAL_BLOB_SIZE). ` +
348
+ 'Use a file-backed bundle (set AGENTS_SECRETS_PASSPHRASE) for values this large.');
349
+ }
350
+ const r = runCred('set', { target: item, input: value });
351
+ if (r.status === 0)
352
+ return;
353
+ if (isCredManUnavailableError(r)) {
354
+ activateFileFallback();
355
+ fileStore.set(item, value);
356
+ return;
357
+ }
358
+ throw new Error(`Failed to store secret '${item}': ${r.stderr.trim() || 'unknown error'}`);
359
+ }
360
+ export function deleteCredManToken(item) {
361
+ if (preflight() === 'file')
362
+ return fileStore.delete(item);
363
+ const r = runCred('delete', { target: item });
364
+ if (r.status === 0)
365
+ return true;
366
+ if (r.status === 3)
367
+ return false;
368
+ if (isCredManUnavailableError(r)) {
369
+ activateFileFallback();
370
+ return fileStore.delete(item);
371
+ }
372
+ return false;
373
+ }
374
+ export function listCredManItems(prefix) {
375
+ if (preflight() === 'file')
376
+ return fileStore.list(prefix);
377
+ const r = runCred('list', { prefix });
378
+ if (r.status === 0)
379
+ return parseWindowsCredList(r.stdout, prefix);
380
+ if (isCredManUnavailableError(r)) {
381
+ activateFileFallback();
382
+ return fileStore.list(prefix);
383
+ }
384
+ return [];
385
+ }
386
+ /**
387
+ * Parse the target names printed by the `list` op (one per line), keeping only
388
+ * those starting with `prefix` and deduping. Same contract as
389
+ * parseSecretToolItems (linux.ts). Exported for tests.
390
+ */
391
+ export function parseWindowsCredList(output, prefix) {
392
+ const items = output
393
+ .split(/\r?\n/)
394
+ .map((s) => s.trim())
395
+ .filter((s) => s.length > 0)
396
+ .filter((s) => s.startsWith(prefix));
397
+ return [...new Set(items)]; // dedupe
398
+ }
399
+ /**
400
+ * KeychainBackend implementation for Windows. Routes through Windows Credential
401
+ * Manager (via PowerShell P/Invoke) with a transparent encrypted-file fallback
402
+ * when the credential store is unreachable.
403
+ */
404
+ export const windowsBackend = {
405
+ has(item) {
406
+ return hasCredManToken(item);
407
+ },
408
+ get(item) {
409
+ return getCredManToken(item);
410
+ },
411
+ set(item, value) {
412
+ setCredManToken(item, value);
413
+ },
414
+ delete(item) {
415
+ return deleteCredManToken(item);
416
+ },
417
+ list(prefix) {
418
+ return listCredManItems(prefix);
419
+ },
420
+ };
421
+ /**
422
+ * Test-only: reset module state so independent test cases don't bleed
423
+ * availability / fallback decisions across each other. Pass `forceAvailable` to
424
+ * pin the powershell-availability probe (skips the real spawn); pass `fileDir`
425
+ * to redirect the encrypted-file store to a temp dir. File-store state lives in
426
+ * ./filestore.ts and is reset there.
427
+ */
428
+ export function _resetForTest(opts = {}) {
429
+ _resetFileStoreForTest({ fileDir: opts.fileDir ?? null, passphrase: opts.passphrase ?? null });
430
+ useFileFallback = opts.forceFileFallback ?? false;
431
+ warnedFallback = false;
432
+ if (opts.forceAvailable === undefined || opts.forceAvailable === null) {
433
+ checkedAvailability = false;
434
+ isAvailable = false;
435
+ }
436
+ else {
437
+ checkedAvailability = true;
438
+ isAvailable = opts.forceAvailable;
439
+ }
440
+ }
@@ -1,4 +1,5 @@
1
1
  import { type SessionActivity, type AwaitingReason, type DetectedPr, type DetectedWorktree, type DetectedTicket } from './state.js';
2
+ import { type SessionProvenance } from './provenance.js';
2
3
  export type ActiveContext = 'terminal' | 'teams' | 'cloud' | 'headless';
3
4
  export type ActiveStatus = 'running' | 'idle' | 'queued' | 'input_required';
4
5
  export interface ActiveSession {
@@ -30,6 +31,13 @@ export interface ActiveSession {
30
31
  status: ActiveStatus;
31
32
  /** How many live PIDs resolve to this same session (subagents/forks). 1 unless collapsed. */
32
33
  pidCount?: number;
34
+ /**
35
+ * Where the process actually lives — machine host, local vs SSH, tmux pane,
36
+ * and whether a rail exists to type back into it. Read from the process env
37
+ * (`/proc/<pid>/environ` on Linux, `ps eww` on macOS) during enrichment.
38
+ * Absent for cloud sessions (no local pid) and any pid whose env is unreadable.
39
+ */
40
+ provenance?: SessionProvenance;
33
41
  teamName?: string;
34
42
  agentId?: string;
35
43
  cloudProvider?: string;
@@ -29,6 +29,7 @@ import { latestSessionFileForCwd } from './db.js';
29
29
  import { extractSessionTopic } from './prompt.js';
30
30
  import { readSessionTail } from './tail.js';
31
31
  import { inferSessionState } from './state.js';
32
+ import { detectProvenance } from './provenance.js';
32
33
  const execFileAsync = promisify(execFile);
33
34
  const HOME = os.homedir();
34
35
  const LIVE_TERMINALS_FILE = path.join(getTerminalsDir(), 'live-terminals.json');
@@ -536,7 +537,23 @@ export async function getActiveSessions(opts = {}) {
536
537
  if (s.pid)
537
538
  knownPids.add(s.pid);
538
539
  const unattributed = opts.skipHeadless ? [] : await listUnattributedActive(knownPids);
539
- return dedupeBySession([...teams, ...terminals, ...cloud, ...unattributed]);
540
+ const merged = dedupeBySession([...teams, ...terminals, ...cloud, ...unattributed]);
541
+ await enrichProvenance(merged);
542
+ return merged;
543
+ }
544
+ /**
545
+ * Attach provenance (host / local-vs-SSH / tmux pane / reply rail) to every
546
+ * session that has a live pid. Mutates in place. Runs after dedupe so we probe
547
+ * each session once, not once per fork pid. Probes run in parallel — each is a
548
+ * single /proc read (Linux) or `ps` call (macOS); failures leave `provenance`
549
+ * undefined rather than blocking the listing.
550
+ */
551
+ async function enrichProvenance(sessions) {
552
+ await Promise.all(sessions.map(async (s) => {
553
+ if (s.provenance || !s.pid)
554
+ return;
555
+ s.provenance = await detectProvenance(s.pid);
556
+ }));
540
557
  }
541
558
  /**
542
559
  * Collapse rows that resolve to the *same* session — a session with many
@@ -0,0 +1,56 @@
1
+ export interface SshOrigin {
2
+ clientIp: string;
3
+ clientPort: number;
4
+ serverIp: string;
5
+ serverPort: number;
6
+ }
7
+ export interface MuxLocation {
8
+ kind: 'tmux' | 'screen';
9
+ /** tmux server socket path (first comma-field of $TMUX). Undefined for screen. */
10
+ socket?: string;
11
+ /** Exact pane id from $TMUX_PANE, e.g. '%3' — the send-keys target. */
12
+ pane?: string;
13
+ /** screen session name from $STY, e.g. '12345.pts-0.host'. */
14
+ session?: string;
15
+ }
16
+ /** How the feed can type back into a session, derived from rails that exist today. */
17
+ export type ReplyRail = {
18
+ rail: 'tmux';
19
+ target: string;
20
+ socket?: string;
21
+ } | null;
22
+ export interface SessionProvenance {
23
+ /** Machine the process runs on — os.hostname(). Drives HOSTS grouping. */
24
+ host: string;
25
+ /** 'ssh' when SSH_CONNECTION is present in the process env, else 'local'. */
26
+ transport: 'local' | 'ssh';
27
+ /** Populated when transport === 'ssh'. */
28
+ ssh?: SshOrigin;
29
+ /** TERM_PROGRAM: 'iTerm.app', 'vscode', 'WezTerm', 'tmux', 'Apple_Terminal', … */
30
+ term?: string;
31
+ /** Multiplexer the process sits inside, from $TMUX / $STY. */
32
+ mux?: MuxLocation;
33
+ /** Whether an existing rail can type back into this session (see module doc). */
34
+ reply: ReplyRail;
35
+ }
36
+ /** Env vars that carry provenance. Kept small so the macOS `ps` scan stays cheap. */
37
+ export declare const PROVENANCE_ENV_KEYS: readonly ["SSH_CONNECTION", "SSH_TTY", "TMUX", "TMUX_PANE", "TERM_PROGRAM", "STY"];
38
+ /** Parse the NUL-separated body of /proc/<pid>/environ into a plain object. */
39
+ export declare function parseProcEnviron(buf: string): Record<string, string>;
40
+ /**
41
+ * Pull known env vars out of a macOS `ps eww` command+env line. For each
42
+ * `KEY=` match we consume the declared number of tokens (default 1), so
43
+ * SSH_CONNECTION's internal spaces survive while a following unknown var
44
+ * (e.g. `PWD=…`) is not swallowed into the previous value.
45
+ */
46
+ export declare function extractKnownEnv(text: string, keys: readonly string[]): Record<string, string>;
47
+ /** `<client_ip> <client_port> <server_ip> <server_port>` → structured origin. */
48
+ export declare function parseSshConnection(value: string): SshOrigin | undefined;
49
+ /** Build a SessionProvenance from a raw env map + the local hostname. Pure. */
50
+ export declare function deriveProvenance(env: Record<string, string>, hostname: string): SessionProvenance;
51
+ /**
52
+ * Resolve provenance for a live pid. Returns undefined when the process env
53
+ * can't be read (process gone, foreign uid, unsupported platform) — we never
54
+ * fabricate a 'local' answer we can't back with the env.
55
+ */
56
+ export declare function detectProvenance(pid: number): Promise<SessionProvenance | undefined>;