@phnx-labs/agents-cli 1.22.66 → 1.22.68
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 +26 -0
- package/README.md +7 -2
- package/dist/bootstrap.js +3 -3
- package/dist/commands/browser.js +46 -21
- package/dist/commands/daemon-test-harness.d.ts +1 -1
- package/dist/commands/daemon-test-harness.js +2 -2
- package/dist/commands/daemon.js +21 -22
- package/dist/commands/exec.js +40 -0
- package/dist/commands/monitors.js +2 -2
- package/dist/commands/repo.js +57 -19
- package/dist/commands/resume.d.ts +16 -0
- package/dist/commands/resume.js +41 -8
- package/dist/commands/routines.js +42 -21
- package/dist/commands/sessions-optimize.js +1 -1
- package/dist/commands/sessions.js +4 -11
- package/dist/commands/share.d.ts +5 -3
- package/dist/commands/share.js +33 -9
- package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
- package/dist/lib/accounting/account-pool-collect.js +6 -4
- package/dist/lib/accounting/usage-ingest.js +4 -2
- package/dist/lib/accounting/usage-sync.d.ts +38 -93
- package/dist/lib/accounting/usage-sync.js +66 -210
- package/dist/lib/accounting/usage.d.ts +11 -3
- package/dist/lib/accounting/usage.js +23 -10
- package/dist/lib/auth-health.d.ts +8 -0
- package/dist/lib/auth-health.js +4 -4
- package/dist/lib/browser/caller-identity.d.ts +12 -0
- package/dist/lib/browser/caller-identity.js +19 -0
- package/dist/lib/browser/ipc.d.ts +37 -32
- package/dist/lib/browser/ipc.js +146 -94
- package/dist/lib/browser/task-index.d.ts +10 -2
- package/dist/lib/browser/task-index.js +22 -3
- package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
- package/dist/lib/daemon/account-state-daemon-service.js +81 -18
- package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
- package/dist/lib/daemon/auth-sync-service.js +17 -6
- package/dist/lib/daemon/catchup-service.d.ts +51 -0
- package/dist/lib/daemon/catchup-service.js +51 -0
- package/dist/lib/daemon/daemon.d.ts +12 -22
- package/dist/lib/daemon/daemon.js +463 -176
- package/dist/lib/daemon/runner.js +2 -0
- package/dist/lib/daemon/service.d.ts +22 -4
- package/dist/lib/daemon/service.js +2 -2
- package/dist/lib/daemon/supervisor.d.ts +55 -15
- package/dist/lib/daemon/supervisor.js +119 -29
- package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
- package/dist/lib/daemon/usage-sync-service.js +22 -18
- package/dist/lib/daemon-health.js +36 -31
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +12 -2
- package/dist/lib/daemon-ticks.d.ts +9 -6
- package/dist/lib/daemon-ticks.js +14 -8
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/exec.js +11 -0
- package/dist/lib/feed/watch.d.ts +3 -0
- package/dist/lib/feed/watch.js +13 -3
- package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
- package/dist/lib/fleet-shared-repo-sync.js +333 -0
- package/dist/lib/fleet-shared-state.d.ts +38 -0
- package/dist/lib/fleet-shared-state.js +105 -0
- package/dist/lib/hosts/remote-cmd.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.js +12 -3
- package/dist/lib/lock-compromise.d.ts +8 -0
- package/dist/lib/lock-compromise.js +12 -0
- package/dist/lib/monitors/engine.d.ts +2 -1
- package/dist/lib/monitors/engine.js +9 -2
- package/dist/lib/refresh-coordinator.js +2 -0
- package/dist/lib/run-trace-sync.d.ts +13 -0
- package/dist/lib/run-trace-sync.js +77 -0
- package/dist/lib/secrets/push.d.ts +10 -0
- package/dist/lib/secrets/push.js +86 -7
- package/dist/lib/secrets/remote.d.ts +18 -6
- package/dist/lib/secrets/remote.js +29 -4
- package/dist/lib/secrets/reserved-sync.d.ts +28 -27
- package/dist/lib/secrets/reserved-sync.js +119 -101
- package/dist/lib/session/active.d.ts +3 -1
- package/dist/lib/session/active.js +2 -0
- package/dist/lib/session/actor-sidecar.d.ts +12 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +5 -2
- package/dist/lib/session/db.js +54 -22
- package/dist/lib/session/discover.d.ts +12 -3
- package/dist/lib/session/discover.js +106 -25
- package/dist/lib/session/prompt.d.ts +11 -0
- package/dist/lib/session/prompt.js +25 -0
- package/dist/lib/session/recovery.d.ts +30 -2
- package/dist/lib/session/recovery.js +56 -4
- package/dist/lib/session/remote/remote-list.d.ts +17 -1
- package/dist/lib/session/remote/remote-list.js +29 -4
- package/dist/lib/session/remote/watch.d.ts +25 -2
- package/dist/lib/session/remote/watch.js +185 -11
- package/dist/lib/session/session-cache.d.ts +2 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/share/publish.d.ts +55 -9
- package/dist/lib/share/publish.js +87 -9
- package/dist/lib/share/worker-template.js +100 -8
- package/dist/lib/ssh-exec.d.ts +2 -0
- package/dist/lib/ssh-exec.js +20 -4
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
- package/dist/lib/testdata/daemon-health-writer.js +8 -0
- package/dist/lib/traces/sync.d.ts +7 -0
- package/dist/lib/traces/sync.js +9 -0
- package/dist/lib/usage-refresh.d.ts +8 -2
- package/dist/lib/usage-refresh.js +3 -3
- package/package.json +1 -1
- package/dist/lib/account-state-service.d.ts +0 -21
- package/dist/lib/account-state-service.js +0 -60
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-sync this device's just-finished session trace when a local, headless
|
|
3
|
+
* `agents run` exits (PHNX-3628).
|
|
4
|
+
*
|
|
5
|
+
* Why exit-armed + spawn, exactly like `run-notify`: the sync reads this
|
|
6
|
+
* device's `sessions.db` and PUTs redacted shards over the network — async work
|
|
7
|
+
* that CANNOT run inside a `process.on('exit')` handler (Node runs no more
|
|
8
|
+
* microtasks after exit). So, mirroring `notifyDesktop`, on exit we SPAWN a
|
|
9
|
+
* detached `agents traces sync` and unref it; it finishes after the parent is
|
|
10
|
+
* gone, and the run itself returns immediately instead of waiting on the PUTs.
|
|
11
|
+
*
|
|
12
|
+
* The upload is incremental by construction (`syncTraces` de-dups by the
|
|
13
|
+
* `traces-sync.json` mtime watermark), so the spawned sync pushes essentially
|
|
14
|
+
* just the session that finished, not the whole history.
|
|
15
|
+
*
|
|
16
|
+
* Default policy: fire ONLY when the user has ALREADY opted into the traces
|
|
17
|
+
* store — i.e. they are signed in AND have run `agents traces sync` at least
|
|
18
|
+
* once (`hasSyncedBefore`). A user who never synced is never touched. Opt out
|
|
19
|
+
* of a given run with `--no-trace-sync` or `AGENTS_NO_TRACE_SYNC=1`.
|
|
20
|
+
*
|
|
21
|
+
* Local runs only: a run placed on another machine (`--device`/`--on`/
|
|
22
|
+
* `--computer`/`--where device:`, `--lease`, `--box`, or `--cloud`) has its
|
|
23
|
+
* trajectory on the REMOTE box, not this device's `sessions.db`, so arming here
|
|
24
|
+
* would upload nothing useful — the remote box runs its own exit sync. The
|
|
25
|
+
* caller gates every remote-placement signal out before arming (it reuses the
|
|
26
|
+
* file's own `hostTargetGiven` predicate plus `--lease`/`--box`; `--cloud`
|
|
27
|
+
* returns earlier).
|
|
28
|
+
*/
|
|
29
|
+
import { spawn } from 'child_process';
|
|
30
|
+
import { getCliLaunch } from './cli-entry.js';
|
|
31
|
+
import { readSession } from './identity/client.js';
|
|
32
|
+
import { hasSyncedBefore } from './traces/sync.js';
|
|
33
|
+
/**
|
|
34
|
+
* The policy gate, pure and unit-testable. `disabled` is the resolved
|
|
35
|
+
* `--no-trace-sync` (commander maps it to `traceSync === false`).
|
|
36
|
+
*/
|
|
37
|
+
export function shouldAutoSyncTraces(disabled) {
|
|
38
|
+
if (disabled)
|
|
39
|
+
return false;
|
|
40
|
+
if (process.env.AGENTS_NO_TRACE_SYNC === '1')
|
|
41
|
+
return false;
|
|
42
|
+
if (!readSession())
|
|
43
|
+
return false; // not signed in → nothing to authenticate an upload
|
|
44
|
+
if (!hasSyncedBefore())
|
|
45
|
+
return false; // never opted into the traces store
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Arm a fire-and-forget `agents traces sync` for when this process exits.
|
|
50
|
+
* No-op unless {@link shouldAutoSyncTraces} passes. Best-effort by
|
|
51
|
+
* construction: a missing binary or a stalled child never affects the run.
|
|
52
|
+
*/
|
|
53
|
+
export function armRunFinishTraceSync(opts = {}) {
|
|
54
|
+
if (!shouldAutoSyncTraces(!!opts.disabled))
|
|
55
|
+
return;
|
|
56
|
+
process.on('exit', () => {
|
|
57
|
+
try {
|
|
58
|
+
// Re-invoke this CLI through getCliLaunch, NOT a hand-rolled
|
|
59
|
+
// [process.execPath, process.argv[1], …] — under the compiled standalone
|
|
60
|
+
// binary argv[1] is the bun virtual entry (/$bunfs/root/agents), which
|
|
61
|
+
// would become a bogus subcommand and silently no-op the whole feature
|
|
62
|
+
// (cli-entry.ts). The spawned `traces sync` is a fresh process with its
|
|
63
|
+
// own event loop, so its own per-request network timeouts (syncTraces)
|
|
64
|
+
// bound it — a watchdog here could never fire anyway (Node runs no loop
|
|
65
|
+
// after an 'exit' handler returns), so we do not pretend to add one.
|
|
66
|
+
const { command, args } = getCliLaunch(['traces', 'sync']);
|
|
67
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' });
|
|
68
|
+
// A child that never starts (ENOENT) emits an async 'error'; without a
|
|
69
|
+
// listener Node re-throws it as uncaught. Swallow — this is best-effort.
|
|
70
|
+
child.on('error', () => { });
|
|
71
|
+
child.unref();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// A synchronous spawn failure must never change the run's exit outcome.
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -34,6 +34,8 @@ export interface PushBundleOptions {
|
|
|
34
34
|
agentOnly?: boolean;
|
|
35
35
|
/** Non-secret literals whose bundle value kind must survive the dotenv transport. */
|
|
36
36
|
literalValues?: Record<string, string>;
|
|
37
|
+
/** Per-SSH-operation deadline. Async daemon callers must set this explicitly. */
|
|
38
|
+
timeoutMs?: number;
|
|
37
39
|
}
|
|
38
40
|
export interface PushBundleResult {
|
|
39
41
|
ok: boolean;
|
|
@@ -121,3 +123,11 @@ export declare function planPushTransport(resolved: ResolvedBundleForPush, bundl
|
|
|
121
123
|
export declare function pushResolvedBundleToHost(resolved: ResolvedBundleForPush, bundle: string, host: string, opts: PushBundleOptions): PushBundleResult;
|
|
122
124
|
/** Resolve and push in one call — for a single host. */
|
|
123
125
|
export declare function pushBundleToHost(bundle: string, host: string, opts: PushBundleOptions): PushBundleResult;
|
|
126
|
+
/**
|
|
127
|
+
* Async, kill-bounded push for daemon services. It preserves the foreground
|
|
128
|
+
* primitive's transport, read-back verification, and literal restoration; only
|
|
129
|
+
* the SSH execution strategy differs.
|
|
130
|
+
*/
|
|
131
|
+
export declare function pushResolvedBundleToHostAsync(resolved: ResolvedBundleForPush, bundle: string, host: string, opts: PushBundleOptions): Promise<PushBundleResult>;
|
|
132
|
+
/** Resolve locally once, then run the daemon-safe async push. */
|
|
133
|
+
export declare function pushBundleToHostAsync(bundle: string, host: string, opts: PushBundleOptions): Promise<PushBundleResult>;
|
package/dist/lib/secrets/push.js
CHANGED
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
* reading a bundle can prompt (Touch ID); doing it per host would prompt per
|
|
20
20
|
* host. `export --device a,b,c` resolves once and pushes three times.
|
|
21
21
|
*/
|
|
22
|
-
import { sshExec } from '../ssh-exec.js';
|
|
22
|
+
import { sshExec, sshExecAsync } from '../ssh-exec.js';
|
|
23
23
|
import { remoteShellFor, buildWindowsStdinImportCommand } from '../hosts/remote-cmd.js';
|
|
24
24
|
import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
25
|
-
import { remoteSecretsRaw, verifyRemoteKeychainPush, keychainWriteFailureMessage, buildRemoteFileImportCommand, credentialTransportSshOpts, } from './remote.js';
|
|
25
|
+
import { remoteSecretsRaw, remoteSecretsRawAsync, verifyRemoteKeychainPush, verifyRemoteKeychainPushAsync, keychainWriteFailureMessage, buildRemoteFileImportCommand, credentialTransportSshOpts, } from './remote.js';
|
|
26
26
|
import { readAndResolveBundleEnv } from './bundles.js';
|
|
27
27
|
/**
|
|
28
28
|
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
@@ -138,8 +138,8 @@ export function pushResolvedBundleToHost(resolved, bundle, host, opts) {
|
|
|
138
138
|
// the raw-`ssh` branch's half of that; the `remote-secrets` branch and the
|
|
139
139
|
// read-back/policy/literal follow-ups pass `secret: true` for the same posture.
|
|
140
140
|
const res = plan.kind === 'ssh'
|
|
141
|
-
? sshExec(host, plan.remoteCmd, { input: plan.input, hostKeyOpts: credentialTransportSshOpts(host).hostKeyOpts, multiplex: plan.multiplex })
|
|
142
|
-
: remoteSecretsRaw(host, plan.args, { input: plan.input, osLookupName: host, secret: true });
|
|
141
|
+
? sshExec(host, plan.remoteCmd, { input: plan.input, timeoutMs: opts.timeoutMs, hostKeyOpts: credentialTransportSshOpts(host).hostKeyOpts, multiplex: plan.multiplex })
|
|
142
|
+
: remoteSecretsRaw(host, plan.args, { input: plan.input, timeoutMs: opts.timeoutMs, osLookupName: host, secret: true });
|
|
143
143
|
if (res.code === null) {
|
|
144
144
|
return fail(res.stderr.trim() || (res.timedOut ? 'ssh timed out' : 'ssh failed'));
|
|
145
145
|
}
|
|
@@ -154,7 +154,7 @@ export function pushResolvedBundleToHost(resolved, bundle, host, opts) {
|
|
|
154
154
|
// - file: a forwarded AGENTS_SECRETS_PASSPHRASE keys ciphertext to a secret
|
|
155
155
|
// the destination daemon does not hold (PHNX-2371).
|
|
156
156
|
// Read the bundle back the way a later resolve will and FAIL LOUDLY.
|
|
157
|
-
const verdict = verifyRemoteKeychainPush(host, bundle, Object.keys(resolved.env), { osLookupName: host, secret: true });
|
|
157
|
+
const verdict = verifyRemoteKeychainPush(host, bundle, Object.keys(resolved.env), { osLookupName: host, secret: true, timeoutMs: opts.timeoutMs });
|
|
158
158
|
if (!verdict.ok) {
|
|
159
159
|
if (opts.remoteBackend === 'keychain' && verdict.kind === 'locked-keychain') {
|
|
160
160
|
return fail(keychainWriteFailureMessage(host, bundle, verdict.reason));
|
|
@@ -168,12 +168,12 @@ export function pushResolvedBundleToHost(resolved, bundle, host, opts) {
|
|
|
168
168
|
return fail(`pushed '${bundle}' but could not verify it on the remote: ${verdict.reason}`);
|
|
169
169
|
}
|
|
170
170
|
for (const step of planLiteralRestoration(bundle, opts.literalValues)) {
|
|
171
|
-
const removed = remoteSecretsRaw(host, step.removeArgs, { osLookupName: host, secret: true });
|
|
171
|
+
const removed = remoteSecretsRaw(host, step.removeArgs, { osLookupName: host, secret: true, timeoutMs: opts.timeoutMs });
|
|
172
172
|
if (removed.code !== 0) {
|
|
173
173
|
const msg = (removed.stderr || removed.stdout || '').trim();
|
|
174
174
|
return fail(`pushed '${bundle}' but could not replace transported ${step.key}${msg ? `: ${msg}` : ''}`);
|
|
175
175
|
}
|
|
176
|
-
const literal = remoteSecretsRaw(host, step.addArgs, { osLookupName: host, secret: true });
|
|
176
|
+
const literal = remoteSecretsRaw(host, step.addArgs, { osLookupName: host, secret: true, timeoutMs: opts.timeoutMs });
|
|
177
177
|
if (literal.code !== 0) {
|
|
178
178
|
const msg = (literal.stderr || literal.stdout || '').trim();
|
|
179
179
|
return fail(`pushed '${bundle}' but could not preserve literal ${step.key}${msg ? `: ${msg}` : ''}`);
|
|
@@ -192,3 +192,82 @@ export function pushResolvedBundleToHost(resolved, bundle, host, opts) {
|
|
|
192
192
|
export function pushBundleToHost(bundle, host, opts) {
|
|
193
193
|
return pushResolvedBundleToHost(resolveBundleForPush(bundle, opts.operation, { agentOnly: opts.agentOnly }), bundle, host, opts);
|
|
194
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Async, kill-bounded push for daemon services. It preserves the foreground
|
|
197
|
+
* primitive's transport, read-back verification, and literal restoration; only
|
|
198
|
+
* the SSH execution strategy differs.
|
|
199
|
+
*/
|
|
200
|
+
export async function pushResolvedBundleToHostAsync(resolved, bundle, host, opts) {
|
|
201
|
+
const fail = (message) => ({ ok: false, host, bundle, keyCount: resolved.keyCount, message });
|
|
202
|
+
const plan = planPushTransport(resolved, bundle, host, opts);
|
|
203
|
+
if (plan.kind === 'refuse')
|
|
204
|
+
return fail(plan.message);
|
|
205
|
+
const res = plan.kind === 'ssh'
|
|
206
|
+
? await sshExecAsync(host, plan.remoteCmd, {
|
|
207
|
+
input: plan.input,
|
|
208
|
+
timeoutMs: opts.timeoutMs,
|
|
209
|
+
hostKeyOpts: credentialTransportSshOpts(host).hostKeyOpts,
|
|
210
|
+
multiplex: plan.multiplex,
|
|
211
|
+
})
|
|
212
|
+
: await remoteSecretsRawAsync(host, plan.args, {
|
|
213
|
+
input: plan.input,
|
|
214
|
+
timeoutMs: opts.timeoutMs,
|
|
215
|
+
osLookupName: host,
|
|
216
|
+
secret: true,
|
|
217
|
+
});
|
|
218
|
+
if (res.code === null)
|
|
219
|
+
return fail(res.stderr.trim() || (res.timedOut ? 'ssh timed out' : 'ssh failed'));
|
|
220
|
+
if (res.code !== 0) {
|
|
221
|
+
const msg = (res.stderr || res.stdout || '').trim();
|
|
222
|
+
return fail(`remote import failed (exit ${res.code})${msg ? `: ${msg}` : ''}`);
|
|
223
|
+
}
|
|
224
|
+
const verdict = await verifyRemoteKeychainPushAsync(host, bundle, Object.keys(resolved.env), {
|
|
225
|
+
osLookupName: host,
|
|
226
|
+
secret: true,
|
|
227
|
+
timeoutMs: opts.timeoutMs,
|
|
228
|
+
});
|
|
229
|
+
if (!verdict.ok) {
|
|
230
|
+
if (opts.remoteBackend === 'keychain' && verdict.kind === 'locked-keychain') {
|
|
231
|
+
return fail(keychainWriteFailureMessage(host, bundle, verdict.reason));
|
|
232
|
+
}
|
|
233
|
+
if (opts.remoteBackend === 'file') {
|
|
234
|
+
return fail(`${host}: pushed '${bundle}' but the remote could not decrypt it (${verdict.reason}). ` +
|
|
235
|
+
`File-backend export never forwards AGENTS_SECRETS_PASSPHRASE — the remote keys ` +
|
|
236
|
+
`the bundle under its own machine-local key. If the destination still cannot read, ` +
|
|
237
|
+
`unset a stale AGENTS_SECRETS_PASSPHRASE there (\`agents doctor\`).`);
|
|
238
|
+
}
|
|
239
|
+
return fail(`pushed '${bundle}' but could not verify it on the remote: ${verdict.reason}`);
|
|
240
|
+
}
|
|
241
|
+
for (const step of planLiteralRestoration(bundle, opts.literalValues)) {
|
|
242
|
+
const removed = await remoteSecretsRawAsync(host, step.removeArgs, {
|
|
243
|
+
osLookupName: host,
|
|
244
|
+
secret: true,
|
|
245
|
+
timeoutMs: opts.timeoutMs,
|
|
246
|
+
});
|
|
247
|
+
if (removed.code !== 0) {
|
|
248
|
+
const msg = (removed.stderr || removed.stdout || '').trim();
|
|
249
|
+
return fail(`pushed '${bundle}' but could not replace transported ${step.key}${msg ? `: ${msg}` : ''}`);
|
|
250
|
+
}
|
|
251
|
+
const literal = await remoteSecretsRawAsync(host, step.addArgs, {
|
|
252
|
+
osLookupName: host,
|
|
253
|
+
secret: true,
|
|
254
|
+
timeoutMs: opts.timeoutMs,
|
|
255
|
+
});
|
|
256
|
+
if (literal.code !== 0) {
|
|
257
|
+
const msg = (literal.stderr || literal.stdout || '').trim();
|
|
258
|
+
return fail(`pushed '${bundle}' but could not preserve literal ${step.key}${msg ? `: ${msg}` : ''}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const remoteMsg = (res.stdout || '').trim().split('\n').map((line) => line.trim()).filter(Boolean).pop();
|
|
262
|
+
return {
|
|
263
|
+
ok: true,
|
|
264
|
+
host,
|
|
265
|
+
bundle,
|
|
266
|
+
keyCount: resolved.keyCount,
|
|
267
|
+
message: remoteMsg || `${resolved.keyCount} key(s) exported`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/** Resolve locally once, then run the daemon-safe async push. */
|
|
271
|
+
export function pushBundleToHostAsync(bundle, host, opts) {
|
|
272
|
+
return pushResolvedBundleToHostAsync(resolveBundleForPush(bundle, opts.operation, { agentOnly: opts.agentOnly }), bundle, host, opts);
|
|
273
|
+
}
|
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
* and injects it ephemerally without persisting it locally.
|
|
7
7
|
*/
|
|
8
8
|
import { type SshExecResult } from '../ssh-exec.js';
|
|
9
|
+
export declare const REMOTE_TIMEOUT_MS = 30000;
|
|
10
|
+
export interface RemoteSecretsRawOptions {
|
|
11
|
+
tty?: boolean;
|
|
12
|
+
input?: string;
|
|
13
|
+
osLookupName?: string;
|
|
14
|
+
secret?: boolean;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
9
17
|
/**
|
|
10
18
|
* SSH options for secret-carrying transport. Pins the managed host key and
|
|
11
19
|
* disables multiplex so no reusable control socket lingers for other invocations.
|
|
@@ -53,12 +61,9 @@ export declare function splitBundleRef(ref: string): {
|
|
|
53
61
|
* result. Used by browse commands; `tty` forces `-tt` so remote passphrase
|
|
54
62
|
* prompts can surface.
|
|
55
63
|
*/
|
|
56
|
-
export declare function remoteSecretsRaw(target: string, args: string[], opts?:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
osLookupName?: string;
|
|
60
|
-
secret?: boolean;
|
|
61
|
-
}): SshExecResult;
|
|
64
|
+
export declare function remoteSecretsRaw(target: string, args: string[], opts?: RemoteSecretsRawOptions): SshExecResult;
|
|
65
|
+
/** Async, kill-bounded inverse of {@link remoteSecretsRaw} for daemon callers. */
|
|
66
|
+
export declare function remoteSecretsRawAsync(target: string, args: string[], opts?: RemoteSecretsRawOptions): Promise<SshExecResult>;
|
|
62
67
|
/**
|
|
63
68
|
* Run a remote `agents secrets <args>` foreground with local stdio inherited,
|
|
64
69
|
* so the remote sees a real TTY and its passphrase prompt surfaces and reads
|
|
@@ -122,7 +127,14 @@ export declare function keychainWriteFailureMessage(host: string, bundle: string
|
|
|
122
127
|
export declare function verifyRemoteKeychainPush(target: string, bundle: string, pushedKeys: string[], opts?: {
|
|
123
128
|
osLookupName?: string;
|
|
124
129
|
secret?: boolean;
|
|
130
|
+
timeoutMs?: number;
|
|
125
131
|
}): RemoteKeychainWriteVerification;
|
|
132
|
+
/** Async, kill-bounded read-back verification for daemon-originated pushes. */
|
|
133
|
+
export declare function verifyRemoteKeychainPushAsync(target: string, bundle: string, pushedKeys: string[], opts?: {
|
|
134
|
+
osLookupName?: string;
|
|
135
|
+
secret?: boolean;
|
|
136
|
+
timeoutMs?: number;
|
|
137
|
+
}): Promise<RemoteKeychainWriteVerification>;
|
|
126
138
|
/**
|
|
127
139
|
* `bash -lc` command + stdin payload for a file-backed remote import.
|
|
128
140
|
*
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Browse streams remote stdout verbatim; use resolves a bundle to an env map
|
|
6
6
|
* and injects it ephemerally without persisting it locally.
|
|
7
7
|
*/
|
|
8
|
-
import { sshExec, sshStream, assertValidSshTarget, shellQuote } from '../ssh-exec.js';
|
|
8
|
+
import { sshExec, sshExecAsync, sshStream, assertValidSshTarget, shellQuote } from '../ssh-exec.js';
|
|
9
9
|
import { resolveHost } from '../hosts/registry.js';
|
|
10
10
|
import { emitSecretAudit } from './audit.js';
|
|
11
11
|
import { sshTargetFor } from '../hosts/types.js';
|
|
@@ -13,7 +13,7 @@ import { buildRemoteAgentsInvocation } from '../hosts/remote-cmd.js';
|
|
|
13
13
|
import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
14
14
|
import { isLoaderOrInterpreterEnv } from './bundles.js';
|
|
15
15
|
import { isHostPinned, hostKeyCheckingOpts } from '../devices/known-hosts.js';
|
|
16
|
-
const REMOTE_TIMEOUT_MS = 30_000;
|
|
16
|
+
export const REMOTE_TIMEOUT_MS = 30_000;
|
|
17
17
|
/** The host part of an ssh target (`user@host` -> `host`) for known_hosts matching. */
|
|
18
18
|
function hostKeyLookupName(target) {
|
|
19
19
|
return target.split('@').pop() ?? target;
|
|
@@ -125,7 +125,20 @@ export function remoteSecretsRaw(target, args, opts = {}) {
|
|
|
125
125
|
? { ...posture, extraSshArgs: ['-tt'], multiplex: false }
|
|
126
126
|
: posture;
|
|
127
127
|
return sshExec(target, remoteCmd, {
|
|
128
|
-
timeoutMs: REMOTE_TIMEOUT_MS,
|
|
128
|
+
timeoutMs: opts.timeoutMs ?? REMOTE_TIMEOUT_MS,
|
|
129
|
+
input: opts.input,
|
|
130
|
+
...conn,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/** Async, kill-bounded inverse of {@link remoteSecretsRaw} for daemon callers. */
|
|
134
|
+
export function remoteSecretsRawAsync(target, args, opts = {}) {
|
|
135
|
+
const remoteCmd = buildRemoteAgentsInvocation(['secrets', ...args], undefined, osForTarget(target, opts.osLookupName));
|
|
136
|
+
const posture = opts.secret ? credentialTransportSshOpts(target) : {};
|
|
137
|
+
const conn = opts.tty
|
|
138
|
+
? { ...posture, extraSshArgs: ['-tt'], multiplex: false }
|
|
139
|
+
: posture;
|
|
140
|
+
return sshExecAsync(target, remoteCmd, {
|
|
141
|
+
timeoutMs: opts.timeoutMs ?? REMOTE_TIMEOUT_MS,
|
|
129
142
|
input: opts.input,
|
|
130
143
|
...conn,
|
|
131
144
|
});
|
|
@@ -270,9 +283,21 @@ export function keychainWriteFailureMessage(host, bundle, reason) {
|
|
|
270
283
|
export function verifyRemoteKeychainPush(target, bundle, pushedKeys, opts = {}) {
|
|
271
284
|
const remoteCmd = buildRemoteAgentsInvocation(['secrets', 'export', bundle, '--plaintext', '--format', 'json'], undefined, osForTarget(target, opts.osLookupName), { AGENTS_SECRETS_REMOTE_TRANSPORT: '1' });
|
|
272
285
|
const res = sshExec(target, remoteCmd, {
|
|
273
|
-
timeoutMs: REMOTE_TIMEOUT_MS,
|
|
286
|
+
timeoutMs: opts.timeoutMs ?? REMOTE_TIMEOUT_MS,
|
|
287
|
+
...(opts.secret ? credentialTransportSshOpts(target) : {}),
|
|
288
|
+
});
|
|
289
|
+
return evaluateRemoteKeychainPushResult(pushedKeys, res);
|
|
290
|
+
}
|
|
291
|
+
/** Async, kill-bounded read-back verification for daemon-originated pushes. */
|
|
292
|
+
export async function verifyRemoteKeychainPushAsync(target, bundle, pushedKeys, opts = {}) {
|
|
293
|
+
const remoteCmd = buildRemoteAgentsInvocation(['secrets', 'export', bundle, '--plaintext', '--format', 'json'], undefined, osForTarget(target, opts.osLookupName), { AGENTS_SECRETS_REMOTE_TRANSPORT: '1' });
|
|
294
|
+
const res = await sshExecAsync(target, remoteCmd, {
|
|
295
|
+
timeoutMs: opts.timeoutMs ?? REMOTE_TIMEOUT_MS,
|
|
274
296
|
...(opts.secret ? credentialTransportSshOpts(target) : {}),
|
|
275
297
|
});
|
|
298
|
+
return evaluateRemoteKeychainPushResult(pushedKeys, res);
|
|
299
|
+
}
|
|
300
|
+
function evaluateRemoteKeychainPushResult(pushedKeys, res) {
|
|
276
301
|
if (res.code !== 0) {
|
|
277
302
|
const why = res.timedOut ? 'timed out' : res.code === null ? 'ssh failed' : `exit ${res.code}`;
|
|
278
303
|
const stderr = `${why}${(res.stderr || res.stdout || '').trim() ? `: ${(res.stderr || res.stdout).trim()}` : ''}`;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { type PushBundleResult } from './push.js';
|
|
2
2
|
import { type DeviceProfile } from '../devices/registry.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type SharedAuthStatus } from '../fleet-shared-state.js';
|
|
4
|
+
/** Each import/read-back SSH operation gets this deadline plus the SSH hard-kill grace. */
|
|
5
|
+
export declare const AUTH_SYNC_PUSH_DEADLINE_MS = 20000;
|
|
4
6
|
export interface AuthSyncDevice {
|
|
5
7
|
name: string;
|
|
6
8
|
reachable: boolean;
|
|
7
9
|
pinned: boolean;
|
|
8
|
-
|
|
10
|
+
remoteAuth: SharedAuthStatus | 'unknown';
|
|
9
11
|
}
|
|
10
12
|
export type AuthSyncPlanItem = {
|
|
11
13
|
action: 'push';
|
|
@@ -15,8 +17,10 @@ export type AuthSyncPlanItem = {
|
|
|
15
17
|
device: string;
|
|
16
18
|
reason: string;
|
|
17
19
|
};
|
|
18
|
-
export declare function planAuthBundlePush(localAuthOk: boolean, devices: AuthSyncDevice[]): AuthSyncPlanItem[];
|
|
20
|
+
export declare function planAuthBundlePush(localAuthOk: boolean, localIsPublisher: boolean, devices: AuthSyncDevice[]): AuthSyncPlanItem[];
|
|
19
21
|
export interface AuthSyncResult {
|
|
22
|
+
publisher: string | null;
|
|
23
|
+
stateChanged: boolean;
|
|
20
24
|
pushed: string[];
|
|
21
25
|
skipped: Array<{
|
|
22
26
|
device: string;
|
|
@@ -27,10 +31,6 @@ export interface AuthSyncResult {
|
|
|
27
31
|
message: string;
|
|
28
32
|
}>;
|
|
29
33
|
}
|
|
30
|
-
export interface AuthSyncProbe {
|
|
31
|
-
reachable: boolean;
|
|
32
|
-
remoteHasAuth: boolean;
|
|
33
|
-
}
|
|
34
34
|
export interface AuthSyncDeps {
|
|
35
35
|
inspectLocal?: () => {
|
|
36
36
|
exists: boolean;
|
|
@@ -38,28 +38,29 @@ export interface AuthSyncDeps {
|
|
|
38
38
|
};
|
|
39
39
|
listDevices?: () => DeviceProfile[];
|
|
40
40
|
localName?: string;
|
|
41
|
+
userAgentsDir?: string;
|
|
41
42
|
isPinned?: (name: string) => boolean;
|
|
42
|
-
|
|
43
|
-
* Per-device reachability + auth presence. Default: `probeDevice({ withSecrets: true })`
|
|
44
|
-
* — the same remote-presence probe `fleet apply` uses in `decideSecretPush`.
|
|
45
|
-
*/
|
|
46
|
-
probe?: (device: DeviceProfile) => AuthSyncProbe;
|
|
47
|
-
push?: (bundle: string, host: string) => PushBundleResult;
|
|
43
|
+
push?: (bundle: string, host: string) => Promise<PushBundleResult>;
|
|
48
44
|
sshTarget?: (device: DeviceProfile) => string;
|
|
49
45
|
}
|
|
46
|
+
export interface PublishAuthVerdictOptions {
|
|
47
|
+
inspectLocal?: () => {
|
|
48
|
+
exists: boolean;
|
|
49
|
+
ok: boolean;
|
|
50
|
+
};
|
|
51
|
+
localName?: string;
|
|
52
|
+
userAgentsDir?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface PublishAuthVerdictResult {
|
|
55
|
+
device: string;
|
|
56
|
+
status: SharedAuthStatus;
|
|
57
|
+
changed: boolean;
|
|
58
|
+
error: string | null;
|
|
59
|
+
}
|
|
60
|
+
/** Publish only safe readiness metadata; useful immediately before a repo push. */
|
|
61
|
+
export declare function publishReservedAuthVerdict(options?: PublishAuthVerdictOptions): PublishAuthVerdictResult;
|
|
50
62
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* Presence is an own-property check on `remoteBundles` (same gate as
|
|
54
|
-
* `decideSecretPush`): a missing listing, a parse failure, or a prototype
|
|
55
|
-
* name must all mean "not present, so push" — never "present, so skip".
|
|
56
|
-
*/
|
|
57
|
-
export declare function authPresenceFromProbe(probe: DeviceProbe): AuthSyncProbe;
|
|
58
|
-
/** Production default: live SSH probe of reachability + `secrets list --json`. */
|
|
59
|
-
export declare function defaultAuthSyncProbe(device: DeviceProfile): AuthSyncProbe;
|
|
60
|
-
/**
|
|
61
|
-
* Push the local file-backed `auth` bundle to every pinned registered device
|
|
62
|
-
* that does not already have it. No-op when the local bundle is missing or on
|
|
63
|
-
* the wrong backend (doctor flags that separately).
|
|
63
|
+
* Publish local readiness, elect one ready source, then asynchronously provision
|
|
64
|
+
* only peers whose shared verdict says the bundle is missing.
|
|
64
65
|
*/
|
|
65
|
-
export declare function syncReservedAuthBundle(deps?: AuthSyncDeps): AuthSyncResult
|
|
66
|
+
export declare function syncReservedAuthBundle(deps?: AuthSyncDeps): Promise<AuthSyncResult>;
|