@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.
- package/CHANGELOG.md +7 -0
- package/README.md +28 -2
- package/dist/commands/commands.js +3 -3
- package/dist/commands/computer-actions.js +1 -0
- package/dist/commands/computer.d.ts +23 -0
- package/dist/commands/computer.js +45 -3
- package/dist/commands/cost.js +2 -2
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.js +51 -2
- package/dist/commands/exec.js +56 -1
- package/dist/commands/hooks.js +3 -3
- package/dist/commands/import.js +1 -1
- package/dist/commands/inspect.js +13 -17
- package/dist/commands/mcp.js +3 -3
- package/dist/commands/permissions.js +3 -3
- package/dist/commands/rules.js +3 -3
- package/dist/commands/secrets-migrate.js +23 -11
- package/dist/commands/secrets.d.ts +20 -0
- package/dist/commands/secrets.js +53 -1
- package/dist/commands/sessions.js +18 -1
- package/dist/commands/skills.js +3 -3
- package/dist/commands/status.d.ts +12 -0
- package/dist/commands/status.js +81 -0
- package/dist/commands/sync.js +2 -2
- package/dist/commands/teams.js +77 -18
- package/dist/commands/usage.js +2 -2
- package/dist/commands/utils.d.ts +8 -0
- package/dist/commands/utils.js +20 -0
- package/dist/commands/versions.js +4 -3
- package/dist/commands/view.d.ts +39 -0
- package/dist/commands/view.js +217 -84
- package/dist/commands/workflows.js +3 -3
- package/dist/index.js +16 -2
- package/dist/lib/acp/harnesses.d.ts +1 -1
- package/dist/lib/acp/harnesses.js +2 -2
- package/dist/lib/agent-spec/index.d.ts +18 -0
- package/dist/lib/agent-spec/index.js +35 -0
- package/dist/lib/agent-spec/primitives.d.ts +28 -0
- package/dist/lib/agent-spec/primitives.js +57 -0
- package/dist/lib/agent-spec/provider.d.ts +2 -0
- package/dist/lib/agent-spec/provider.js +9 -0
- package/dist/lib/agent-spec/resolve.d.ts +33 -0
- package/dist/lib/agent-spec/resolve.js +174 -0
- package/dist/lib/agent-spec/types.d.ts +57 -0
- package/dist/lib/agent-spec/types.js +18 -0
- package/dist/lib/agents.d.ts +12 -0
- package/dist/lib/agents.js +115 -32
- package/dist/lib/browser/chrome.js +20 -0
- package/dist/lib/browser/drivers/ssh.d.ts +19 -0
- package/dist/lib/browser/drivers/ssh.js +18 -3
- package/dist/lib/crabbox/cli.d.ts +98 -0
- package/dist/lib/crabbox/cli.js +218 -0
- package/dist/lib/crabbox/lease.d.ts +41 -0
- package/dist/lib/crabbox/lease.js +73 -0
- package/dist/lib/crabbox/runtimes.d.ts +57 -0
- package/dist/lib/crabbox/runtimes.js +109 -0
- package/dist/lib/doctor-diff.js +29 -2
- package/dist/lib/drift-sync.d.ts +43 -0
- package/dist/lib/drift-sync.js +179 -0
- package/dist/lib/exec.d.ts +15 -0
- package/dist/lib/exec.js +21 -11
- package/dist/lib/hosts/dispatch.d.ts +27 -10
- package/dist/lib/hosts/dispatch.js +55 -19
- package/dist/lib/hosts/option.d.ts +14 -0
- package/dist/lib/hosts/option.js +19 -0
- package/dist/lib/hosts/passthrough.d.ts +30 -0
- package/dist/lib/hosts/passthrough.js +141 -0
- package/dist/lib/hosts/remote-cmd.d.ts +36 -0
- package/dist/lib/hosts/remote-cmd.js +56 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/platform/winpath.d.ts +31 -2
- package/dist/lib/platform/winpath.js +133 -24
- package/dist/lib/pwsh.d.ts +11 -0
- package/dist/lib/pwsh.js +13 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +42 -1
- package/dist/lib/secrets/agent.js +89 -11
- package/dist/lib/secrets/bundles.js +61 -21
- package/dist/lib/secrets/filestore.js +31 -1
- package/dist/lib/secrets/index.d.ts +44 -1
- package/dist/lib/secrets/index.js +108 -10
- package/dist/lib/secrets/linux.d.ts +14 -0
- package/dist/lib/secrets/linux.js +21 -0
- package/dist/lib/secrets/windows.d.ts +74 -0
- package/dist/lib/secrets/windows.js +440 -0
- package/dist/lib/session/active.d.ts +8 -0
- package/dist/lib/session/active.js +18 -1
- package/dist/lib/session/provenance.d.ts +56 -0
- package/dist/lib/session/provenance.js +157 -0
- package/dist/lib/shims.d.ts +20 -0
- package/dist/lib/shims.js +53 -20
- package/dist/lib/ssh-exec.d.ts +22 -0
- package/dist/lib/ssh-exec.js +59 -2
- package/dist/lib/ssh-tunnel.d.ts +0 -5
- package/dist/lib/ssh-tunnel.js +65 -8
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/sync-status.d.ts +102 -0
- package/dist/lib/sync-status.js +135 -0
- package/dist/lib/teams/agents.d.ts +24 -0
- package/dist/lib/teams/agents.js +30 -1
- package/dist/lib/types.d.ts +20 -1
- package/dist/lib/usage.d.ts +30 -0
- package/dist/lib/usage.js +159 -2
- package/dist/lib/versions.d.ts +2 -4
- package/dist/lib/versions.js +7 -20
- package/package.json +2 -1
- package/dist/lib/agent-spec.d.ts +0 -36
- 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
|
-
|
|
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>;
|