@phnx-labs/agents-cli 1.20.87 → 1.20.89
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 +323 -0
- package/README.md +12 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/commands.js +7 -7
- package/dist/commands/doctor.d.ts +0 -19
- package/dist/commands/doctor.js +219 -305
- package/dist/commands/exec.js +7 -19
- package/dist/commands/factory.js +26 -2
- package/dist/commands/funnel.js +16 -1
- package/dist/commands/inspect.js +3 -5
- package/dist/commands/menubar.js +117 -34
- package/dist/commands/routines.js +25 -3
- package/dist/commands/secrets-rotate-passphrase.d.ts +17 -0
- package/dist/commands/secrets-rotate-passphrase.js +96 -0
- package/dist/commands/secrets.js +2 -0
- package/dist/commands/sessions.d.ts +7 -1
- package/dist/commands/sessions.js +40 -12
- package/dist/commands/ssh.js +3 -3
- package/dist/commands/usage.d.ts +3 -2
- package/dist/commands/usage.js +2 -9
- package/dist/commands/webhook.js +7 -2
- package/dist/lib/agents.d.ts +31 -1
- package/dist/lib/agents.js +55 -0
- package/dist/lib/command-skills.d.ts +10 -0
- package/dist/lib/command-skills.js +14 -0
- package/dist/lib/commands.js +28 -2
- package/dist/lib/daemon.d.ts +29 -0
- package/dist/lib/daemon.js +75 -6
- package/dist/lib/devices/doctor-findings.d.ts +167 -0
- package/dist/lib/devices/doctor-findings.js +893 -0
- package/dist/lib/devices/fleet-divergence.d.ts +22 -0
- package/dist/lib/devices/fleet-divergence.js +34 -10
- package/dist/lib/devices/fleet-inventory.d.ts +17 -6
- package/dist/lib/devices/fleet-inventory.js +56 -8
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +14 -3
- package/dist/lib/exec.js +41 -8
- package/dist/lib/factory/snapshot.d.ts +78 -0
- package/dist/lib/factory/snapshot.js +209 -0
- package/dist/lib/fs-atomic.d.ts +14 -1
- package/dist/lib/fs-atomic.js +35 -3
- package/dist/lib/funnel.d.ts +1 -0
- package/dist/lib/funnel.js +8 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -2
- package/dist/lib/menubar/install-menubar.d.ts +53 -2
- package/dist/lib/menubar/install-menubar.js +183 -28
- package/dist/lib/platform/process.d.ts +2 -0
- package/dist/lib/platform/process.js +5 -3
- package/dist/lib/project-resources.js +34 -20
- package/dist/lib/resources.d.ts +8 -0
- package/dist/lib/resources.js +34 -1
- package/dist/lib/routines-placement.d.ts +2 -1
- package/dist/lib/routines-placement.js +8 -4
- package/dist/lib/routines.d.ts +57 -1
- package/dist/lib/routines.js +74 -1
- package/dist/lib/runner.d.ts +16 -1
- package/dist/lib/runner.js +58 -16
- package/dist/lib/sandbox.d.ts +2 -0
- package/dist/lib/sandbox.js +38 -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/bundles.js +9 -34
- package/dist/lib/secrets/filestore.d.ts +152 -34
- package/dist/lib/secrets/filestore.js +676 -123
- package/dist/lib/secrets/rc-hygiene.d.ts +0 -6
- package/dist/lib/secrets/rc-hygiene.js +0 -24
- package/dist/lib/session/active.d.ts +6 -6
- package/dist/lib/session/active.js +6 -6
- package/dist/lib/session/discover.d.ts +5 -0
- package/dist/lib/session/discover.js +137 -1
- package/dist/lib/session/parse.d.ts +2 -0
- package/dist/lib/session/parse.js +76 -37
- package/dist/lib/session/remote-active.d.ts +4 -1
- package/dist/lib/session/remote-active.js +8 -2
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/viewing-in.d.ts +31 -0
- package/dist/lib/session/viewing-in.js +47 -0
- package/dist/lib/staleness/detectors/commands.js +14 -5
- package/dist/lib/staleness/types.d.ts +2 -0
- package/dist/lib/staleness/writers/commands.js +13 -7
- package/dist/lib/state.d.ts +17 -0
- package/dist/lib/state.js +30 -2
- package/dist/lib/triggers/handlers.d.ts +95 -0
- package/dist/lib/triggers/handlers.js +384 -0
- package/dist/lib/triggers/webhook.d.ts +10 -2
- package/dist/lib/triggers/webhook.js +65 -11
- package/dist/lib/usage.d.ts +72 -1
- package/dist/lib/usage.js +21 -27
- package/dist/lib/versions.js +30 -13
- package/package.json +1 -1
|
@@ -3,18 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* An AES-256-GCM encrypted-file store under `~/.agents/.cache/secrets/`. The
|
|
5
5
|
* encryption key is scrypt-derived from a passphrase read from
|
|
6
|
-
* `AGENTS_SECRETS_PASSPHRASE` (preferred)
|
|
7
|
-
*
|
|
6
|
+
* `AGENTS_SECRETS_PASSPHRASE` (preferred) or a machine-local key the store
|
|
7
|
+
* auto-provisions on first use. One `<item>.enc` JSON file per item, mode 0600.
|
|
8
8
|
*
|
|
9
|
-
* Two callers:
|
|
9
|
+
* Two callers, one policy: the store silently auto-provisions a stable
|
|
10
|
+
* machine-local key (a 0600 file under `~/.agents/.secrets-key/`) on EVERY
|
|
11
|
+
* platform, so it works out of the box with no passphrase to set or remember and
|
|
12
|
+
* never pops a prompt or Touch ID sheet.
|
|
10
13
|
* - Linux (src/lib/secrets/linux.ts): the headless fallback when the default
|
|
11
|
-
* Secret Service collection is locked.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* AGENTS_SECRETS_PASSPHRASE (or TTY) — never the silent machine-local
|
|
17
|
-
* auto-provision — so a remote box holds ciphertext only.
|
|
14
|
+
* Secret Service collection is locked.
|
|
15
|
+
* - macOS/Windows file-backed bundles (src/lib/secrets/bundles.ts): an explicit,
|
|
16
|
+
* opt-in non-biometry backend for headless/remote runs.
|
|
17
|
+
* Set AGENTS_SECRETS_PASSPHRASE to opt into a key held off disk instead (e.g. to
|
|
18
|
+
* share one bundle's ciphertext across boxes under a common key).
|
|
18
19
|
*
|
|
19
20
|
* The item-name scheme is shared with the keychain backend so a file-backed
|
|
20
21
|
* item and its keychain twin carry identical names:
|
|
@@ -22,31 +23,19 @@
|
|
|
22
23
|
*/
|
|
23
24
|
import type { KeychainBackend } from './index.js';
|
|
24
25
|
export declare function fileDir(): string;
|
|
25
|
-
/**
|
|
26
|
-
* Turn off terminal echo on the controlling TTY, or throw — fail CLOSED. If echo
|
|
27
|
-
* cannot be disabled (`stty` missing, no controlling terminal) we must NOT fall
|
|
28
|
-
* through and read the passphrase anyway: that echoes the secret to the screen
|
|
29
|
-
* and into scrollback (RUSH-1764). Refuse and point the user at the environment
|
|
30
|
-
* variable instead. `run` performs the echo-disable and throws iff it fails.
|
|
31
|
-
* Exported so the fail-closed contract has direct test coverage.
|
|
32
|
-
*/
|
|
33
|
-
export declare function disableTtyEchoOrThrow(run: () => void): void;
|
|
34
26
|
/** True if a machine-local passphrase has already been provisioned. */
|
|
35
27
|
export declare function machinePassphraseExists(): boolean;
|
|
36
28
|
/**
|
|
37
29
|
* Resolve the passphrase for the encrypted file store.
|
|
38
30
|
*
|
|
39
31
|
* Order: AGENTS_SECRETS_PASSPHRASE > previously-provisioned machine-local key >
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* on-disk key — the caller (bundles.ts) guards this before we get here.
|
|
32
|
+
* a freshly auto-provisioned machine-local key. It NEVER prompts and NEVER
|
|
33
|
+
* hard-fails — the file store must work on every platform (macOS included)
|
|
34
|
+
* without the user setting, typing, or remembering a passphrase. Provisioning
|
|
35
|
+
* writes a 0600 key file (encryption-at-rest, same posture as an SSH key); set
|
|
36
|
+
* AGENTS_SECRETS_PASSPHRASE to opt into an off-disk key.
|
|
46
37
|
*/
|
|
47
|
-
export declare function getPassphrase(
|
|
48
|
-
allowAutoProvision?: boolean;
|
|
49
|
-
}): string;
|
|
38
|
+
export declare function getPassphrase(): string;
|
|
50
39
|
/** Encrypted-file on-disk shape. Exported for tests. */
|
|
51
40
|
export interface EncFile {
|
|
52
41
|
salt: string;
|
|
@@ -61,12 +50,8 @@ export declare function encryptForFallback(plaintext: string, passphrase: string
|
|
|
61
50
|
* ciphertext (auth-tag mismatch). Exported for tests. */
|
|
62
51
|
export declare function decryptForFallback(enc: EncFile, passphrase: string): string;
|
|
63
52
|
declare function fileHas(item: string): boolean;
|
|
64
|
-
declare function fileGet(item: string
|
|
65
|
-
|
|
66
|
-
}): string;
|
|
67
|
-
declare function fileSet(item: string, value: string, opts?: {
|
|
68
|
-
allowAutoProvision?: boolean;
|
|
69
|
-
}): void;
|
|
53
|
+
declare function fileGet(item: string): string;
|
|
54
|
+
declare function fileSet(item: string, value: string): void;
|
|
70
55
|
declare function fileDelete(item: string): boolean;
|
|
71
56
|
declare function fileList(prefix: string): string[];
|
|
72
57
|
/** True if the fallback dir has any committed encrypted items. */
|
|
@@ -85,10 +70,143 @@ export declare const fileStore: {
|
|
|
85
70
|
export declare const fileBackend: KeychainBackend;
|
|
86
71
|
/** Resolved passphrase directory (exported for integration tests). */
|
|
87
72
|
export declare function resolvePassphraseDir(): string;
|
|
73
|
+
/**
|
|
74
|
+
* Path of the machine-local passphrase file that currently holds the file-store
|
|
75
|
+
* key, or null if none is provisioned. Prefers the canonical #479 location and
|
|
76
|
+
* falls back to the legacy co-located path, mirroring `readMachinePassphrase`.
|
|
77
|
+
*/
|
|
78
|
+
export declare function machinePassphraseSourcePath(): string | null;
|
|
79
|
+
/** Outcome of a `rotatePassphrase` run. Carries no secret material. */
|
|
80
|
+
export interface RotatePassphraseReport {
|
|
81
|
+
/** True when nothing was written (report-only). */
|
|
82
|
+
dryRun: boolean;
|
|
83
|
+
/** True when the store was re-encrypted and the key file swapped. */
|
|
84
|
+
committed: boolean;
|
|
85
|
+
/** Encrypted items that decrypt under the current key and were (or would be) re-keyed. */
|
|
86
|
+
bundleCount: number;
|
|
87
|
+
/** `.enc` files that do NOT decrypt under the current key — left untouched, never re-keyed. */
|
|
88
|
+
skipped: string[];
|
|
89
|
+
/** Every re-keyed item round-tripped (decrypt) under the new key before any swap. */
|
|
90
|
+
roundTripOk: boolean;
|
|
91
|
+
/** The machine-local passphrase file that was (or would be) rewritten in place. */
|
|
92
|
+
keyFilePath: string;
|
|
93
|
+
/**
|
|
94
|
+
* A previous rotation had left artifacts on disk and they were healed before
|
|
95
|
+
* this rotation proceeded. This happens under `--dry-run` too — recovery is how
|
|
96
|
+
* a crashed store becomes readable again without re-keying it — and is the only
|
|
97
|
+
* thing a dry run writes.
|
|
98
|
+
*/
|
|
99
|
+
recoveredInterruptedRotation: boolean;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Recover from a rotation that was interrupted mid-swap on a prior run, so the
|
|
103
|
+
* store is always left in a single, self-consistent, readable state.
|
|
104
|
+
*
|
|
105
|
+
* Recovery is CONTENT-aware, not presence-aware, and it classifies the WHOLE
|
|
106
|
+
* store, not just one item. The mere existence of the store dir and the key file
|
|
107
|
+
* does not prove they match (RUSH-1975 data-loss window): on the non-co-located
|
|
108
|
+
* key path the swap is four renames, and a crash after the store swap
|
|
109
|
+
* (`stageDir`->`dir`) but before the key swap (`keyTmp`->`keyPath`) finishes
|
|
110
|
+
* leaves a NEW-key store next to the OLD key file, both present. A presence check
|
|
111
|
+
* would see "both here" and wrongly sweep the only copies of the old ciphertext
|
|
112
|
+
* (`<dir>.rotate-old-*`) and the new key (`<key>.rotate-new`), permanently
|
|
113
|
+
* orphaning every secret. So we probe the actual ciphertext with `classifyStore`,
|
|
114
|
+
* which distinguishes a store that opens fully under one key ('all') from one that
|
|
115
|
+
* is MIXED — some items under the live key, others under the incoming key ('some',
|
|
116
|
+
* e.g. after a mid-swap crash contaminated by a later `secrets set`):
|
|
117
|
+
*
|
|
118
|
+
* 1. The live key opens EVERY non-orphan item ('all') → rotation complete and
|
|
119
|
+
* consistent (or never interrupted); sweeping the `.rotate-*` artifacts is safe —
|
|
120
|
+
* unless a `<dir>.rotate-old-*` backup still holds `.enc` items absent from the
|
|
121
|
+
* live dir. That means the live dir is not the post-swap store but a fresh dir an
|
|
122
|
+
* interstitial `secrets set` created after a crash in the move-aside window left
|
|
123
|
+
* the store dir absent, so the backup is the only copy of those items → REFUSE.
|
|
124
|
+
* 2. Else, if `<key>.rotate-new` opens every non-orphan item ('all'), the crash
|
|
125
|
+
* landed after the store swap but before the key swap finished → finish the
|
|
126
|
+
* rotation forward by installing `.rotate-new` as the live key, then sweep.
|
|
127
|
+
* 3. Else, if neither key opens any item, roll back: restore the
|
|
128
|
+
* `<dir>.rotate-old-*` backup over `dir` and `<key>.rotate-oldkey` over the key
|
|
129
|
+
* file — but only once the backup is proven to open fully under the old key.
|
|
130
|
+
* 4. If a key opens SOME but not all items ('some'), the store is MIXED — an
|
|
131
|
+
* interrupted rotation contaminated by a later write, with live data under two
|
|
132
|
+
* keys at once. Sweeping would delete the only copy of one of those keys, so we
|
|
133
|
+
* REFUSE: throw an actionable error and preserve every recovery artifact for
|
|
134
|
+
* out-of-band repair. Likewise, if neither forward nor rollback can be proven,
|
|
135
|
+
* leave every artifact in place — a leftover temp dir is recoverable, deleting
|
|
136
|
+
* the only copy of a key or ciphertext is not.
|
|
137
|
+
*
|
|
138
|
+
* A phase-marker / journal file was considered and deliberately skipped: the
|
|
139
|
+
* AES-256-GCM auth tag already makes the decrypt probe an authoritative,
|
|
140
|
+
* self-validating record of which key matches the store. A separate marker would
|
|
141
|
+
* be a second source of truth that can disagree with reality — its own write has
|
|
142
|
+
* crash windows, and a stale marker misleads — so it would weaken, not strengthen,
|
|
143
|
+
* this guarantee. Idempotent; a no-op when no rotation artifacts are present.
|
|
144
|
+
* Callers run this under the store lock (see `withStoreLock`).
|
|
145
|
+
*/
|
|
146
|
+
/**
|
|
147
|
+
* Whether a previous rotation left artifacts on disk — i.e. whether
|
|
148
|
+
* {@link recoverInterruptedRotation} would do any work. Read-only, so `--dry-run`
|
|
149
|
+
* can report a pending recovery without performing (and thus writing) one.
|
|
150
|
+
*/
|
|
151
|
+
export declare function hasInterruptedRotationArtifacts(keyPath: string): boolean;
|
|
152
|
+
/**
|
|
153
|
+
* Rotate the machine-local file-store passphrase: decrypt every `.enc` item
|
|
154
|
+
* under the current key and re-encrypt it under a freshly generated one, then
|
|
155
|
+
* swap both the ciphertext and the key file atomically.
|
|
156
|
+
*
|
|
157
|
+
* Safety contract (RUSH-1975):
|
|
158
|
+
* - Verify before writing: every re-keyed item must round-trip decrypt under
|
|
159
|
+
* the new key, and the re-keyed count must reconcile with the source, or the
|
|
160
|
+
* run aborts having written nothing.
|
|
161
|
+
* - Atomic: the new store is staged in a sibling temp dir, fsync'd, then swapped
|
|
162
|
+
* into place by directory rename; the new key file is fsync'd and swapped the
|
|
163
|
+
* same way. A crash before the swap leaves the old store and old key fully
|
|
164
|
+
* intact and readable; a crash inside the swap self-heals on the next run
|
|
165
|
+
* (see `recoverInterruptedRotation`). No half-re-keyed store is ever exposed.
|
|
166
|
+
* - No plaintext (secret value or passphrase) is ever written to disk, argv, or
|
|
167
|
+
* a log — only ciphertext is staged, and the new key lands only in the 0600
|
|
168
|
+
* key file.
|
|
169
|
+
* - Items that do not decrypt under the current key (orphan caches, stale test
|
|
170
|
+
* artifacts written under another key) are copied through verbatim, never
|
|
171
|
+
* re-keyed, and reported in `skipped`.
|
|
172
|
+
*
|
|
173
|
+
* `newPassphrase` and the `on*` callbacks are test seams. `newPassphrase` pins the
|
|
174
|
+
* generated key so a test can assert the swap. `onStagedBeforeCommit` fires after
|
|
175
|
+
* staging but before any swap (a crash here leaves the old store fully intact).
|
|
176
|
+
* `onStoreSwappedBeforeKeySwap` fires after the store swap but before the key swap
|
|
177
|
+
* begins (Window A: NEW-key store beside the OLD key file). `onKeyBackedUpBeforeNewKey`
|
|
178
|
+
* fires after the old key is moved aside but before the new key lands (Window B: NEW
|
|
179
|
+
* store, key file absent). Each throws to simulate a mid-swap crash at that exact
|
|
180
|
+
* point, and next-run recovery must heal it without data loss. `tamperStaged` forces
|
|
181
|
+
* a staged item to fail its round-trip check, exercising the verify-before-swap abort.
|
|
182
|
+
*/
|
|
183
|
+
export interface RotatePassphraseOpts {
|
|
184
|
+
dryRun?: boolean;
|
|
185
|
+
newPassphrase?: string;
|
|
186
|
+
onStagedBeforeCommit?: () => void;
|
|
187
|
+
onStoreMovedAsideBeforeSwap?: () => void;
|
|
188
|
+
onStoreSwappedBeforeKeySwap?: (heartbeat: () => void) => void;
|
|
189
|
+
onKeyBackedUpBeforeNewKey?: () => void;
|
|
190
|
+
tamperStaged?: boolean;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Rotate under the exclusive store lock, so no `secrets set`/`delete` and no
|
|
194
|
+
* second rotation can interleave with the swap (see `withStoreLock`). The whole
|
|
195
|
+
* run — recovery, verify, swap — holds the lock; it is released on return or throw.
|
|
196
|
+
*/
|
|
197
|
+
export declare function rotatePassphrase(opts?: RotatePassphraseOpts): RotatePassphraseReport;
|
|
88
198
|
/** Test-only: reset module state (file dir + cached passphrase). */
|
|
89
199
|
export declare function _resetFileStoreForTest(opts?: {
|
|
90
200
|
fileDir?: string | null;
|
|
91
201
|
passphraseDir?: string | null;
|
|
92
202
|
passphrase?: string | null;
|
|
93
203
|
}): void;
|
|
204
|
+
/** Test-only: shorten the store-lock acquire timeout so a contended-lock assertion
|
|
205
|
+
* fails fast instead of waiting out the 30s production budget. */
|
|
206
|
+
export declare function _setFileStoreLockTimeoutForTest(ms: number | null): void;
|
|
207
|
+
/** Test-only: shrink the store-lock stale window so a heartbeat/steal assertion runs
|
|
208
|
+
* in milliseconds instead of the 5s production window. */
|
|
209
|
+
export declare function _setFileStoreLockStaleMsForTest(ms: number | null): void;
|
|
210
|
+
/** Test-only: the cross-process store-lock target (sibling of the store dir). */
|
|
211
|
+
export declare function _fileStoreLockPathForTest(): string;
|
|
94
212
|
export {};
|