@phnx-labs/agents-cli 1.22.24 → 1.22.26
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 +350 -1
- package/README.md +31 -6
- package/dist/bin/agents +0 -0
- package/dist/browser.js +14 -4
- package/dist/commands/apply.js +52 -8
- package/dist/commands/browser.js +35 -0
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +108 -9
- package/dist/commands/exec.js +19 -16
- package/dist/commands/feed.d.ts +5 -0
- package/dist/commands/feed.js +21 -1
- package/dist/commands/focus.js +2 -2
- package/dist/commands/insights.d.ts +25 -19
- package/dist/commands/insights.js +107 -33
- package/dist/commands/menubar.js +8 -0
- package/dist/commands/reconnect.d.ts +46 -0
- package/dist/commands/reconnect.js +109 -0
- package/dist/commands/routines.d.ts +3 -0
- package/dist/commands/routines.js +72 -49
- package/dist/commands/run-cloud.js +1 -1
- package/dist/commands/secrets.d.ts +2 -8
- package/dist/commands/secrets.js +29 -105
- package/dist/commands/sessions-browser.d.ts +1 -1
- package/dist/commands/sessions-browser.js +27 -7
- package/dist/commands/sessions-resume.js +3 -2
- package/dist/commands/sessions.d.ts +13 -1
- package/dist/commands/sessions.js +28 -2
- package/dist/commands/setup-secrets.d.ts +1 -0
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup-watchdog.js +5 -10
- package/dist/commands/setup.d.ts +26 -3
- package/dist/commands/setup.js +106 -47
- package/dist/commands/teams.d.ts +6 -0
- package/dist/commands/teams.js +46 -2
- package/dist/commands/trends.d.ts +8 -0
- package/dist/commands/trends.js +10 -156
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +26 -66
- package/dist/index.js +37 -2
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +155 -23
- package/dist/lib/analytics/dashboard.d.ts +10 -6
- package/dist/lib/analytics/dashboard.js +6 -4
- package/dist/lib/analytics/mix-commands.d.ts +53 -0
- package/dist/lib/analytics/mix-commands.js +229 -0
- package/dist/lib/analytics/recipes.d.ts +19 -14
- package/dist/lib/analytics/recipes.js +4 -2
- package/dist/lib/browser/ipc.d.ts +26 -0
- package/dist/lib/browser/ipc.js +139 -24
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +1 -1
- package/dist/lib/browser/stream.d.ts +14 -0
- package/dist/lib/browser/stream.js +71 -0
- package/dist/lib/channels/owner-sink.d.ts +27 -0
- package/dist/lib/channels/owner-sink.js +93 -0
- package/dist/lib/cloud/cursor.d.ts +79 -0
- package/dist/lib/cloud/cursor.js +228 -0
- package/dist/lib/cloud/registry.js +2 -0
- package/dist/lib/cloud/types.d.ts +7 -2
- package/dist/lib/cloud/types.js +14 -0
- package/dist/lib/crabbox/cli.d.ts +2 -2
- package/dist/lib/crabbox/config.d.ts +7 -8
- package/dist/lib/crabbox/config.js +14 -14
- package/dist/lib/crabbox/lease.d.ts +11 -4
- package/dist/lib/crabbox/lease.js +40 -8
- package/dist/lib/crabbox/setup-copy.d.ts +5 -0
- package/dist/lib/crabbox/setup-copy.js +17 -1
- package/dist/lib/daemon.js +27 -1
- package/dist/lib/device-config.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +13 -1
- package/dist/lib/devices/doctor-findings.js +73 -2
- package/dist/lib/events.d.ts +9 -0
- package/dist/lib/events.js +58 -0
- package/dist/lib/exec.d.ts +3 -3
- package/dist/lib/exec.js +24 -10
- package/dist/lib/feed-outcome.d.ts +3 -0
- package/dist/lib/feed-outcome.js +18 -10
- package/dist/lib/feed.d.ts +4 -0
- package/dist/lib/fleet/apply.d.ts +59 -3
- package/dist/lib/fleet/apply.js +183 -6
- package/dist/lib/fleet/types.d.ts +21 -2
- package/dist/lib/hooks/cache.js +15 -0
- package/dist/lib/hosts/passthrough.d.ts +44 -0
- package/dist/lib/hosts/passthrough.js +84 -12
- package/dist/lib/hosts/ready.d.ts +2 -0
- package/dist/lib/hosts/ready.js +10 -1
- package/dist/lib/hosts/reconnect.d.ts +14 -12
- package/dist/lib/hosts/reconnect.js +41 -40
- package/dist/lib/mcp.js +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/snapshot.d.ts +15 -0
- package/dist/lib/menubar/snapshot.js +40 -0
- package/dist/lib/plugins.js +13 -1
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/routine-process-cleanup.d.ts +9 -0
- package/dist/lib/routine-process-cleanup.js +73 -0
- package/dist/lib/routines.js +14 -2
- package/dist/lib/runner.d.ts +0 -3
- package/dist/lib/runner.js +6 -19
- 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 +1 -20
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +13 -0
- package/dist/lib/secrets/push.d.ts +94 -0
- package/dist/lib/secrets/push.js +145 -0
- package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
- package/dist/lib/secrets/rc-hygiene.js +14 -1
- package/dist/lib/secrets/reaper.d.ts +15 -1
- package/dist/lib/secrets/reaper.js +30 -3
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/db.d.ts +21 -3
- package/dist/lib/session/db.js +229 -15
- package/dist/lib/session/discover.d.ts +1 -0
- package/dist/lib/session/discover.js +115 -19
- package/dist/lib/session/insights.d.ts +18 -0
- package/dist/lib/session/insights.js +143 -1
- package/dist/lib/session/remote-list.d.ts +2 -0
- package/dist/lib/session/remote-list.js +1 -0
- package/dist/lib/session/session-cache.d.ts +4 -4
- package/dist/lib/session/session-cache.js +4 -4
- package/dist/lib/session/tool-index.js +133 -22
- package/dist/lib/session/tool-store.d.ts +26 -2
- package/dist/lib/session/tool-store.js +36 -17
- package/dist/lib/shims.js +21 -1
- package/dist/lib/signin-badge.js +2 -0
- package/dist/lib/ssh-exec.js +8 -2
- package/dist/lib/startup/command-registry.d.ts +16 -0
- package/dist/lib/startup/command-registry.js +46 -0
- package/dist/lib/teams/agents.d.ts +13 -0
- package/dist/lib/teams/agents.js +76 -8
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/placement-probe.d.ts +21 -0
- package/dist/lib/teams/placement-probe.js +135 -0
- package/dist/lib/teams/scheduler.d.ts +74 -1
- package/dist/lib/teams/scheduler.js +187 -10
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/versions.js +16 -1
- package/dist/lib/watchdog/service.d.ts +17 -0
- package/dist/lib/watchdog/service.js +39 -0
- package/package.json +1 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
3
|
+
* `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
|
|
4
|
+
* quote pair and takes the inner bytes verbatim (no unescaping), so any single-line
|
|
5
|
+
* value survives unchanged with no escaping. Newlines would break its line-based
|
|
6
|
+
* parse, so multi-line values are rejected rather than silently corrupted.
|
|
7
|
+
*/
|
|
8
|
+
export declare function bundleEnvToDotenv(env: Record<string, string>): string;
|
|
9
|
+
/** Where the bundle should live ON THE REMOTE. */
|
|
10
|
+
export type RemoteBackend = 'keychain' | 'file';
|
|
11
|
+
/** A bundle read once, ready to push to any number of hosts. */
|
|
12
|
+
export interface ResolvedBundleForPush {
|
|
13
|
+
/** key -> value. Never logged; only its KEY NAMES are ever surfaced. */
|
|
14
|
+
env: Record<string, string>;
|
|
15
|
+
/** The same values as a dotenv blob, shipped over ssh stdin (never argv). */
|
|
16
|
+
dotenv: string;
|
|
17
|
+
keyCount: number;
|
|
18
|
+
}
|
|
19
|
+
export interface PushBundleOptions {
|
|
20
|
+
remoteBackend: RemoteBackend;
|
|
21
|
+
/** Overwrite a key that already exists on the remote. */
|
|
22
|
+
force?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Forwarded to the remote as the FIRST stdin line for the FILE backend only,
|
|
25
|
+
* and only when non-empty.
|
|
26
|
+
*
|
|
27
|
+
* Empty is the DEFAULT and the good path: the remote's file store then
|
|
28
|
+
* auto-provisions its own machine-local key (0600, `~/.agents/.secrets-key/`)
|
|
29
|
+
* and reads headlessly. Setting this keys the remote bundle under a shared
|
|
30
|
+
* off-disk secret instead — an opt-in, never a requirement. Requiring one is
|
|
31
|
+
* what pushed operators toward exporting the master key fleet-wide (RUSH-1968).
|
|
32
|
+
*/
|
|
33
|
+
passphrase?: string;
|
|
34
|
+
/** Label for the audit trail — `export --host` vs `fleet apply`. */
|
|
35
|
+
operation: string;
|
|
36
|
+
}
|
|
37
|
+
export interface PushBundleResult {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
host: string;
|
|
40
|
+
bundle: string;
|
|
41
|
+
keyCount: number;
|
|
42
|
+
/** One line for the caller to render. Never contains a secret value. */
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read and resolve a bundle once, for pushing to one or more hosts.
|
|
47
|
+
*
|
|
48
|
+
* `agentOnly` + `keyMode: 'storage'` match what `export --host` has always
|
|
49
|
+
* passed: storage-shaped values, and the headless guard that fails fast rather
|
|
50
|
+
* than popping Touch ID inside an automated run.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveBundleForPush(bundle: string, caller: string): ResolvedBundleForPush;
|
|
53
|
+
/**
|
|
54
|
+
* WHICH transport a (backend, target-OS) pair selects, and the exact bytes it
|
|
55
|
+
* will send. One of four outcomes, and picking the wrong one is silent: a
|
|
56
|
+
* Windows target handed a POSIX `bash -lc` produces garbage, and a Windows
|
|
57
|
+
* target handed `--from -` hangs forever on a stdin the `agents.ps1` shim never
|
|
58
|
+
* forwards. Neither shows up as a failed ssh, so the selection is the thing
|
|
59
|
+
* worth pinning.
|
|
60
|
+
*
|
|
61
|
+
* Separated from execution so it is decidable without a network: the branch is
|
|
62
|
+
* chosen from the device registry alone, and `pushResolvedBundleToHost` below
|
|
63
|
+
* does nothing but run what this returns.
|
|
64
|
+
*/
|
|
65
|
+
export type PushTransport =
|
|
66
|
+
/** No supported command exists for this pair — fail loud, never a wrong path. */
|
|
67
|
+
{
|
|
68
|
+
kind: 'refuse';
|
|
69
|
+
message: string;
|
|
70
|
+
}
|
|
71
|
+
/** A command sent over the raw ssh engine, with the .env on stdin. */
|
|
72
|
+
| {
|
|
73
|
+
kind: 'ssh';
|
|
74
|
+
remoteCmd: string;
|
|
75
|
+
input: string;
|
|
76
|
+
}
|
|
77
|
+
/** The OS-aware `agents secrets` wrapper, the READ inverse's own path. */
|
|
78
|
+
| {
|
|
79
|
+
kind: 'remote-secrets';
|
|
80
|
+
args: string[];
|
|
81
|
+
input: string;
|
|
82
|
+
};
|
|
83
|
+
/** Choose the transport for one push. Pure: registry read in, plan out. */
|
|
84
|
+
export declare function planPushTransport(resolved: ResolvedBundleForPush, bundle: string, host: string, opts: PushBundleOptions): PushTransport;
|
|
85
|
+
/**
|
|
86
|
+
* Push an already-resolved bundle to ONE host.
|
|
87
|
+
*
|
|
88
|
+
* Drives the remote's own `agents secrets import --from -`, so the values land
|
|
89
|
+
* in the remote's chosen backend and the .env is read off ssh stdin rather than
|
|
90
|
+
* parsed by a remote shell. `import` auto-creates the bundle.
|
|
91
|
+
*/
|
|
92
|
+
export declare function pushResolvedBundleToHost(resolved: ResolvedBundleForPush, bundle: string, host: string, opts: PushBundleOptions): PushBundleResult;
|
|
93
|
+
/** Resolve and push in one call — for a single host. */
|
|
94
|
+
export declare function pushBundleToHost(bundle: string, host: string, opts: PushBundleOptions): PushBundleResult;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push one bundle's values to a remote host over SSH — the provisioning
|
|
3
|
+
* primitive behind `agents secrets export --host` and, from RUSH-1968, behind
|
|
4
|
+
* `agents fleet apply --provision-secrets`.
|
|
5
|
+
*
|
|
6
|
+
* This logic used to live inline in the `export --host` command action. It moved
|
|
7
|
+
* here because `lib/fleet/apply.ts` needs it and a lib MUST NOT import a command
|
|
8
|
+
* module — and because the absence of a callable primitive is part of why
|
|
9
|
+
* `fleet apply` never provisioned secrets at all, leaving an operator to
|
|
10
|
+
* hand-export the file store's master key across the fleet.
|
|
11
|
+
*
|
|
12
|
+
* Two rules shape the shape of this module:
|
|
13
|
+
*
|
|
14
|
+
* - **It never prints.** The lib layer stays `console.*`-free (SEC-14/SEC-16), so
|
|
15
|
+
* every outcome is returned as data and the CALLER renders it. That is also
|
|
16
|
+
* what lets `fleet apply` fold a push into its own per-device report instead of
|
|
17
|
+
* interleaving stray lines into it.
|
|
18
|
+
* - **Resolve once, push N times.** `resolveBundleForPush` is separate because
|
|
19
|
+
* reading a bundle can prompt (Touch ID); doing it per host would prompt per
|
|
20
|
+
* host. `export --host a,b,c` resolves once and pushes three times.
|
|
21
|
+
*/
|
|
22
|
+
import { sshExec } from '../ssh-exec.js';
|
|
23
|
+
import { remoteShellFor, buildWindowsStdinImportCommand } from '../hosts/remote-cmd.js';
|
|
24
|
+
import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
25
|
+
import { remoteSecretsRaw, verifyRemoteKeychainPush, keychainWriteFailureMessage, buildRemoteFileImportCommand, } from './remote.js';
|
|
26
|
+
import { readAndResolveBundleEnv } from './bundles.js';
|
|
27
|
+
/**
|
|
28
|
+
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
29
|
+
* `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
|
|
30
|
+
* quote pair and takes the inner bytes verbatim (no unescaping), so any single-line
|
|
31
|
+
* value survives unchanged with no escaping. Newlines would break its line-based
|
|
32
|
+
* parse, so multi-line values are rejected rather than silently corrupted.
|
|
33
|
+
*/
|
|
34
|
+
export function bundleEnvToDotenv(env) {
|
|
35
|
+
const lines = [];
|
|
36
|
+
for (const [k, v] of Object.entries(env)) {
|
|
37
|
+
if (/[\r\n]/.test(v)) {
|
|
38
|
+
throw new Error(`Key '${k}' has a multi-line value; the SSH .env transport can't carry newlines. ` +
|
|
39
|
+
`Set it directly on the remote with 'agents secrets add ${k} --value-stdin'.`);
|
|
40
|
+
}
|
|
41
|
+
lines.push(`${k}="${v}"`);
|
|
42
|
+
}
|
|
43
|
+
return lines.join('\n') + '\n';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read and resolve a bundle once, for pushing to one or more hosts.
|
|
47
|
+
*
|
|
48
|
+
* `agentOnly` + `keyMode: 'storage'` match what `export --host` has always
|
|
49
|
+
* passed: storage-shaped values, and the headless guard that fails fast rather
|
|
50
|
+
* than popping Touch ID inside an automated run.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveBundleForPush(bundle, caller) {
|
|
53
|
+
const { env } = readAndResolveBundleEnv(bundle, { caller, keyMode: 'storage', agentOnly: true });
|
|
54
|
+
return { env, dotenv: bundleEnvToDotenv(env), keyCount: Object.keys(env).length };
|
|
55
|
+
}
|
|
56
|
+
function isPowershellTarget(host) {
|
|
57
|
+
return remoteShellFor(resolveRemoteOsSync(host.split('@').pop() ?? host)) === 'powershell';
|
|
58
|
+
}
|
|
59
|
+
/** Choose the transport for one push. Pure: registry read in, plan out. */
|
|
60
|
+
export function planPushTransport(resolved, bundle, host, opts) {
|
|
61
|
+
const powershell = isPowershellTarget(host);
|
|
62
|
+
if (opts.remoteBackend === 'file') {
|
|
63
|
+
// Both file-backend paths build a POSIX `bash -lc` command. Refuse a Windows
|
|
64
|
+
// target cleanly rather than emit broken PowerShell (fail loud at the
|
|
65
|
+
// boundary, never a silent wrong path).
|
|
66
|
+
if (powershell) {
|
|
67
|
+
return { kind: 'refuse', message: 'file backend export to a Windows target is not yet supported' };
|
|
68
|
+
}
|
|
69
|
+
const { remoteCmd, input } = buildRemoteFileImportCommand(bundle, resolved.dotenv, {
|
|
70
|
+
passphrase: opts.passphrase ?? '',
|
|
71
|
+
force: opts.force,
|
|
72
|
+
});
|
|
73
|
+
return { kind: 'ssh', remoteCmd, input };
|
|
74
|
+
}
|
|
75
|
+
if (powershell) {
|
|
76
|
+
// Keychain on a Windows target: the `agents.ps1` shim doesn't forward
|
|
77
|
+
// ssh-piped stdin to node, so `--from -` would hang. Bridge the piped .env
|
|
78
|
+
// through PowerShell into a temp file and import `--from <file>` (deleted
|
|
79
|
+
// afterwards). Same hardened ssh engine; the .env still only ever crosses
|
|
80
|
+
// the wire over ssh stdin.
|
|
81
|
+
return {
|
|
82
|
+
kind: 'ssh',
|
|
83
|
+
remoteCmd: buildWindowsStdinImportCommand(bundle, { force: opts.force }),
|
|
84
|
+
input: resolved.dotenv,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Keychain on a POSIX target: OS-aware wrapping + the hardened ssh engine
|
|
88
|
+
// (BatchMode, ConnectTimeout, keepalive, control-socket reuse) via the same
|
|
89
|
+
// path the READ inverse (`remoteResolveEnv`) uses.
|
|
90
|
+
return {
|
|
91
|
+
kind: 'remote-secrets',
|
|
92
|
+
args: ['import', bundle, '--from', '-', ...(opts.force ? ['--force'] : [])],
|
|
93
|
+
input: resolved.dotenv,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Push an already-resolved bundle to ONE host.
|
|
98
|
+
*
|
|
99
|
+
* Drives the remote's own `agents secrets import --from -`, so the values land
|
|
100
|
+
* in the remote's chosen backend and the .env is read off ssh stdin rather than
|
|
101
|
+
* parsed by a remote shell. `import` auto-creates the bundle.
|
|
102
|
+
*/
|
|
103
|
+
export function pushResolvedBundleToHost(resolved, bundle, host, opts) {
|
|
104
|
+
const fail = (message) => ({ ok: false, host, bundle, keyCount: resolved.keyCount, message });
|
|
105
|
+
const plan = planPushTransport(resolved, bundle, host, opts);
|
|
106
|
+
if (plan.kind === 'refuse')
|
|
107
|
+
return fail(plan.message);
|
|
108
|
+
const res = plan.kind === 'ssh'
|
|
109
|
+
? sshExec(host, plan.remoteCmd, { input: plan.input })
|
|
110
|
+
: remoteSecretsRaw(host, plan.args, { input: plan.input, osLookupName: host });
|
|
111
|
+
if (res.code === null) {
|
|
112
|
+
return fail(res.stderr.trim() || (res.timedOut ? 'ssh timed out' : 'ssh failed'));
|
|
113
|
+
}
|
|
114
|
+
if (res.code !== 0) {
|
|
115
|
+
const msg = (res.stderr || res.stdout || '').trim();
|
|
116
|
+
return fail(`remote import failed (exit ${res.code})${msg ? `: ${msg}` : ''}`);
|
|
117
|
+
}
|
|
118
|
+
// A keychain-backed push to a macOS remote over headless SSH can land the
|
|
119
|
+
// bundle metadata but no READABLE value items: the remote login keychain is
|
|
120
|
+
// locked in the non-interactive SSH context, so Security accepts the write but
|
|
121
|
+
// the biometry-ACL'd item is unreadable — and the remote `import` still exits
|
|
122
|
+
// 0. Read it back the way a release will and FAIL LOUDLY, rather than leave a
|
|
123
|
+
// metadata-only bundle that breaks later with "stored item not found". The
|
|
124
|
+
// file backend is headless-readable by construction, so it is skipped.
|
|
125
|
+
if (opts.remoteBackend === 'keychain') {
|
|
126
|
+
const verdict = verifyRemoteKeychainPush(host, bundle, Object.keys(resolved.env), { osLookupName: host });
|
|
127
|
+
if (!verdict.ok) {
|
|
128
|
+
return fail(verdict.kind === 'locked-keychain'
|
|
129
|
+
? keychainWriteFailureMessage(host, bundle, verdict.reason)
|
|
130
|
+
: `pushed '${bundle}' but could not verify it on the remote: ${verdict.reason}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const remoteMsg = (res.stdout || '').trim().split('\n').map((l) => l.trim()).filter(Boolean).pop();
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
host,
|
|
137
|
+
bundle,
|
|
138
|
+
keyCount: resolved.keyCount,
|
|
139
|
+
message: remoteMsg || `${resolved.keyCount} key(s) exported`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Resolve and push in one call — for a single host. */
|
|
143
|
+
export function pushBundleToHost(bundle, host, opts) {
|
|
144
|
+
return pushResolvedBundleToHost(resolveBundleForPush(bundle, opts.operation), bundle, host, opts);
|
|
145
|
+
}
|
|
@@ -32,6 +32,20 @@ export interface RcSecretFinding {
|
|
|
32
32
|
/** The file-store master passphrase gets called out separately — it is the highest-severity case. */
|
|
33
33
|
isMasterPassphrase: boolean;
|
|
34
34
|
}
|
|
35
|
+
/** The file-store master key. Its own resolution prefers an off-env 0600 file, so
|
|
36
|
+
* a shell-rc export is always wrong once the store exists (RUSH-1968). */
|
|
37
|
+
export declare const MASTER_PASSPHRASE = "AGENTS_SECRETS_PASSPHRASE";
|
|
38
|
+
/**
|
|
39
|
+
* True when the file-store master key is live in THIS process's environment.
|
|
40
|
+
*
|
|
41
|
+
* The scanner above reads FILES, and that leaves a hole: a value inherited by a
|
|
42
|
+
* long-lived process outlives the rc line that set it, so deleting the export
|
|
43
|
+
* makes `scanRcExports` report clean while every shell, editor and agent started
|
|
44
|
+
* beforehand still carries the key and passes it to everything it spawns.
|
|
45
|
+
* Returns a boolean — never the value — so a finding built from it can be
|
|
46
|
+
* printed, logged, or shipped without leaking the secret.
|
|
47
|
+
*/
|
|
48
|
+
export declare function masterPassphraseInEnv(): boolean;
|
|
35
49
|
/** True if a variable name looks like it holds a credential value. */
|
|
36
50
|
export declare function isCredentialName(name: string): boolean;
|
|
37
51
|
/**
|
|
@@ -33,7 +33,20 @@ export const RC_FILENAMES = [
|
|
|
33
33
|
];
|
|
34
34
|
/** The file-store master key. Its own resolution prefers an off-env 0600 file, so
|
|
35
35
|
* a shell-rc export is always wrong once the store exists (RUSH-1968). */
|
|
36
|
-
const MASTER_PASSPHRASE = 'AGENTS_SECRETS_PASSPHRASE';
|
|
36
|
+
export const MASTER_PASSPHRASE = 'AGENTS_SECRETS_PASSPHRASE';
|
|
37
|
+
/**
|
|
38
|
+
* True when the file-store master key is live in THIS process's environment.
|
|
39
|
+
*
|
|
40
|
+
* The scanner above reads FILES, and that leaves a hole: a value inherited by a
|
|
41
|
+
* long-lived process outlives the rc line that set it, so deleting the export
|
|
42
|
+
* makes `scanRcExports` report clean while every shell, editor and agent started
|
|
43
|
+
* beforehand still carries the key and passes it to everything it spawns.
|
|
44
|
+
* Returns a boolean — never the value — so a finding built from it can be
|
|
45
|
+
* printed, logged, or shipped without leaking the secret.
|
|
46
|
+
*/
|
|
47
|
+
export function masterPassphraseInEnv() {
|
|
48
|
+
return (process.env[MASTER_PASSPHRASE] ?? '').length > 0;
|
|
49
|
+
}
|
|
37
50
|
/**
|
|
38
51
|
* Last `_`-delimited segment values that mark a variable as credential-shaped.
|
|
39
52
|
* Matched against the FINAL segment (segment equality, not substring) so
|
|
@@ -27,7 +27,12 @@ export interface KeychainProcessSnapshot {
|
|
|
27
27
|
* `null` means "could not capture" — the planner must fail closed.
|
|
28
28
|
*/
|
|
29
29
|
startTime: string | null;
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* True when this process is a REAP-ELIGIBLE helper invocation — the installed
|
|
32
|
+
* helper binary running a short-lived keychain verb. False for a non-helper
|
|
33
|
+
* process AND for the long-lived `watch-lock` watcher (see
|
|
34
|
+
* {@link isReapableHelperCommand}), which must never be reaped.
|
|
35
|
+
*/
|
|
31
36
|
isHelper: boolean;
|
|
32
37
|
}
|
|
33
38
|
/**
|
|
@@ -78,6 +83,15 @@ export declare function planKeychainReap(snapshots: KeychainProcessSnapshot[], n
|
|
|
78
83
|
* Returns null for an unparseable value so the caller drops the row.
|
|
79
84
|
*/
|
|
80
85
|
export declare function parseEtimeToSeconds(raw: string): number | null;
|
|
86
|
+
/**
|
|
87
|
+
* Whether a `ps` command line is a REAP-ELIGIBLE helper invocation: the installed
|
|
88
|
+
* helper binary running a short-lived keychain verb (get/has/list/set/delete/
|
|
89
|
+
* migrate-*) that a wedged `coreauthd` can hang. Returns false for a non-helper
|
|
90
|
+
* command AND for the deliberately long-lived `watch-lock` watcher — matching by
|
|
91
|
+
* the full argv (`ps … command=`), so a live-parent `watch-lock` child is never
|
|
92
|
+
* mistaken for a stuck read and killed. Pure; unit-tested.
|
|
93
|
+
*/
|
|
94
|
+
export declare function isReapableHelperCommand(command: string, helperPath: string): boolean;
|
|
81
95
|
/** Test seam: reset the persisted candidate state. */
|
|
82
96
|
export declare function resetKeychainReaperCandidatesForTest(): void;
|
|
83
97
|
/**
|
|
@@ -134,6 +134,31 @@ function parsePsLine(line) {
|
|
|
134
134
|
return null;
|
|
135
135
|
return { pid, ppid, elapsedSec, command };
|
|
136
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* The one helper verb that is DELIBERATELY long-lived: the broker's auto-lock
|
|
139
|
+
* sleep/lock watcher (`spawn(getKeychainHelperPath(), ['watch-lock'], …)` in
|
|
140
|
+
* `agent.ts`). It lives for the broker's whole hold — potentially days — as a
|
|
141
|
+
* healthy child of the live broker/daemon, emitting LOCK/SLEEP lines that wipe
|
|
142
|
+
* the in-memory secret store on sleep. It is NOT a stuck keychain read, so the
|
|
143
|
+
* reaper must never target it: killing it silently disables auto-lock-on-sleep.
|
|
144
|
+
*/
|
|
145
|
+
const HELPER_WATCH_LOCK_VERB = 'watch-lock';
|
|
146
|
+
/**
|
|
147
|
+
* Whether a `ps` command line is a REAP-ELIGIBLE helper invocation: the installed
|
|
148
|
+
* helper binary running a short-lived keychain verb (get/has/list/set/delete/
|
|
149
|
+
* migrate-*) that a wedged `coreauthd` can hang. Returns false for a non-helper
|
|
150
|
+
* command AND for the deliberately long-lived `watch-lock` watcher — matching by
|
|
151
|
+
* the full argv (`ps … command=`), so a live-parent `watch-lock` child is never
|
|
152
|
+
* mistaken for a stuck read and killed. Pure; unit-tested.
|
|
153
|
+
*/
|
|
154
|
+
export function isReapableHelperCommand(command, helperPath) {
|
|
155
|
+
if (command === helperPath)
|
|
156
|
+
return true; // bare exec, no verb — never watch-lock
|
|
157
|
+
if (!command.startsWith(`${helperPath} `))
|
|
158
|
+
return false; // not our helper
|
|
159
|
+
const firstArg = command.slice(helperPath.length + 1).trimStart().split(/\s+/)[0];
|
|
160
|
+
return firstArg !== HELPER_WATCH_LOCK_VERB;
|
|
161
|
+
}
|
|
137
162
|
/** Module-state for the two-sweep stuck-parent debounce. */
|
|
138
163
|
let stuckParentCandidates = new Map();
|
|
139
164
|
/** Test seam: reset the persisted candidate state. */
|
|
@@ -178,9 +203,11 @@ export function reapOrphanedKeychainProcesses() {
|
|
|
178
203
|
if (!parsed)
|
|
179
204
|
continue;
|
|
180
205
|
const { pid, ppid, elapsedSec, command } = parsed;
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
206
|
+
// Reap-eligible = the helper binary running a short-lived keychain verb. The
|
|
207
|
+
// full-argv match excludes the deliberately long-lived `watch-lock` watcher,
|
|
208
|
+
// whose live-parent child would otherwise be killed as if it were stuck
|
|
209
|
+
// (RUSH-2232 — that silently disabled auto-lock-on-sleep).
|
|
210
|
+
const isHelper = isReapableHelperCommand(command, helperPath);
|
|
184
211
|
rows.push({ pid, ppid, elapsedSec, isHelper, startTime: null });
|
|
185
212
|
}
|
|
186
213
|
const rowByPid = new Map(rows.map((r) => [r.pid, r]));
|
|
@@ -126,6 +126,10 @@ export interface ActiveSession {
|
|
|
126
126
|
*/
|
|
127
127
|
lastActivityMs?: number;
|
|
128
128
|
status: ActiveStatus;
|
|
129
|
+
/** Indexed launch origin, backfilled by the sessions command for JSON consumers. */
|
|
130
|
+
origin?: 'cli' | 'routine';
|
|
131
|
+
/** Routine definition name when origin is `routine`. */
|
|
132
|
+
routineName?: string;
|
|
129
133
|
/**
|
|
130
134
|
* Foreground/background presence for the detach/attach model:
|
|
131
135
|
* `attached` — live interactive TUI you're watching;
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
* BM25 ranking, and label-first search for /rename'd sessions.
|
|
8
8
|
*/
|
|
9
9
|
import Database from '../sqlite.js';
|
|
10
|
-
import type { SessionAgentId, SessionMeta } from './types.js';
|
|
10
|
+
import type { SessionAgentId, SessionEvent, SessionMeta } from './types.js';
|
|
11
11
|
import { type IndexedToolCall } from './tool-calls.js';
|
|
12
12
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
13
13
|
* assert against the constant instead of hardcoding a number that every bump
|
|
14
14
|
* then has to chase (docs/05-sessions.md calls the constant the source of truth). */
|
|
15
|
-
export declare const SCHEMA_VERSION =
|
|
15
|
+
export declare const SCHEMA_VERSION = 36;
|
|
16
16
|
/**
|
|
17
17
|
* Bump to force `agents sessions backfill resources` to re-derive every
|
|
18
18
|
* session's skill/slash-command tallies on its next run (resource_scan_ledger
|
|
@@ -26,7 +26,7 @@ export declare const RESOURCE_INDEX_VERSION = 1;
|
|
|
26
26
|
* re-derives on the next `agents insights` instead of silently reporting stale
|
|
27
27
|
* numbers alongside fresh ones. Same role as RESOURCE_INDEX_VERSION.
|
|
28
28
|
*/
|
|
29
|
-
export declare const INSIGHTS_EXTRACTOR_VERSION =
|
|
29
|
+
export declare const INSIGHTS_EXTRACTOR_VERSION = 4;
|
|
30
30
|
/** Raw row shape returned from the sessions table. */
|
|
31
31
|
export interface SessionRow {
|
|
32
32
|
id: string;
|
|
@@ -149,6 +149,23 @@ export interface FtsOptimizeResult {
|
|
|
149
149
|
* the DB file; run VACUUM (with the daemon stopped) to return it to the OS.
|
|
150
150
|
*/
|
|
151
151
|
export declare function optimizeSessionSearchIndex(): FtsOptimizeResult[];
|
|
152
|
+
/**
|
|
153
|
+
* Keep the FTS indexes from degrading on the normal scan path.
|
|
154
|
+
*
|
|
155
|
+
* `optimizeSessionSearchIndex` is the full, unbounded compaction behind
|
|
156
|
+
* `agents sessions optimize`. Leaving it as the ONLY compaction meant the index
|
|
157
|
+
* degraded until a human happened to run that command, which is how
|
|
158
|
+
* `tool_call_text_data` reached gigabytes for tens of MB of content. This is the
|
|
159
|
+
* automatic counterpart: bounded, threshold-gated, and safe to call after every
|
|
160
|
+
* batch of writes. Non-destructive — merging never changes what is searchable.
|
|
161
|
+
*
|
|
162
|
+
* Returns one result per table it actually merged (empty when every table is
|
|
163
|
+
* under the threshold, which is the common case on a warm index).
|
|
164
|
+
*/
|
|
165
|
+
export declare function maintainSessionSearchIndex(db?: Database.Database, options?: {
|
|
166
|
+
segmentThreshold?: number;
|
|
167
|
+
mergePages?: number;
|
|
168
|
+
}): FtsOptimizeResult[];
|
|
152
169
|
/**
|
|
153
170
|
* Try to claim the right to run the incremental scan. Returns true if this
|
|
154
171
|
* process should proceed with scanning, false if another live process is
|
|
@@ -245,6 +262,7 @@ export declare function upsertSessionsBatch(entries: Array<{
|
|
|
245
262
|
scan?: ScanStamp;
|
|
246
263
|
parserState?: string;
|
|
247
264
|
contentText?: string;
|
|
265
|
+
events?: SessionEvent[];
|
|
248
266
|
toolCalls?: IndexedToolCall[];
|
|
249
267
|
toolScan?: ScanStamp;
|
|
250
268
|
toolIndexMode?: 'replace' | 'append';
|