@phnx-labs/agents-cli 1.20.64 → 1.20.66
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 +51 -3
- package/README.md +156 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.d.ts +48 -0
- package/dist/commands/exec.js +159 -49
- package/dist/commands/feed.js +25 -11
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +748 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/plugins.js +28 -7
- package/dist/commands/routines.js +23 -2
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-browser.d.ts +82 -0
- package/dist/commands/sessions-browser.js +320 -0
- package/dist/commands/sessions-export.js +8 -3
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +157 -10
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +150 -0
- package/dist/commands/ssh.js +54 -3
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +26 -0
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +35 -14
- package/dist/lib/agents.d.ts +46 -0
- package/dist/lib/agents.js +121 -3
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.js +5 -1
- package/dist/lib/crabbox/runtimes.js +11 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +62 -19
- package/dist/lib/devices/connect.d.ts +18 -1
- package/dist/lib/devices/connect.js +10 -2
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/known-hosts.d.ts +62 -0
- package/dist/lib/devices/known-hosts.js +137 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +66 -20
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +79 -4
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +205 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +45 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/picker.d.ts +53 -0
- package/dist/lib/picker.js +214 -1
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.d.ts +31 -1
- package/dist/lib/plugins.js +175 -15
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +19 -0
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +102 -9
- package/dist/lib/secrets/agent.d.ts +83 -10
- package/dist/lib/secrets/agent.js +237 -70
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/filestore.d.ts +9 -0
- package/dist/lib/secrets/filestore.js +21 -8
- package/dist/lib/secrets/index.d.ts +7 -0
- package/dist/lib/secrets/index.js +26 -6
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +14 -0
- package/dist/lib/session/bundle.js +12 -1
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/share/capture.d.ts +29 -0
- package/dist/lib/share/capture.js +140 -0
- package/dist/lib/share/config.d.ts +35 -0
- package/dist/lib/share/config.js +100 -0
- package/dist/lib/share/og.d.ts +25 -0
- package/dist/lib/share/og.js +84 -0
- package/dist/lib/share/provision.d.ts +10 -0
- package/dist/lib/share/provision.js +91 -0
- package/dist/lib/share/publish.d.ts +57 -0
- package/dist/lib/share/publish.js +145 -0
- package/dist/lib/share/worker-template.d.ts +2 -0
- package/dist/lib/share/worker-template.js +82 -0
- package/dist/lib/shims.d.ts +13 -0
- package/dist/lib/shims.js +42 -2
- package/dist/lib/ssh-exec.d.ts +24 -0
- package/dist/lib/ssh-exec.js +15 -3
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +86 -7
- package/dist/lib/startup/command-registry.d.ts +3 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +36 -1
- package/dist/lib/usage.js +7 -5
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -1
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* macOS only: Linux libsecret has no biometry prompt, so there's nothing to
|
|
24
24
|
* deduplicate — every entry point here no-ops off darwin.
|
|
25
25
|
*/
|
|
26
|
+
import * as net from 'net';
|
|
26
27
|
import type { SecretsBundle } from './bundles.js';
|
|
27
28
|
/** Default lifetime of an unlocked bundle when `--ttl` is not given. */
|
|
28
29
|
export declare const DEFAULT_TTL_MS: number;
|
|
@@ -74,6 +75,13 @@ export interface AgentStatusEntry {
|
|
|
74
75
|
expiresAt: number;
|
|
75
76
|
keyCount: number;
|
|
76
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Read the current broker capability token, or null if none is present. Clients
|
|
80
|
+
* read it fresh per request and attach it to every non-ping command; a broker
|
|
81
|
+
* restart mints a new token, so a stale read simply fails authorization and the
|
|
82
|
+
* caller falls back to a direct keychain read (soft, never a hard error).
|
|
83
|
+
*/
|
|
84
|
+
export declare function readAgentToken(): string | null;
|
|
77
85
|
/** True if a legacy standalone-broker launchd plist is still installed. */
|
|
78
86
|
export declare function secretsAgentServiceInstalled(): boolean;
|
|
79
87
|
/**
|
|
@@ -97,17 +105,21 @@ export type Request = {
|
|
|
97
105
|
} | {
|
|
98
106
|
cmd: 'get';
|
|
99
107
|
name: string;
|
|
108
|
+
token?: string;
|
|
100
109
|
} | {
|
|
101
110
|
cmd: 'load';
|
|
102
111
|
name: string;
|
|
103
112
|
bundle: SecretsBundle;
|
|
104
113
|
env: Record<string, string>;
|
|
105
114
|
ttlMs: number;
|
|
115
|
+
token?: string;
|
|
106
116
|
} | {
|
|
107
117
|
cmd: 'lock';
|
|
108
118
|
name?: string;
|
|
119
|
+
token?: string;
|
|
109
120
|
} | {
|
|
110
121
|
cmd: 'status';
|
|
122
|
+
token?: string;
|
|
111
123
|
};
|
|
112
124
|
export type Response = {
|
|
113
125
|
ok: true;
|
|
@@ -164,6 +176,28 @@ export declare function handleAgentRequest(store: Map<string, StoredBundle>, req
|
|
|
164
176
|
* has direct regression coverage (the inline stdout handler isn't unit-testable).
|
|
165
177
|
*/
|
|
166
178
|
export declare function shouldWipeOnWatchEvent(chunk: string): boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Authorization gate applied to every request BEFORE handleAgentRequest touches
|
|
181
|
+
* the store (RUSH-1760). A bare same-UID socket connection is no longer trusted
|
|
182
|
+
* to load/get arbitrary bundle env: each command except the liveness `ping` must
|
|
183
|
+
* carry the per-broker capability token, which lives in a 0600 file inside the
|
|
184
|
+
* 0700 agent dir and so is readable only by the UID that owns the broker.
|
|
185
|
+
*
|
|
186
|
+
* Fail closed: a missing/empty expected token (no token file) rejects every
|
|
187
|
+
* command but ping. `ping` stays unauthenticated — it exposes only the protocol
|
|
188
|
+
* and cli version, and clients need it to detect a reachable broker before they
|
|
189
|
+
* have any reason to read the token. Pure + exported for direct unit testing.
|
|
190
|
+
*/
|
|
191
|
+
export declare function isRequestAuthorized(req: Request, expectedToken: string | null): boolean;
|
|
192
|
+
type BrokerConnectionHandler = (conn: net.Socket) => void;
|
|
193
|
+
/**
|
|
194
|
+
* Build the socket `connection` handler shared by both brokers (standalone and
|
|
195
|
+
* daemon-hosted): newline-framed JSON in, one response line out, with the
|
|
196
|
+
* authorization gate (isRequestAuthorized) applied before `handle`. `token`
|
|
197
|
+
* resolves the currently-expected capability token per request so a token
|
|
198
|
+
* rotation on broker restart is picked up without rebuilding the handler.
|
|
199
|
+
*/
|
|
200
|
+
export declare function makeConnectionHandler(handle: (req: Request) => Response, token: () => string | null): BrokerConnectionHandler;
|
|
167
201
|
/**
|
|
168
202
|
* Run the broker in the foreground. Spawned detached by ensureAgentRunning via
|
|
169
203
|
* `agents secrets _agent-run`. Holds the store in memory, serves the socket,
|
|
@@ -207,6 +241,15 @@ export declare function agentGetSync(name: string): {
|
|
|
207
241
|
bundle: SecretsBundle;
|
|
208
242
|
env: Record<string, string>;
|
|
209
243
|
} | null;
|
|
244
|
+
/**
|
|
245
|
+
* Synchronous liveness check: is a broker actually LISTENING and answering (not
|
|
246
|
+
* just a lingering socket file)? Used to decide whether the auto-cache may take
|
|
247
|
+
* the synchronous warm path — a dead broker whose socket outlived it (crash,
|
|
248
|
+
* OOM, version-skew teardown) must NOT drag a foreground read through the
|
|
249
|
+
* worker's 20s cold-start budget. A stale socket refuses instantly, so this is
|
|
250
|
+
* fast in both the alive and dead cases. macOS only.
|
|
251
|
+
*/
|
|
252
|
+
export declare function agentReachableSync(): boolean;
|
|
210
253
|
/**
|
|
211
254
|
* Synchronously evict one bundle from the broker. Called after a mutating
|
|
212
255
|
* keychain write (add / rotate / remove / rename / delete) so the broker never
|
|
@@ -237,25 +280,54 @@ export declare function agentAutoLoadMetaSync(nameSetHash: string, bundles: Secr
|
|
|
237
280
|
* per ~7d), so auto-caching is ON by default; opt out with
|
|
238
281
|
* `secrets.agent.auto: false`. Best-effort; an unreadable meta reads as on. */
|
|
239
282
|
export declare function secretsAgentAutoEnabled(): boolean;
|
|
283
|
+
/** Minimum / maximum bounds for the configurable hold window. A too-small value
|
|
284
|
+
* would defeat the broker (constant re-prompts); a too-large one pins secrets in
|
|
285
|
+
* memory far longer than intended. */
|
|
286
|
+
export declare const MIN_HOLD_MS: number;
|
|
287
|
+
export declare const MAX_HOLD_MS: number;
|
|
288
|
+
/**
|
|
289
|
+
* How long an unlocked / auto-cached bundle is held before the next read
|
|
290
|
+
* re-prompts. Defaults to DEFAULT_TTL_MS (7d); override with
|
|
291
|
+
* `secrets.agent.holdMs` (milliseconds) in agents.yaml — e.g. 86400000 for a 24h
|
|
292
|
+
* cap. Clamped to [MIN_HOLD_MS, MAX_HOLD_MS] so a typo can neither disable the
|
|
293
|
+
* hold nor pin a secret in memory indefinitely. Best-effort: an unreadable or
|
|
294
|
+
* non-numeric value falls back to the 7d default. Pure except for the meta read.
|
|
295
|
+
*/
|
|
296
|
+
export declare function secretsHoldMs(): number;
|
|
297
|
+
/** Pure clamp for a configured `holdMs`: a positive finite number is bounded to
|
|
298
|
+
* [MIN_HOLD_MS, MAX_HOLD_MS]; anything else (absent, 0, negative, NaN, non-number)
|
|
299
|
+
* falls back to the 7d default. Exported for direct unit testing. */
|
|
300
|
+
export declare function clampHoldMs(v: unknown): number;
|
|
240
301
|
/**
|
|
241
302
|
* Fire-and-forget: populate the broker with a freshly-resolved bundle so the
|
|
242
303
|
* NEXT process reads it without a prompt. Used by the auto-cache path after a
|
|
243
|
-
* real keychain read of a `daily`-policy bundle
|
|
244
|
-
*
|
|
245
|
-
*
|
|
304
|
+
* real keychain read of a `daily`-policy bundle, so the NEXT concurrent read is
|
|
305
|
+
* silent. Env travels over stdin, never argv.
|
|
306
|
+
*
|
|
307
|
+
* Reliability (this is what makes `daily` actually "stick"): when a broker is
|
|
308
|
+
* ALREADY listening, warm it SYNCHRONOUSLY with a bounded wait so the bundle is
|
|
309
|
+
* held by the time this process exits. The old detached-only path lost the race
|
|
310
|
+
* under load — a short-lived reader (`agents secrets export`, a release-script
|
|
311
|
+
* loop) exited before the unref'd worker connected, so the cache silently never
|
|
312
|
+
* populated and every read re-prompted despite the `daily` policy. Only when the
|
|
313
|
+
* broker must COLD-START (no socket yet) do we fall back to the detached worker,
|
|
314
|
+
* so a first-ever read never blocks on a multi-second broker boot.
|
|
246
315
|
*
|
|
247
|
-
* The worker reuses the robust `ensureAgentRunning` path (spawn-then-ping
|
|
248
|
-
*
|
|
249
|
-
* broker is itself a cold-starting full CLI and can take several seconds to bind
|
|
250
|
-
* the socket, so a short fixed budget would give up before it's ready and the
|
|
251
|
-
* cache would silently never populate. Best-effort; never throws. macOS only.
|
|
316
|
+
* The worker reuses the robust `ensureAgentRunning` path (spawn-then-ping) rather
|
|
317
|
+
* than a tight inline retry loop. Best-effort; never throws. macOS only.
|
|
252
318
|
*/
|
|
253
319
|
export declare function agentAutoLoadSync(name: string, bundle: SecretsBundle, env: Record<string, string>, ttlMs: number): void;
|
|
254
320
|
/**
|
|
255
321
|
* Body of the hidden `secrets _agent-load` worker. Reads one `{name, bundle,
|
|
256
322
|
* env, ttlMs}` payload from stdin, ensures the broker is up (robust, generous
|
|
257
|
-
* budget), and loads the bundle into it.
|
|
258
|
-
*
|
|
323
|
+
* budget), and loads the bundle into it.
|
|
324
|
+
*
|
|
325
|
+
* Exit code is load-truthful: 0 ONLY when the bundle was actually loaded into a
|
|
326
|
+
* reachable broker; non-zero on any failure (malformed payload, broker couldn't
|
|
327
|
+
* be brought up, or the load transport failed). The synchronous caller
|
|
328
|
+
* (agentAutoLoadSync) relies on this to decide whether to skip the detached
|
|
329
|
+
* fallback — a bare "process exited 0" would otherwise be a false-positive
|
|
330
|
+
* success that silently reintroduces the very re-prompt storm this path fixes.
|
|
259
331
|
*/
|
|
260
332
|
export declare function runAgentLoadFromStdin(): Promise<void>;
|
|
261
333
|
/** Store a resolved bundle in the broker. Returns false on transport failure. */
|
|
@@ -287,3 +359,4 @@ export declare function agentPing(): Promise<{
|
|
|
287
359
|
* gets starved under heavy load, so it's last.
|
|
288
360
|
*/
|
|
289
361
|
export declare function ensureAgentRunning(timeoutMs?: number): Promise<boolean>;
|
|
362
|
+
export {};
|
|
@@ -27,6 +27,7 @@ import * as net from 'net';
|
|
|
27
27
|
import * as fs from 'fs';
|
|
28
28
|
import * as os from 'os';
|
|
29
29
|
import * as path from 'path';
|
|
30
|
+
import { randomBytes } from 'crypto';
|
|
30
31
|
import { spawn, spawnSync, execFileSync } from 'child_process';
|
|
31
32
|
import { getHelpersDir, readMeta } from '../state.js';
|
|
32
33
|
import { isAlive } from '../platform/process.js';
|
|
@@ -109,6 +110,42 @@ function socketPath() {
|
|
|
109
110
|
function pidPath() {
|
|
110
111
|
return path.join(agentDir(), 'agent.pid');
|
|
111
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Path of the per-broker capability token. It lives in the 0700 agent dir and is
|
|
115
|
+
* written 0600, so only the same UID that owns the broker can read it — the file
|
|
116
|
+
* permission IS the authorization boundary. See isRequestAuthorized.
|
|
117
|
+
*/
|
|
118
|
+
function tokenPath() {
|
|
119
|
+
return path.join(agentDir(), 'agent.token');
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Read the current broker capability token, or null if none is present. Clients
|
|
123
|
+
* read it fresh per request and attach it to every non-ping command; a broker
|
|
124
|
+
* restart mints a new token, so a stale read simply fails authorization and the
|
|
125
|
+
* caller falls back to a direct keychain read (soft, never a hard error).
|
|
126
|
+
*/
|
|
127
|
+
export function readAgentToken() {
|
|
128
|
+
try {
|
|
129
|
+
const t = fs.readFileSync(tokenPath(), 'utf-8').trim();
|
|
130
|
+
return t.length > 0 ? t : null;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Mint + persist a fresh capability token (0600) at socket-bind time. Only the
|
|
137
|
+
* process that actually binds the socket calls this, so a losing starter never
|
|
138
|
+
* clobbers the live owner's token. Returns the token for in-memory comparison. */
|
|
139
|
+
function writeAgentToken() {
|
|
140
|
+
const token = randomBytes(32).toString('hex');
|
|
141
|
+
const fp = tokenPath();
|
|
142
|
+
fs.writeFileSync(fp, token, { mode: 0o600 });
|
|
143
|
+
try {
|
|
144
|
+
fs.chmodSync(fp, 0o600);
|
|
145
|
+
}
|
|
146
|
+
catch { /* dir 0700 already gates it */ }
|
|
147
|
+
return token;
|
|
148
|
+
}
|
|
112
149
|
/**
|
|
113
150
|
* Argv for re-invoking THIS cli with a hidden subcommand, so a side-by-side dev
|
|
114
151
|
* build spawns its own helpers rather than the registry-installed one. We always
|
|
@@ -272,6 +309,60 @@ export function handleAgentRequest(store, req, now = Date.now()) {
|
|
|
272
309
|
export function shouldWipeOnWatchEvent(chunk) {
|
|
273
310
|
return /\bSLEEP\b/.test(chunk);
|
|
274
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Authorization gate applied to every request BEFORE handleAgentRequest touches
|
|
314
|
+
* the store (RUSH-1760). A bare same-UID socket connection is no longer trusted
|
|
315
|
+
* to load/get arbitrary bundle env: each command except the liveness `ping` must
|
|
316
|
+
* carry the per-broker capability token, which lives in a 0600 file inside the
|
|
317
|
+
* 0700 agent dir and so is readable only by the UID that owns the broker.
|
|
318
|
+
*
|
|
319
|
+
* Fail closed: a missing/empty expected token (no token file) rejects every
|
|
320
|
+
* command but ping. `ping` stays unauthenticated — it exposes only the protocol
|
|
321
|
+
* and cli version, and clients need it to detect a reachable broker before they
|
|
322
|
+
* have any reason to read the token. Pure + exported for direct unit testing.
|
|
323
|
+
*/
|
|
324
|
+
export function isRequestAuthorized(req, expectedToken) {
|
|
325
|
+
if (req.cmd === 'ping')
|
|
326
|
+
return true;
|
|
327
|
+
if (!expectedToken)
|
|
328
|
+
return false;
|
|
329
|
+
return req.token === expectedToken;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Build the socket `connection` handler shared by both brokers (standalone and
|
|
333
|
+
* daemon-hosted): newline-framed JSON in, one response line out, with the
|
|
334
|
+
* authorization gate (isRequestAuthorized) applied before `handle`. `token`
|
|
335
|
+
* resolves the currently-expected capability token per request so a token
|
|
336
|
+
* rotation on broker restart is picked up without rebuilding the handler.
|
|
337
|
+
*/
|
|
338
|
+
export function makeConnectionHandler(handle, token) {
|
|
339
|
+
return (conn) => {
|
|
340
|
+
conn.setEncoding('utf-8');
|
|
341
|
+
let buf = '';
|
|
342
|
+
conn.on('data', (chunk) => {
|
|
343
|
+
buf += chunk;
|
|
344
|
+
let nl;
|
|
345
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
346
|
+
const line = buf.slice(0, nl);
|
|
347
|
+
buf = buf.slice(nl + 1);
|
|
348
|
+
if (!line.trim())
|
|
349
|
+
continue;
|
|
350
|
+
let resp;
|
|
351
|
+
try {
|
|
352
|
+
const req = JSON.parse(line);
|
|
353
|
+
resp = isRequestAuthorized(req, token())
|
|
354
|
+
? handle(req)
|
|
355
|
+
: { ok: false, error: 'unauthorized' };
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
resp = { ok: false, error: err.message };
|
|
359
|
+
}
|
|
360
|
+
conn.write(JSON.stringify(resp) + '\n');
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
conn.on('error', () => { });
|
|
364
|
+
};
|
|
365
|
+
}
|
|
275
366
|
/**
|
|
276
367
|
* Bind the shared broker socket without stealing it from another live owner.
|
|
277
368
|
* Both the standalone service and daemon-hosted broker use this single path so
|
|
@@ -293,6 +384,13 @@ async function bindBrokerSocket(sock, onConnection) {
|
|
|
293
384
|
fs.chmodSync(sock, 0o600);
|
|
294
385
|
}
|
|
295
386
|
catch { /* dir 0700 already gates it */ }
|
|
387
|
+
// Mint the capability token here — the one moment this process is the
|
|
388
|
+
// confirmed socket owner — so a losing starter never clobbers the live
|
|
389
|
+
// owner's token (RUSH-1760).
|
|
390
|
+
try {
|
|
391
|
+
writeAgentToken();
|
|
392
|
+
}
|
|
393
|
+
catch { /* dir 0700 gates the socket regardless */ }
|
|
296
394
|
resolve(server);
|
|
297
395
|
});
|
|
298
396
|
});
|
|
@@ -428,29 +526,7 @@ export async function runSecretsAgent(opts = {}) {
|
|
|
428
526
|
emptySince = Date.now();
|
|
429
527
|
return resp;
|
|
430
528
|
};
|
|
431
|
-
const onConnection = (
|
|
432
|
-
conn.setEncoding('utf-8');
|
|
433
|
-
let buf = '';
|
|
434
|
-
conn.on('data', (chunk) => {
|
|
435
|
-
buf += chunk;
|
|
436
|
-
let nl;
|
|
437
|
-
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
438
|
-
const line = buf.slice(0, nl);
|
|
439
|
-
buf = buf.slice(nl + 1);
|
|
440
|
-
if (!line.trim())
|
|
441
|
-
continue;
|
|
442
|
-
let resp;
|
|
443
|
-
try {
|
|
444
|
-
resp = handle(JSON.parse(line));
|
|
445
|
-
}
|
|
446
|
-
catch (err) {
|
|
447
|
-
resp = { ok: false, error: err.message };
|
|
448
|
-
}
|
|
449
|
-
conn.write(JSON.stringify(resp) + '\n');
|
|
450
|
-
}
|
|
451
|
-
});
|
|
452
|
-
conn.on('error', () => { });
|
|
453
|
-
};
|
|
529
|
+
const onConnection = makeConnectionHandler(handle, readAgentToken);
|
|
454
530
|
let server = null;
|
|
455
531
|
do {
|
|
456
532
|
try {
|
|
@@ -558,29 +634,7 @@ export async function startHostedBroker() {
|
|
|
558
634
|
const store = new Map();
|
|
559
635
|
const sock = socketPath(); // agentDir() creates the 0700 dir as a side effect
|
|
560
636
|
const handle = (req) => handleAgentRequest(store, req);
|
|
561
|
-
const onConn = (
|
|
562
|
-
conn.setEncoding('utf-8');
|
|
563
|
-
let buf = '';
|
|
564
|
-
conn.on('data', (chunk) => {
|
|
565
|
-
buf += chunk;
|
|
566
|
-
let nl;
|
|
567
|
-
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
568
|
-
const line = buf.slice(0, nl);
|
|
569
|
-
buf = buf.slice(nl + 1);
|
|
570
|
-
if (!line.trim())
|
|
571
|
-
continue;
|
|
572
|
-
let resp;
|
|
573
|
-
try {
|
|
574
|
-
resp = handle(JSON.parse(line));
|
|
575
|
-
}
|
|
576
|
-
catch (err) {
|
|
577
|
-
resp = { ok: false, error: err.message };
|
|
578
|
-
}
|
|
579
|
-
conn.write(JSON.stringify(resp) + '\n');
|
|
580
|
-
}
|
|
581
|
-
});
|
|
582
|
-
conn.on('error', () => { });
|
|
583
|
-
};
|
|
637
|
+
const onConn = makeConnectionHandler(handle, readAgentToken);
|
|
584
638
|
const server = await bindBrokerSocket(sock, onConn);
|
|
585
639
|
if (!server)
|
|
586
640
|
return null;
|
|
@@ -632,6 +686,9 @@ export async function startHostedBroker() {
|
|
|
632
686
|
/** Open the socket, send one request, resolve the one response. Async path —
|
|
633
687
|
* used by the unlock/lock/status commands, which already run in async actions. */
|
|
634
688
|
function request(req, timeoutMs = 2000) {
|
|
689
|
+
// Attach the capability token to every command except ping (the auth gate;
|
|
690
|
+
// RUSH-1760). ping stays tokenless so a client can probe reachability first.
|
|
691
|
+
const authedReq = req.cmd === 'ping' ? req : { ...req, token: readAgentToken() ?? undefined };
|
|
635
692
|
return new Promise((resolve) => {
|
|
636
693
|
const conn = net.createConnection(socketPath());
|
|
637
694
|
let buf = '';
|
|
@@ -649,7 +706,7 @@ function request(req, timeoutMs = 2000) {
|
|
|
649
706
|
};
|
|
650
707
|
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
651
708
|
conn.on('error', () => finish(null));
|
|
652
|
-
conn.on('connect', () => conn.write(JSON.stringify(
|
|
709
|
+
conn.on('connect', () => conn.write(JSON.stringify(authedReq) + '\n'));
|
|
653
710
|
conn.setEncoding('utf-8');
|
|
654
711
|
conn.on('data', (chunk) => {
|
|
655
712
|
buf += chunk;
|
|
@@ -679,14 +736,15 @@ export function agentSocketExists() {
|
|
|
679
736
|
* -e: [execPath, <socket>, <name>].
|
|
680
737
|
*/
|
|
681
738
|
const SYNC_GET_PROGRAM = `
|
|
682
|
-
const net = require('net');
|
|
683
|
-
const sock = process.argv[1], name = process.argv[2];
|
|
739
|
+
const net = require('net'), fs = require('fs');
|
|
740
|
+
const sock = process.argv[1], name = process.argv[2], tokenPath = process.argv[3];
|
|
741
|
+
let token; try { token = fs.readFileSync(tokenPath, 'utf-8').trim() || undefined; } catch (e) {}
|
|
684
742
|
const c = net.createConnection(sock);
|
|
685
743
|
let buf = '';
|
|
686
744
|
const miss = () => { try { c.destroy(); } catch (e) {} process.exit(3); };
|
|
687
745
|
const timer = setTimeout(miss, 2000);
|
|
688
746
|
c.on('error', miss);
|
|
689
|
-
c.on('connect', () => c.write(JSON.stringify({ cmd: 'get', name }) + '\\n'));
|
|
747
|
+
c.on('connect', () => c.write(JSON.stringify({ cmd: 'get', name, token }) + '\\n'));
|
|
690
748
|
c.setEncoding('utf-8');
|
|
691
749
|
c.on('data', (d) => {
|
|
692
750
|
buf += d;
|
|
@@ -707,7 +765,7 @@ c.on('data', (d) => {
|
|
|
707
765
|
export function agentGetSync(name) {
|
|
708
766
|
if (!agentSocketExists())
|
|
709
767
|
return null;
|
|
710
|
-
const r = spawnSync(process.execPath, ['-e', SYNC_GET_PROGRAM, socketPath(), name], {
|
|
768
|
+
const r = spawnSync(process.execPath, ['-e', SYNC_GET_PROGRAM, socketPath(), name, tokenPath()], {
|
|
711
769
|
encoding: 'utf-8',
|
|
712
770
|
timeout: 3000,
|
|
713
771
|
});
|
|
@@ -723,6 +781,46 @@ export function agentGetSync(name) {
|
|
|
723
781
|
return null;
|
|
724
782
|
}
|
|
725
783
|
}
|
|
784
|
+
/** Inline node program for a synchronous liveness ping. Connects, sends one
|
|
785
|
+
* `{cmd:'ping'}`, exits 0 iff a valid ping response comes back, else 3. A stale
|
|
786
|
+
* socket file with no listener refuses the connection immediately, so this
|
|
787
|
+
* fast-fails without riding any cold-start logic. argv after -e: [execPath, <socket>]. */
|
|
788
|
+
const SYNC_PING_PROGRAM = `
|
|
789
|
+
const net = require('net');
|
|
790
|
+
const sock = process.argv[1];
|
|
791
|
+
const c = net.createConnection(sock);
|
|
792
|
+
let buf = '';
|
|
793
|
+
const dead = () => { try { c.destroy(); } catch (e) {} process.exit(3); };
|
|
794
|
+
const timer = setTimeout(dead, 700);
|
|
795
|
+
c.on('error', dead);
|
|
796
|
+
c.on('connect', () => c.write(JSON.stringify({ cmd: 'ping' }) + '\\n'));
|
|
797
|
+
c.setEncoding('utf-8');
|
|
798
|
+
c.on('data', (d) => {
|
|
799
|
+
buf += d;
|
|
800
|
+
const nl = buf.indexOf('\\n');
|
|
801
|
+
if (nl < 0) return;
|
|
802
|
+
clearTimeout(timer);
|
|
803
|
+
let r; try { r = JSON.parse(buf.slice(0, nl)); } catch (e) { return dead(); }
|
|
804
|
+
try { c.destroy(); } catch (e) {}
|
|
805
|
+
process.exit(r && r.ok && r.cmd === 'ping' ? 0 : 3);
|
|
806
|
+
});
|
|
807
|
+
`;
|
|
808
|
+
/**
|
|
809
|
+
* Synchronous liveness check: is a broker actually LISTENING and answering (not
|
|
810
|
+
* just a lingering socket file)? Used to decide whether the auto-cache may take
|
|
811
|
+
* the synchronous warm path — a dead broker whose socket outlived it (crash,
|
|
812
|
+
* OOM, version-skew teardown) must NOT drag a foreground read through the
|
|
813
|
+
* worker's 20s cold-start budget. A stale socket refuses instantly, so this is
|
|
814
|
+
* fast in both the alive and dead cases. macOS only.
|
|
815
|
+
*/
|
|
816
|
+
export function agentReachableSync() {
|
|
817
|
+
if (!onDarwin())
|
|
818
|
+
return false;
|
|
819
|
+
if (!agentSocketExists())
|
|
820
|
+
return false;
|
|
821
|
+
const r = spawnSync(process.execPath, ['-e', SYNC_PING_PROGRAM, socketPath()], { timeout: 1500 });
|
|
822
|
+
return r.status === 0 && !r.error;
|
|
823
|
+
}
|
|
726
824
|
/**
|
|
727
825
|
* Inline node program for the synchronous evict path. Mirrors SYNC_GET_PROGRAM:
|
|
728
826
|
* writeBundle is synchronous and called synchronously everywhere, so a stale
|
|
@@ -731,14 +829,15 @@ export function agentGetSync(name) {
|
|
|
731
829
|
* argv after -e: [execPath, <socket>, <name>].
|
|
732
830
|
*/
|
|
733
831
|
const SYNC_LOCK_PROGRAM = `
|
|
734
|
-
const net = require('net');
|
|
735
|
-
const sock = process.argv[1], name = process.argv[2];
|
|
832
|
+
const net = require('net'), fs = require('fs');
|
|
833
|
+
const sock = process.argv[1], name = process.argv[2], tokenPath = process.argv[3];
|
|
834
|
+
let token; try { token = fs.readFileSync(tokenPath, 'utf-8').trim() || undefined; } catch (e) {}
|
|
736
835
|
const c = net.createConnection(sock);
|
|
737
836
|
let buf = '';
|
|
738
837
|
const down = () => { try { c.destroy(); } catch (e) {} process.exit(3); };
|
|
739
838
|
const timer = setTimeout(down, 2000);
|
|
740
839
|
c.on('error', down);
|
|
741
|
-
c.on('connect', () => c.write(JSON.stringify({ cmd: 'lock', name }) + '\\n'));
|
|
840
|
+
c.on('connect', () => c.write(JSON.stringify({ cmd: 'lock', name, token }) + '\\n'));
|
|
742
841
|
c.setEncoding('utf-8');
|
|
743
842
|
c.on('data', (d) => {
|
|
744
843
|
buf += d;
|
|
@@ -763,7 +862,7 @@ export function agentEvictSync(name) {
|
|
|
763
862
|
if (!agentSocketExists())
|
|
764
863
|
return;
|
|
765
864
|
try {
|
|
766
|
-
spawnSync(process.execPath, ['-e', SYNC_LOCK_PROGRAM, socketPath(), name], { timeout: 3000 });
|
|
865
|
+
spawnSync(process.execPath, ['-e', SYNC_LOCK_PROGRAM, socketPath(), name, tokenPath()], { timeout: 3000 });
|
|
767
866
|
}
|
|
768
867
|
catch { /* best-effort */ }
|
|
769
868
|
}
|
|
@@ -817,26 +916,81 @@ export function secretsAgentAutoEnabled() {
|
|
|
817
916
|
return true;
|
|
818
917
|
}
|
|
819
918
|
}
|
|
919
|
+
/** Minimum / maximum bounds for the configurable hold window. A too-small value
|
|
920
|
+
* would defeat the broker (constant re-prompts); a too-large one pins secrets in
|
|
921
|
+
* memory far longer than intended. */
|
|
922
|
+
export const MIN_HOLD_MS = 60 * 1000; // 1m
|
|
923
|
+
export const MAX_HOLD_MS = 30 * 24 * 60 * 60 * 1000; // 30d
|
|
924
|
+
/**
|
|
925
|
+
* How long an unlocked / auto-cached bundle is held before the next read
|
|
926
|
+
* re-prompts. Defaults to DEFAULT_TTL_MS (7d); override with
|
|
927
|
+
* `secrets.agent.holdMs` (milliseconds) in agents.yaml — e.g. 86400000 for a 24h
|
|
928
|
+
* cap. Clamped to [MIN_HOLD_MS, MAX_HOLD_MS] so a typo can neither disable the
|
|
929
|
+
* hold nor pin a secret in memory indefinitely. Best-effort: an unreadable or
|
|
930
|
+
* non-numeric value falls back to the 7d default. Pure except for the meta read.
|
|
931
|
+
*/
|
|
932
|
+
export function secretsHoldMs() {
|
|
933
|
+
try {
|
|
934
|
+
return clampHoldMs(readMeta().secrets?.agent?.holdMs);
|
|
935
|
+
}
|
|
936
|
+
catch {
|
|
937
|
+
return DEFAULT_TTL_MS;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
/** Pure clamp for a configured `holdMs`: a positive finite number is bounded to
|
|
941
|
+
* [MIN_HOLD_MS, MAX_HOLD_MS]; anything else (absent, 0, negative, NaN, non-number)
|
|
942
|
+
* falls back to the 7d default. Exported for direct unit testing. */
|
|
943
|
+
export function clampHoldMs(v) {
|
|
944
|
+
if (typeof v === 'number' && Number.isFinite(v) && v > 0) {
|
|
945
|
+
return Math.min(Math.max(Math.floor(v), MIN_HOLD_MS), MAX_HOLD_MS);
|
|
946
|
+
}
|
|
947
|
+
return DEFAULT_TTL_MS;
|
|
948
|
+
}
|
|
820
949
|
/**
|
|
821
950
|
* Fire-and-forget: populate the broker with a freshly-resolved bundle so the
|
|
822
951
|
* NEXT process reads it without a prompt. Used by the auto-cache path after a
|
|
823
|
-
* real keychain read of a `daily`-policy bundle
|
|
824
|
-
*
|
|
825
|
-
*
|
|
952
|
+
* real keychain read of a `daily`-policy bundle, so the NEXT concurrent read is
|
|
953
|
+
* silent. Env travels over stdin, never argv.
|
|
954
|
+
*
|
|
955
|
+
* Reliability (this is what makes `daily` actually "stick"): when a broker is
|
|
956
|
+
* ALREADY listening, warm it SYNCHRONOUSLY with a bounded wait so the bundle is
|
|
957
|
+
* held by the time this process exits. The old detached-only path lost the race
|
|
958
|
+
* under load — a short-lived reader (`agents secrets export`, a release-script
|
|
959
|
+
* loop) exited before the unref'd worker connected, so the cache silently never
|
|
960
|
+
* populated and every read re-prompted despite the `daily` policy. Only when the
|
|
961
|
+
* broker must COLD-START (no socket yet) do we fall back to the detached worker,
|
|
962
|
+
* so a first-ever read never blocks on a multi-second broker boot.
|
|
826
963
|
*
|
|
827
|
-
* The worker reuses the robust `ensureAgentRunning` path (spawn-then-ping
|
|
828
|
-
*
|
|
829
|
-
* broker is itself a cold-starting full CLI and can take several seconds to bind
|
|
830
|
-
* the socket, so a short fixed budget would give up before it's ready and the
|
|
831
|
-
* cache would silently never populate. Best-effort; never throws. macOS only.
|
|
964
|
+
* The worker reuses the robust `ensureAgentRunning` path (spawn-then-ping) rather
|
|
965
|
+
* than a tight inline retry loop. Best-effort; never throws. macOS only.
|
|
832
966
|
*/
|
|
833
967
|
export function agentAutoLoadSync(name, bundle, env, ttlMs) {
|
|
834
968
|
if (!onDarwin())
|
|
835
969
|
return;
|
|
970
|
+
const payload = JSON.stringify({ name, bundle, env, ttlMs });
|
|
971
|
+
// Broker actually LISTENING → deterministic synchronous warm (bounded; the read
|
|
972
|
+
// already paid a Touch ID, so <1s here is invisible). We gate on a real liveness
|
|
973
|
+
// ping, NOT mere socket-file existence: a broker that died leaving its socket
|
|
974
|
+
// behind (crash, OOM, or the version-skew teardown in this file) would otherwise
|
|
975
|
+
// drag this FOREGROUND read through the worker's 20s cold-start budget on every
|
|
976
|
+
// read. A dead/stale socket fails the ping fast, so we drop straight to the
|
|
977
|
+
// detached path (which does the cold-start + stale-socket cleanup off the hot
|
|
978
|
+
// path) — restoring "a dead broker costs the foreground read nothing".
|
|
979
|
+
if (agentReachableSync()) {
|
|
980
|
+
try {
|
|
981
|
+
const { cmd, args } = cliSpawn(['secrets', '_agent-load']);
|
|
982
|
+
const r = spawnSync(cmd, args, { input: payload, timeout: 3000, stdio: ['pipe', 'ignore', 'ignore'] });
|
|
983
|
+
if (!r.error && r.status === 0)
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
catch {
|
|
987
|
+
// fall through to the detached best-effort path
|
|
988
|
+
}
|
|
989
|
+
}
|
|
836
990
|
try {
|
|
837
991
|
const { cmd, args } = cliSpawn(['secrets', '_agent-load']);
|
|
838
992
|
const worker = spawn(cmd, args, { stdio: ['pipe', 'ignore', 'ignore'], detached: true });
|
|
839
|
-
worker.stdin?.write(
|
|
993
|
+
worker.stdin?.write(payload);
|
|
840
994
|
worker.stdin?.end();
|
|
841
995
|
worker.unref();
|
|
842
996
|
}
|
|
@@ -847,8 +1001,14 @@ export function agentAutoLoadSync(name, bundle, env, ttlMs) {
|
|
|
847
1001
|
/**
|
|
848
1002
|
* Body of the hidden `secrets _agent-load` worker. Reads one `{name, bundle,
|
|
849
1003
|
* env, ttlMs}` payload from stdin, ensures the broker is up (robust, generous
|
|
850
|
-
* budget), and loads the bundle into it.
|
|
851
|
-
*
|
|
1004
|
+
* budget), and loads the bundle into it.
|
|
1005
|
+
*
|
|
1006
|
+
* Exit code is load-truthful: 0 ONLY when the bundle was actually loaded into a
|
|
1007
|
+
* reachable broker; non-zero on any failure (malformed payload, broker couldn't
|
|
1008
|
+
* be brought up, or the load transport failed). The synchronous caller
|
|
1009
|
+
* (agentAutoLoadSync) relies on this to decide whether to skip the detached
|
|
1010
|
+
* fallback — a bare "process exited 0" would otherwise be a false-positive
|
|
1011
|
+
* success that silently reintroduces the very re-prompt storm this path fixes.
|
|
852
1012
|
*/
|
|
853
1013
|
export async function runAgentLoadFromStdin() {
|
|
854
1014
|
if (!onDarwin())
|
|
@@ -861,15 +1021,22 @@ export async function runAgentLoadFromStdin() {
|
|
|
861
1021
|
payload = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
|
|
862
1022
|
}
|
|
863
1023
|
catch {
|
|
864
|
-
|
|
1024
|
+
process.exitCode = 1; // malformed payload — nothing loaded
|
|
1025
|
+
return;
|
|
865
1026
|
}
|
|
866
|
-
if (!payload || !payload.name || !payload.bundle || !payload.env)
|
|
1027
|
+
if (!payload || !payload.name || !payload.bundle || !payload.env) {
|
|
1028
|
+
process.exitCode = 1;
|
|
867
1029
|
return;
|
|
1030
|
+
}
|
|
868
1031
|
// Generous budget: the broker is a cold-starting full CLI; under load it can
|
|
869
1032
|
// take several seconds to bind. We're detached, so waiting costs nothing.
|
|
870
|
-
if (!(await ensureAgentRunning(20000)))
|
|
1033
|
+
if (!(await ensureAgentRunning(20000))) {
|
|
1034
|
+
process.exitCode = 1; // broker couldn't be brought up — did NOT load
|
|
871
1035
|
return;
|
|
872
|
-
|
|
1036
|
+
}
|
|
1037
|
+
const loaded = await agentLoad(payload.name, payload.bundle, payload.env, payload.ttlMs ?? DEFAULT_TTL_MS);
|
|
1038
|
+
if (!loaded)
|
|
1039
|
+
process.exitCode = 1; // transport failed — did NOT load
|
|
873
1040
|
}
|
|
874
1041
|
/** Store a resolved bundle in the broker. Returns false on transport failure. */
|
|
875
1042
|
export async function agentLoad(name, bundle, env, ttlMs) {
|
|
@@ -221,6 +221,32 @@ export declare function assertRemoteBundleFlagsUnsupported(bundleName: string, h
|
|
|
221
221
|
allowExpiredFlag: string;
|
|
222
222
|
}): void;
|
|
223
223
|
export declare function resolveBundleEnv(bundle: SecretsBundle, _opts?: ResolveBundleOptions): Record<string, string>;
|
|
224
|
+
/**
|
|
225
|
+
* True when the current process is a background / non-interactive context that
|
|
226
|
+
* must NEVER raise a Keychain biometry prompt on the interactive user's screen —
|
|
227
|
+
* a prompt nobody is watching. Two signals, either sufficient:
|
|
228
|
+
* - `AGENTS_RUNTIME` is `headless` or `teams` (set on the child env by
|
|
229
|
+
* `agents run --headless`, scheduled routines, and teammates — see
|
|
230
|
+
* exec.ts:resolveInteractive, runner.ts, teams/agents.ts).
|
|
231
|
+
* - neither stdin nor stdout is a TTY (a detached/backgrounded task whose
|
|
232
|
+
* stdio is redirected to a log — e.g. a release script run in the
|
|
233
|
+
* background as `( ... ) >log 2>&1 </dev/null`).
|
|
234
|
+
* `AGENTS_SECRETS_NO_PROMPT=1` forces headless-safe; `=0` force-allows a prompt
|
|
235
|
+
* even in a non-TTY context. An interactive `eval "$(agents secrets export X)"`
|
|
236
|
+
* keeps its terminal stdin, so it is NOT classified headless and still prompts.
|
|
237
|
+
*
|
|
238
|
+
* Only **macOS keychain** reads pop an interactive Touch ID sheet — the secrets
|
|
239
|
+
* broker itself is a no-op off darwin (see agent.ts), and libsecret (Linux) /
|
|
240
|
+
* the Windows credential store resolve without any prompt. So off-darwin this
|
|
241
|
+
* ALWAYS returns false: forcing broker-only there would break every headless
|
|
242
|
+
* Linux/Windows read (CI, `agents run --headless`, routines, the Linux-driven
|
|
243
|
+
* release flow) for no benefit — there is no prompt to suppress.
|
|
244
|
+
*
|
|
245
|
+
* A read in a macOS headless context resolves broker-only (agentOnly) and fails
|
|
246
|
+
* fast with an actionable error instead of hijacking Touch ID. This generalizes
|
|
247
|
+
* the per-caller pattern already used by the daemon (daemon.ts:readDaemonClaudeOAuthToken).
|
|
248
|
+
*/
|
|
249
|
+
export declare function isHeadlessSecretsContext(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
|
224
250
|
/**
|
|
225
251
|
* Read a bundle's metadata AND resolve its env in a single Touch ID prompt.
|
|
226
252
|
*
|