@phnx-labs/agents-cli 1.20.78 → 1.20.82
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 +487 -0
- package/README.md +25 -25
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +9 -0
- package/dist/commands/activity.js +153 -24
- package/dist/commands/apply.js +11 -4
- package/dist/commands/cli.js +1 -1
- package/dist/commands/commands.d.ts +1 -1
- package/dist/commands/commands.js +7 -6
- package/dist/commands/defaults.js +3 -11
- package/dist/commands/doctor.d.ts +65 -0
- package/dist/commands/doctor.js +575 -31
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.js +24 -5
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +87 -11
- package/dist/commands/harness.js +2 -2
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/import.js +8 -4
- package/dist/commands/logs.js +6 -0
- package/dist/commands/models.js +1 -1
- package/dist/commands/packages.js +6 -6
- package/dist/commands/permissions.js +1 -1
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/pull.d.ts +1 -1
- package/dist/commands/pull.js +4 -4
- package/dist/commands/repo.d.ts +78 -0
- package/dist/commands/repo.js +187 -11
- package/dist/commands/routines.js +12 -2
- package/dist/commands/rules.js +13 -12
- package/dist/commands/secrets.d.ts +3 -1
- package/dist/commands/secrets.js +100 -32
- package/dist/commands/sessions-inject.d.ts +7 -4
- package/dist/commands/sessions-inject.js +18 -18
- package/dist/commands/sessions-picker.js +32 -3
- package/dist/commands/sessions-resume.d.ts +16 -0
- package/dist/commands/sessions-resume.js +14 -13
- package/dist/commands/sessions.d.ts +31 -0
- package/dist/commands/sessions.js +141 -16
- package/dist/commands/set.d.ts +15 -0
- package/dist/commands/set.js +79 -0
- package/dist/commands/setup-fleet.d.ts +21 -0
- package/dist/commands/setup-fleet.js +201 -0
- package/dist/commands/setup-secrets.d.ts +20 -0
- package/dist/commands/setup-secrets.js +223 -0
- package/dist/commands/setup.js +17 -3
- package/dist/commands/skills.js +1 -1
- package/dist/commands/sync.js +7 -2
- package/dist/commands/teams-picker.js +0 -1
- package/dist/commands/teams.d.ts +33 -1
- package/dist/commands/teams.js +208 -44
- package/dist/commands/versions.js +10 -5
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.js +11 -0
- package/dist/index.js +5 -8
- package/dist/lib/acp/harnesses.js +0 -7
- package/dist/lib/activity.d.ts +156 -0
- package/dist/lib/activity.js +282 -0
- package/dist/lib/agents.d.ts +9 -2
- package/dist/lib/agents.js +72 -63
- package/dist/lib/browser/service.js +3 -0
- package/dist/lib/browser/types.d.ts +7 -0
- package/dist/lib/capabilities.js +6 -2
- package/dist/lib/crabbox/lease.js +2 -2
- package/dist/lib/crabbox/setup-copy.d.ts +4 -4
- package/dist/lib/crabbox/setup-copy.js +4 -4
- package/dist/lib/daemon.d.ts +18 -0
- package/dist/lib/daemon.js +47 -6
- package/dist/lib/devices/fleet.d.ts +1 -1
- package/dist/lib/devices/fleet.js +1 -1
- package/dist/lib/doctor-diff.d.ts +20 -0
- package/dist/lib/doctor-diff.js +6 -1
- package/dist/lib/drift.d.ts +31 -11
- package/dist/lib/drift.js +58 -7
- package/dist/lib/events.d.ts +8 -1
- package/dist/lib/events.js +10 -1
- package/dist/lib/exec.js +35 -8
- package/dist/lib/feed-post.d.ts +28 -1
- package/dist/lib/feed-post.js +110 -2
- package/dist/lib/fleet/apply.js +8 -1
- package/dist/lib/fleet/auth-sync.d.ts +18 -1
- package/dist/lib/fleet/auth-sync.js +25 -11
- package/dist/lib/fleet/remote-login.js +5 -0
- package/dist/lib/git.d.ts +14 -14
- package/dist/lib/git.js +41 -44
- package/dist/lib/hooks.d.ts +42 -0
- package/dist/lib/hooks.js +137 -93
- package/dist/lib/hosts/dispatch.d.ts +28 -1
- package/dist/lib/hosts/dispatch.js +33 -5
- package/dist/lib/hosts/option.js +2 -2
- package/dist/lib/hosts/passthrough.d.ts +22 -1
- package/dist/lib/hosts/passthrough.js +238 -13
- package/dist/lib/hosts/run-target.d.ts +2 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/hq/floor.d.ts +1 -1
- package/dist/lib/hq/floor.js +6 -1
- package/dist/lib/mcp.js +3 -71
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/notify-desktop.d.ts +15 -0
- package/dist/lib/menubar/notify-desktop.js +41 -5
- package/dist/lib/merged-resources.d.ts +11 -0
- package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
- package/dist/lib/permissions.d.ts +0 -32
- package/dist/lib/permissions.js +4 -157
- package/dist/lib/picker.js +1 -1
- package/dist/lib/platform/process.d.ts +16 -5
- package/dist/lib/platform/process.js +54 -0
- package/dist/lib/plugins.d.ts +0 -8
- package/dist/lib/plugins.js +4 -110
- package/dist/lib/project-resources.js +4 -1
- package/dist/lib/refresh.d.ts +5 -3
- package/dist/lib/refresh.js +7 -5
- package/dist/lib/resources/commands.js +4 -1
- package/dist/lib/resources/mcp.js +0 -4
- package/dist/lib/resources/permissions.d.ts +1 -1
- package/dist/lib/resources/permissions.js +1 -5
- package/dist/lib/resources/rules.js +4 -1
- package/dist/lib/resources/skills.js +4 -1
- package/dist/lib/resources/subagents.js +4 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/routines.d.ts +20 -0
- package/dist/lib/routines.js +6 -0
- package/dist/lib/run-defaults.d.ts +1 -0
- package/dist/lib/run-defaults.js +9 -0
- package/dist/lib/runner.d.ts +0 -12
- package/dist/lib/runner.js +49 -10
- 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 +16 -0
- package/dist/lib/secrets/agent.js +105 -3
- package/dist/lib/secrets/audit.d.ts +46 -0
- package/dist/lib/secrets/audit.js +56 -0
- package/dist/lib/secrets/bundles.d.ts +11 -8
- package/dist/lib/secrets/bundles.js +31 -23
- package/dist/lib/secrets/index.d.ts +2 -1
- package/dist/lib/secrets/index.js +8 -2
- package/dist/lib/secrets/remote.d.ts +75 -2
- package/dist/lib/secrets/remote.js +139 -5
- package/dist/lib/secrets/sync.js +3 -3
- package/dist/lib/session/active.d.ts +110 -26
- package/dist/lib/session/active.js +161 -50
- package/dist/lib/session/actor-sidecar.d.ts +23 -0
- package/dist/lib/session/actor-sidecar.js +101 -0
- package/dist/lib/session/db.d.ts +4 -1
- package/dist/lib/session/db.js +65 -8
- package/dist/lib/session/discover.d.ts +4 -0
- package/dist/lib/session/discover.js +13 -0
- package/dist/lib/session/hook-sessions.d.ts +9 -0
- package/dist/lib/session/hook-sessions.js +57 -8
- package/dist/lib/session/pid-registry.d.ts +13 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/types.d.ts +16 -0
- package/dist/lib/staleness/detectors/commands.js +2 -2
- package/dist/lib/staleness/detectors/permissions.js +1 -45
- package/dist/lib/staleness/writers/commands.js +4 -5
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -2
- package/dist/lib/startup/command-registry.js +2 -4
- package/dist/lib/subagents-registry.js +5 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -12
- package/dist/lib/teams/agents.d.ts +0 -2
- package/dist/lib/teams/agents.js +10 -27
- package/dist/lib/teams/index.d.ts +1 -1
- package/dist/lib/teams/index.js +1 -1
- package/dist/lib/types.d.ts +17 -9
- package/dist/lib/usage.d.ts +28 -5
- package/dist/lib/usage.js +271 -8
- package/dist/lib/versions.js +7 -1
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +25 -13
- package/package.json +1 -2
- package/dist/commands/check.d.ts +0 -15
- package/dist/commands/check.js +0 -180
- package/dist/commands/resources.d.ts +0 -5
|
@@ -43,12 +43,17 @@ export declare function isDangerousRemoteEnvKey(name: string): boolean;
|
|
|
43
43
|
*/
|
|
44
44
|
export declare function resolveHostSshTarget(nameOrAlias: string): Promise<string>;
|
|
45
45
|
/**
|
|
46
|
-
* Merge `--host <single>`
|
|
47
|
-
* list.
|
|
46
|
+
* Merge `--host <single>` / `--hosts <a,b,c>` (and their `--device` / `--devices`
|
|
47
|
+
* aliases) into an ordered, de-duplicated list. All four flags compose; any alone
|
|
48
|
+
* works. `--device`/`--devices` resolve identically to `--host`/`--hosts` so the
|
|
49
|
+
* fleet-wide `--device` vocabulary (see `agents activity`, `agents run --device`)
|
|
50
|
+
* works on the secrets remote commands too. Empty when none is set.
|
|
48
51
|
*/
|
|
49
52
|
export declare function parseHostsOption(opts: {
|
|
50
53
|
host?: string;
|
|
51
54
|
hosts?: string;
|
|
55
|
+
device?: string;
|
|
56
|
+
devices?: string;
|
|
52
57
|
}): string[];
|
|
53
58
|
/**
|
|
54
59
|
* Split a `bundle@host` reference. No `@` → a local bundle (host undefined).
|
|
@@ -104,3 +109,71 @@ export declare function remoteSecretsStream(target: string, args: string[], opts
|
|
|
104
109
|
export declare function remoteResolveEnv(target: string, bundle: string, opts?: {
|
|
105
110
|
osLookupName?: string;
|
|
106
111
|
}): Promise<Record<string, string>>;
|
|
112
|
+
/**
|
|
113
|
+
* Outcome of a post-push read-back verification (see verifyRemoteKeychainPush).
|
|
114
|
+
* - `ok` — the pushed keys materialized readably on the remote.
|
|
115
|
+
* - `locked-keychain` — the read-back gave the SPECIFIC signal of a keychain
|
|
116
|
+
* that didn't persist (the remote's headless "not unlocked
|
|
117
|
+
* in the secrets agent" guard, or a "stored item … not
|
|
118
|
+
* found" on read-back, or pushed keys simply absent). Only
|
|
119
|
+
* this verdict earns the locked-login-keychain diagnosis +
|
|
120
|
+
* `--remote-backend file` steer.
|
|
121
|
+
* - `error` — a DIFFERENT failure (flaky SSH, timeout, unparseable
|
|
122
|
+
* payload). The raw error is re-surfaced verbatim, never
|
|
123
|
+
* mislabeled as a locked keychain.
|
|
124
|
+
*/
|
|
125
|
+
export type RemoteKeychainWriteVerification = {
|
|
126
|
+
ok: true;
|
|
127
|
+
} | {
|
|
128
|
+
ok: false;
|
|
129
|
+
kind: 'locked-keychain';
|
|
130
|
+
reason: string;
|
|
131
|
+
} | {
|
|
132
|
+
ok: false;
|
|
133
|
+
kind: 'error';
|
|
134
|
+
reason: string;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Decide whether a keychain-backed push to a remote actually PERSISTED its secret
|
|
138
|
+
* value items, given the read-back of that bundle from the remote's own store.
|
|
139
|
+
*
|
|
140
|
+
* The silent-failure this guards: pushing `--remote-backend keychain` (default) to
|
|
141
|
+
* a macOS host over headless SSH lands the bundle METADATA but not readable value
|
|
142
|
+
* items — the remote login keychain is locked in the non-interactive SSH context,
|
|
143
|
+
* so Security accepts the item WRITE at the DB level but the biometry-ACL'd item is
|
|
144
|
+
* unreadable, and the remote `import` still reports success (values written first,
|
|
145
|
+
* metadata `noAcl` last — bundles.ts writeBundleWithItems). The metadata-only bundle
|
|
146
|
+
* then fails every later read with the confusing `Bundle '<b>' key '<k>': stored
|
|
147
|
+
* item '<item>' not found` (bundles.ts resolveBundleEnv). We catch it by reading
|
|
148
|
+
* the bundle back the same way a release will (`secrets export --plaintext --format
|
|
149
|
+
* json`, driven headlessly on the remote so its `agentOnly` guard FAILS FAST before
|
|
150
|
+
* any keychain read — no Touch ID prompt) and confirming every pushed key returned.
|
|
151
|
+
*
|
|
152
|
+
* Pure so both branches are unit-testable without a real locked keychain: inject the
|
|
153
|
+
* "read-back failed / key absent" condition through `readBack`.
|
|
154
|
+
*/
|
|
155
|
+
export declare function evaluateKeychainWriteVerification(pushedKeys: string[], readBack: {
|
|
156
|
+
ok: true;
|
|
157
|
+
keys: string[];
|
|
158
|
+
} | {
|
|
159
|
+
ok: false;
|
|
160
|
+
stderr: string;
|
|
161
|
+
}): RemoteKeychainWriteVerification;
|
|
162
|
+
/**
|
|
163
|
+
* The actionable error message for a failed keychain-over-SSH push verification.
|
|
164
|
+
* Names the cause (locked remote login keychain) and steers to the two real fixes:
|
|
165
|
+
* re-run with the headless-readable file backend, or unlock the remote keychain.
|
|
166
|
+
* Pure + exported so the exact guidance is asserted in tests.
|
|
167
|
+
*/
|
|
168
|
+
export declare function keychainWriteFailureMessage(host: string, bundle: string, reason: string): string;
|
|
169
|
+
/**
|
|
170
|
+
* Read a bundle back from a remote over SSH (headlessly, so it fails fast rather
|
|
171
|
+
* than prompting Touch ID) and confirm the pushed keys materialized. Drives the
|
|
172
|
+
* remote's own `secrets export <bundle> --plaintext --format json` — the same read
|
|
173
|
+
* a headless release performs — but keeps only the KEY NAMES; the plaintext values
|
|
174
|
+
* are dropped immediately and never retained or logged. Returns a verification
|
|
175
|
+
* verdict; the caller renders `keychainWriteFailureMessage` on failure.
|
|
176
|
+
*/
|
|
177
|
+
export declare function verifyRemoteKeychainPush(target: string, bundle: string, pushedKeys: string[], opts?: {
|
|
178
|
+
osLookupName?: string;
|
|
179
|
+
}): RemoteKeychainWriteVerification;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { sshExec, sshStream, assertValidSshTarget } from '../ssh-exec.js';
|
|
19
19
|
import { resolveHost } from '../hosts/registry.js';
|
|
20
|
-
import {
|
|
20
|
+
import { emitSecretAudit } from './audit.js';
|
|
21
21
|
import { sshTargetFor } from '../hosts/types.js';
|
|
22
22
|
import { buildRemoteAgentsInvocation } from '../hosts/remote-cmd.js';
|
|
23
23
|
import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
@@ -74,8 +74,11 @@ export async function resolveHostSshTarget(nameOrAlias) {
|
|
|
74
74
|
return nameOrAlias;
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
|
-
* Merge `--host <single>`
|
|
78
|
-
* list.
|
|
77
|
+
* Merge `--host <single>` / `--hosts <a,b,c>` (and their `--device` / `--devices`
|
|
78
|
+
* aliases) into an ordered, de-duplicated list. All four flags compose; any alone
|
|
79
|
+
* works. `--device`/`--devices` resolve identically to `--host`/`--hosts` so the
|
|
80
|
+
* fleet-wide `--device` vocabulary (see `agents activity`, `agents run --device`)
|
|
81
|
+
* works on the secrets remote commands too. Empty when none is set.
|
|
79
82
|
*/
|
|
80
83
|
export function parseHostsOption(opts) {
|
|
81
84
|
const out = [];
|
|
@@ -89,9 +92,14 @@ export function parseHostsOption(opts) {
|
|
|
89
92
|
};
|
|
90
93
|
if (opts.host)
|
|
91
94
|
push(opts.host);
|
|
95
|
+
if (opts.device)
|
|
96
|
+
push(opts.device);
|
|
92
97
|
if (opts.hosts)
|
|
93
98
|
for (const h of opts.hosts.split(','))
|
|
94
99
|
push(h);
|
|
100
|
+
if (opts.devices)
|
|
101
|
+
for (const h of opts.devices.split(','))
|
|
102
|
+
push(h);
|
|
95
103
|
return out;
|
|
96
104
|
}
|
|
97
105
|
/**
|
|
@@ -203,8 +211,8 @@ export async function remoteResolveEnv(target, bundle, opts = {}) {
|
|
|
203
211
|
// event on the INITIATING host too (values were pulled into this process and
|
|
204
212
|
// injected locally). Covers `secrets exec --host` and `run --secrets b@host`.
|
|
205
213
|
// Values never enter the payload — only the bundle, target host, and count.
|
|
206
|
-
|
|
207
|
-
|
|
214
|
+
emitSecretAudit({
|
|
215
|
+
event: 'secrets.get',
|
|
208
216
|
bundle,
|
|
209
217
|
operation: 'remote resolve',
|
|
210
218
|
source: 'remote',
|
|
@@ -214,3 +222,129 @@ export async function remoteResolveEnv(target, bundle, opts = {}) {
|
|
|
214
222
|
});
|
|
215
223
|
return env;
|
|
216
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* The remote's headless read-back raises one of these when a keychain-backed bundle
|
|
227
|
+
* has metadata but no readable value items — the exact locked-login-keychain
|
|
228
|
+
* signature. Anything else (connection refused, timeout, host key error) is a
|
|
229
|
+
* transient/unrelated failure and must NOT be mislabeled as a locked keychain.
|
|
230
|
+
*/
|
|
231
|
+
function isLockedKeychainReadBackError(stderr) {
|
|
232
|
+
const s = stderr.toLowerCase();
|
|
233
|
+
return (
|
|
234
|
+
// bundles.ts agentOnly guard: "…is not unlocked in the secrets agent…"
|
|
235
|
+
s.includes('not unlocked') ||
|
|
236
|
+
s.includes('secrets agent') ||
|
|
237
|
+
// resolveBundleEnv: "Bundle '<b>' key '<k>': stored item '<item>' not found."
|
|
238
|
+
s.includes('stored item') ||
|
|
239
|
+
s.includes('not found'));
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Decide whether a keychain-backed push to a remote actually PERSISTED its secret
|
|
243
|
+
* value items, given the read-back of that bundle from the remote's own store.
|
|
244
|
+
*
|
|
245
|
+
* The silent-failure this guards: pushing `--remote-backend keychain` (default) to
|
|
246
|
+
* a macOS host over headless SSH lands the bundle METADATA but not readable value
|
|
247
|
+
* items — the remote login keychain is locked in the non-interactive SSH context,
|
|
248
|
+
* so Security accepts the item WRITE at the DB level but the biometry-ACL'd item is
|
|
249
|
+
* unreadable, and the remote `import` still reports success (values written first,
|
|
250
|
+
* metadata `noAcl` last — bundles.ts writeBundleWithItems). The metadata-only bundle
|
|
251
|
+
* then fails every later read with the confusing `Bundle '<b>' key '<k>': stored
|
|
252
|
+
* item '<item>' not found` (bundles.ts resolveBundleEnv). We catch it by reading
|
|
253
|
+
* the bundle back the same way a release will (`secrets export --plaintext --format
|
|
254
|
+
* json`, driven headlessly on the remote so its `agentOnly` guard FAILS FAST before
|
|
255
|
+
* any keychain read — no Touch ID prompt) and confirming every pushed key returned.
|
|
256
|
+
*
|
|
257
|
+
* Pure so both branches are unit-testable without a real locked keychain: inject the
|
|
258
|
+
* "read-back failed / key absent" condition through `readBack`.
|
|
259
|
+
*/
|
|
260
|
+
export function evaluateKeychainWriteVerification(pushedKeys, readBack) {
|
|
261
|
+
if (!readBack.ok) {
|
|
262
|
+
const stderr = readBack.stderr.trim();
|
|
263
|
+
if (isLockedKeychainReadBackError(stderr)) {
|
|
264
|
+
// The remote's own headless read raised the not-unlocked / not-found signal —
|
|
265
|
+
// exactly the confusing error the user hits later. Surface it now, at push
|
|
266
|
+
// time, with the fix.
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
kind: 'locked-keychain',
|
|
270
|
+
reason: `the remote could not read it back${stderr ? ` (${stderr})` : ''}`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
// A transient / unrelated failure (flaky SSH, timeout, bad payload). Re-surface
|
|
274
|
+
// verbatim — do NOT diagnose a locked keychain from a connection error.
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
kind: 'error',
|
|
278
|
+
reason: stderr || 'read-back failed',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const present = new Set(readBack.keys);
|
|
282
|
+
const missing = pushedKeys.filter((k) => !present.has(k));
|
|
283
|
+
if (missing.length > 0) {
|
|
284
|
+
// Read-back succeeded but some pushed keys are absent — the value items didn't
|
|
285
|
+
// persist. Same locked-keychain cause and fix.
|
|
286
|
+
return {
|
|
287
|
+
ok: false,
|
|
288
|
+
kind: 'locked-keychain',
|
|
289
|
+
reason: `${missing.length} of ${pushedKeys.length} key(s) did not persist on the remote ` +
|
|
290
|
+
`(missing: ${missing.slice(0, 5).join(', ')}${missing.length > 5 ? ', …' : ''})`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return { ok: true };
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* The actionable error message for a failed keychain-over-SSH push verification.
|
|
297
|
+
* Names the cause (locked remote login keychain) and steers to the two real fixes:
|
|
298
|
+
* re-run with the headless-readable file backend, or unlock the remote keychain.
|
|
299
|
+
* Pure + exported so the exact guidance is asserted in tests.
|
|
300
|
+
*/
|
|
301
|
+
export function keychainWriteFailureMessage(host, bundle, reason) {
|
|
302
|
+
return (`${host}: pushed '${bundle}' but the keychain items did not persist — ${reason}. ` +
|
|
303
|
+
`A macOS login keychain is LOCKED under headless SSH, so a keychain-backed write ` +
|
|
304
|
+
`lands the bundle metadata but no readable secret items, and later reads fail with ` +
|
|
305
|
+
`"stored item '…' not found". Re-run with a headless-readable backend:\n` +
|
|
306
|
+
` agents secrets export ${bundle} --host ${host} --remote-backend file\n` +
|
|
307
|
+
`(needs AGENTS_SECRETS_PASSPHRASE set locally), or unlock the remote keychain first ` +
|
|
308
|
+
`(e.g. an interactive login / \`agents secrets unlock\` on ${host}) and retry.`);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Read a bundle back from a remote over SSH (headlessly, so it fails fast rather
|
|
312
|
+
* than prompting Touch ID) and confirm the pushed keys materialized. Drives the
|
|
313
|
+
* remote's own `secrets export <bundle> --plaintext --format json` — the same read
|
|
314
|
+
* a headless release performs — but keeps only the KEY NAMES; the plaintext values
|
|
315
|
+
* are dropped immediately and never retained or logged. Returns a verification
|
|
316
|
+
* verdict; the caller renders `keychainWriteFailureMessage` on failure.
|
|
317
|
+
*/
|
|
318
|
+
export function verifyRemoteKeychainPush(target, bundle, pushedKeys, opts = {}) {
|
|
319
|
+
const remoteCmd = buildRemoteAgentsInvocation(['secrets', 'export', bundle, '--plaintext', '--format', 'json'], undefined, osForTarget(target, opts.osLookupName));
|
|
320
|
+
const res = sshExec(target, remoteCmd, { timeoutMs: REMOTE_TIMEOUT_MS });
|
|
321
|
+
if (res.code !== 0) {
|
|
322
|
+
const why = res.timedOut ? 'timed out' : res.code === null ? 'ssh failed' : `exit ${res.code}`;
|
|
323
|
+
const stderr = `${why}${(res.stderr || res.stdout || '').trim() ? `: ${(res.stderr || res.stdout).trim()}` : ''}`;
|
|
324
|
+
return evaluateKeychainWriteVerification(pushedKeys, { ok: false, stderr });
|
|
325
|
+
}
|
|
326
|
+
// Take the outer { … } object (tolerate login-shell banner noise), read the key
|
|
327
|
+
// names, and immediately discard the values — we only need presence here.
|
|
328
|
+
const raw = res.stdout;
|
|
329
|
+
const start = raw.indexOf('{');
|
|
330
|
+
const end = raw.lastIndexOf('}');
|
|
331
|
+
const jsonText = start >= 0 && end >= start ? raw.slice(start, end + 1) : raw.trim();
|
|
332
|
+
let keys;
|
|
333
|
+
try {
|
|
334
|
+
const parsed = JSON.parse(jsonText);
|
|
335
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
336
|
+
return evaluateKeychainWriteVerification(pushedKeys, {
|
|
337
|
+
ok: false,
|
|
338
|
+
stderr: 'unexpected read-back payload',
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
keys = Object.keys(parsed);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return evaluateKeychainWriteVerification(pushedKeys, {
|
|
345
|
+
ok: false,
|
|
346
|
+
stderr: 'could not parse read-back JSON',
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return evaluateKeychainWriteVerification(pushedKeys, { ok: true, keys });
|
|
350
|
+
}
|
package/dist/lib/secrets/sync.js
CHANGED
|
@@ -14,7 +14,7 @@ import * as crypto from 'crypto';
|
|
|
14
14
|
import { deleteKeychainToken, getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from './index.js';
|
|
15
15
|
import { readBundle, writeBundle, keychainItemsForBundle, validateBundleName, } from './bundles.js';
|
|
16
16
|
import { rushSyncBackend } from './drivers/rush.js';
|
|
17
|
-
import {
|
|
17
|
+
import { emitSecretAudit } from './audit.js';
|
|
18
18
|
// PBKDF2 cost. 600k SHA-256 iters matches OWASP 2023+ guidance and keeps a
|
|
19
19
|
// passphrase prompt under a second on the hardware the CLI targets.
|
|
20
20
|
const PBKDF2_ITER = 600_000;
|
|
@@ -190,8 +190,8 @@ export async function pushBundle(name, opts) {
|
|
|
190
190
|
// bundle off-machine — the most sensitive read there is. It bypasses
|
|
191
191
|
// readAndResolveBundleEnv, so audit it explicitly. Values never enter the
|
|
192
192
|
// payload; only the bundle name and how many keys were read.
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
emitSecretAudit({
|
|
194
|
+
event: 'secrets.get',
|
|
195
195
|
bundle: name,
|
|
196
196
|
operation: 'sync push',
|
|
197
197
|
source: 'sync-push',
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import type { CloudTaskStatus } from '../cloud/types.js';
|
|
2
2
|
import { type PidSessionEntry } from './pid-registry.js';
|
|
3
3
|
import { type HookSessionIndex } from './hook-sessions.js';
|
|
4
|
-
import { type SessionActivity, type AwaitingReason, type StructuredQuestion, type TodoProgress, type DetectedPr, type DetectedWorktree, type DetectedTicket } from './state.js';
|
|
5
|
-
import type
|
|
4
|
+
import { type SessionState, type SessionActivity, type AwaitingReason, type StructuredQuestion, type TodoProgress, type DetectedPr, type DetectedWorktree, type DetectedTicket } from './state.js';
|
|
5
|
+
import { type SessionAttachment } from './types.js';
|
|
6
6
|
import { type SessionProvenance } from './provenance.js';
|
|
7
7
|
import { type DeviceRegistry } from '../devices/registry.js';
|
|
8
8
|
import { type Presence } from './detached.js';
|
|
9
|
+
/**
|
|
10
|
+
* The owner (actor id) to show for a session in `--active`. Prefers the actor
|
|
11
|
+
* recorded on the live-attribution source (the pid registry / teammate record),
|
|
12
|
+
* but falls back to the durable per-session actor sidecar — written at spawn and,
|
|
13
|
+
* unlike the pid entry, NOT overwritten by the SessionStart hook's own by-pid
|
|
14
|
+
* write. Without this fallback a real `agents run` shows no owner whenever the
|
|
15
|
+
* hook's actor-less entry wins the by-pid file (RUSH-2018 fix).
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveOwner(pidActor: string | null | undefined, sessionId: string | undefined): string | undefined;
|
|
9
18
|
/**
|
|
10
19
|
* Per-PID `lsof` probes run bounded and staggered rather than as one parallel
|
|
11
20
|
* fan-out: a simultaneous system-wide `lsof` burst reads to behavioral EDR
|
|
@@ -15,14 +24,27 @@ import { type Presence } from './detached.js';
|
|
|
15
24
|
export declare const LSOF_CONCURRENCY = 4;
|
|
16
25
|
export type ActiveContext = 'terminal' | 'teams' | 'cloud' | 'headless';
|
|
17
26
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* you
|
|
23
|
-
*
|
|
27
|
+
* Every status here is COMPUTED by the framework from observable signals — PID
|
|
28
|
+
* liveness and transcript last-write (mtime) — never self-reported by the agent.
|
|
29
|
+
*
|
|
30
|
+
* - `running` / `idle` / `input_required` — a live process's working / stopped
|
|
31
|
+
* / waiting-on-you activity, from its transcript (see {@link computeLiveSignals}).
|
|
32
|
+
* - `queued` — dispatched, work in the pipeline (cloud/headless launch).
|
|
33
|
+
* - `closed` — the PID is dead. The process has exited; report that, don't
|
|
34
|
+
* fabricate `idle` ("done, waiting for you") for a process that is simply gone.
|
|
35
|
+
* - `abandoned` — no transcript write in {@link ABANDONED_STALE_MS} (days): the
|
|
36
|
+
* session is dangling, whether its PID is dead (long gone) or still alive but
|
|
37
|
+
* stuck making no progress. A soft signal — it clears the moment it writes again.
|
|
38
|
+
* - `unknown` — the residual: genuinely NO signal (no PID info AND no file).
|
|
39
|
+
*
|
|
40
|
+
* A LIVE, fresh process is never `unknown` — "the process is alive" is itself a
|
|
41
|
+
* positive signal, so an opaque/unparseable live harness resolves to `running`
|
|
42
|
+
* (its honest floor), and every tracked harness with a locatable, parseable
|
|
43
|
+
* transcript (claude, codex, grok, droid, rush, gemini, kimi, hermes, opencode,
|
|
44
|
+
* antigravity) gets a real working/waiting/idle from its own parser — see
|
|
45
|
+
* {@link computeLiveSignals}, {@link lifecycleStatus} and {@link resolveFallbackStatus}.
|
|
24
46
|
*/
|
|
25
|
-
export type ActiveStatus = 'running' | 'idle' | 'queued' | 'input_required' | 'unknown';
|
|
47
|
+
export type ActiveStatus = 'running' | 'idle' | 'queued' | 'input_required' | 'closed' | 'abandoned' | 'unknown';
|
|
26
48
|
export interface ActiveSession {
|
|
27
49
|
context: ActiveContext;
|
|
28
50
|
kind: string;
|
|
@@ -109,6 +131,14 @@ export interface ActiveSession {
|
|
|
109
131
|
* Absent for cloud sessions (no local pid) and any pid whose env is unreadable.
|
|
110
132
|
*/
|
|
111
133
|
provenance?: SessionProvenance;
|
|
134
|
+
/**
|
|
135
|
+
* Who initiated this session — the resolved actor id stamped at spawn
|
|
136
|
+
* (`resolveActor().id`, read back from the pid registry / teammate record).
|
|
137
|
+
* A tailnet login/email for a resolved human, `UNRESOLVED@<host>` when it
|
|
138
|
+
* couldn't be determined, absent when the launch predates actor stamping.
|
|
139
|
+
* Surfaced as the owner column in `--active` (RUSH-2018).
|
|
140
|
+
*/
|
|
141
|
+
owner?: string;
|
|
112
142
|
/**
|
|
113
143
|
* The machine this session runs on, as a normalized device id (machineId()
|
|
114
144
|
* form). Set when merging cross-machine results so the grouped `--active`
|
|
@@ -172,6 +202,16 @@ export interface ActiveQueryOptions {
|
|
|
172
202
|
* transcript just because a GUI app service with the same basename is alive.
|
|
173
203
|
*/
|
|
174
204
|
export declare const ACTIVE_SESSION_STALE_MS: number;
|
|
205
|
+
/**
|
|
206
|
+
* A session whose transcript hasn't been written in this long is ABANDONED /
|
|
207
|
+
* dangling — the framework can no longer treat it as live work, whether its PID
|
|
208
|
+
* is dead (long gone) or still alive but making no progress (a hung agent).
|
|
209
|
+
* Two days: past a normal work gap, well short of a session legitimately kept
|
|
210
|
+
* open across a long weekend. Deliberately far wider than
|
|
211
|
+
* {@link ACTIVE_SESSION_STALE_MS} (which bounds transcript-borrowing, a different
|
|
212
|
+
* concern) — this is the lifecycle threshold, not the freshness window.
|
|
213
|
+
*/
|
|
214
|
+
export declare const ABANDONED_STALE_MS: number;
|
|
175
215
|
/**
|
|
176
216
|
* Resolve an agent kind from a process's reported executable. `comm` may be an
|
|
177
217
|
* absolute path (shim-launched agents), and Windows image names carry an
|
|
@@ -219,30 +259,74 @@ export declare function sessionFileTimes(sessionFile: string | undefined): {
|
|
|
219
259
|
birthtimeMs?: number;
|
|
220
260
|
mtimeMs?: number;
|
|
221
261
|
};
|
|
262
|
+
/**
|
|
263
|
+
* The lifecycle status computed purely from the two hard signals the framework
|
|
264
|
+
* always has — PID liveness and transcript last-write (mtime) — independent of
|
|
265
|
+
* anything the agent self-reports or what its last transcript turn happened to
|
|
266
|
+
* look like. Returns the definitive lifecycle label when one applies, or
|
|
267
|
+
* `undefined` when the process is live and fresh (so the caller uses the richer
|
|
268
|
+
* activity-derived status instead).
|
|
269
|
+
*
|
|
270
|
+
* - No write in {@link ABANDONED_STALE_MS} ⇒ `abandoned` (dangling): a
|
|
271
|
+
* days-stale session is not live work, whether its PID is dead (long gone)
|
|
272
|
+
* or still alive but stuck. Checked first — it outranks a bare `closed`.
|
|
273
|
+
* - Otherwise, PID dead ⇒ `closed`: the process has exited. This is the fix
|
|
274
|
+
* for the old "dead PID reports idle" lie — `idle` reads as "done, waiting
|
|
275
|
+
* for you", but a dead process is done, period.
|
|
276
|
+
* - Otherwise (alive + fresh) ⇒ `undefined`: defer to the activity engine.
|
|
277
|
+
*/
|
|
278
|
+
export declare function lifecycleStatus(pidAlive: boolean, mtimeMs: number | undefined, nowMs?: number): ActiveStatus | undefined;
|
|
222
279
|
/**
|
|
223
280
|
* The ONE place a fallback status is decided when no rich transcript state is
|
|
224
|
-
* available —
|
|
225
|
-
*
|
|
226
|
-
*
|
|
281
|
+
* available — an opaque kind we cannot parse (cursor, openclaw), or a transcript
|
|
282
|
+
* whose parse/tail was empty or unreadable. Honest by construction: computed from
|
|
283
|
+
* PID + mtime, never a fabricated `idle`.
|
|
227
284
|
*
|
|
228
|
-
* -
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* UI reads as "done and waiting"), and it never lies as `running` either.
|
|
237
|
-
* - No transcript and the process is not known alive → nothing to report ⇒ `idle`.
|
|
285
|
+
* - Dead PID, or a days-stale transcript ⇒ {@link lifecycleStatus} (`closed` /
|
|
286
|
+
* `abandoned`). A dead process is `closed`, not the old fabricated `idle`
|
|
287
|
+
* (which the UI reads as "done, waiting for you"); a dead process whose file
|
|
288
|
+
* also vanished is still `closed` (death is a definitive answer, not `unknown`).
|
|
289
|
+
* - Live + fresh ⇒ `running`. A live process is, at minimum, running: we may
|
|
290
|
+
* not see WHAT an opaque harness (or an empty tail) is doing, but the process
|
|
291
|
+
* being alive is a positive signal — never a blank `unknown` (the old blanket
|
|
292
|
+
* bug for every non-claude/codex live agent) nor a downgraded `idle`.
|
|
238
293
|
*/
|
|
239
|
-
export declare function resolveFallbackStatus(sessionFile: string | undefined, pidAlive: boolean): ActiveStatus;
|
|
294
|
+
export declare function resolveFallbackStatus(sessionFile: string | undefined, pidAlive: boolean, nowMs?: number): ActiveStatus;
|
|
240
295
|
/**
|
|
241
|
-
* Locate the live transcript for an agent process. Claude files are keyed by
|
|
242
|
-
*
|
|
243
|
-
*
|
|
296
|
+
* Locate the live transcript for an agent process. Claude files are keyed by cwd
|
|
297
|
+
* (+ optional session uuid), so they resolve straight off disk. Every OTHER
|
|
298
|
+
* tracked harness — Codex (date-partitioned), plus grok / droid / rush / gemini /
|
|
299
|
+
* kimi / hermes / opencode / antigravity (per-session dirs, SQLite, single-JSON)
|
|
300
|
+
* — is resolved through the session index by cwd: the newest indexed transcript
|
|
301
|
+
* for that cwd, bounded by ACTIVE_SESSION_STALE_MS so a live pid never borrows a
|
|
302
|
+
* weeks-old transcript. This is what lets a live NON-claude/codex agent get a real
|
|
303
|
+
* status instead of falling through to `unknown` (the file feeds
|
|
304
|
+
* {@link computeLiveSignals}). An opaque kind we don't track (cursor) still yields
|
|
305
|
+
* undefined here and degrades honestly to a live `running`.
|
|
244
306
|
*/
|
|
245
307
|
export declare function findSessionFileForKind(kind: string, cwd?: string, sessionId?: string): string | undefined;
|
|
308
|
+
/** Live per-session signals derived from one transcript-tail read. */
|
|
309
|
+
interface LiveSignals {
|
|
310
|
+
/** Rich inferred state (activity/preview/badges). Undefined when unreadable. */
|
|
311
|
+
state?: SessionState;
|
|
312
|
+
/** Rolling output-token throughput (tokens/sec). Undefined when no usage in the tail. */
|
|
313
|
+
tokPerSec?: number;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Derive the inferred state (working / waiting / idle + preview/badges) and, for
|
|
317
|
+
* the two harnesses whose raw lines carry it, the output-token throughput.
|
|
318
|
+
*
|
|
319
|
+
* Claude/Codex take the fast bounded byte-tail ({@link readSessionTailWithRaw}) —
|
|
320
|
+
* the hot path, and the only two that also yield throughput (their raw lines
|
|
321
|
+
* carry usage the event model drops). EVERY OTHER tracked harness (grok, droid,
|
|
322
|
+
* rush, gemini, kimi, hermes, opencode, antigravity) is parsed with its own
|
|
323
|
+
* parser and run through the SAME {@link inferSessionState}, so a live
|
|
324
|
+
* non-claude/codex agent gets a real working/waiting/idle instead of the blanket
|
|
325
|
+
* `unknown` it used to fall through to. An opaque/untracked kind (cursor) or an
|
|
326
|
+
* unreadable/empty transcript yields an empty signal set, and the caller's
|
|
327
|
+
* {@link resolveFallbackStatus} reports the honest live floor (`running`).
|
|
328
|
+
*/
|
|
329
|
+
export declare function computeLiveSignals(kind: string, sessionFile: string | undefined, cwd: string | undefined, pidAlive: boolean): LiveSignals;
|
|
246
330
|
/** Live teams teammates. Reuses AgentManager which already polls PIDs via `kill -0`. */
|
|
247
331
|
export declare function listTeamsActive(): Promise<ActiveSession[]>;
|
|
248
332
|
/** Live editor-terminal agents across every IDE window. */
|