@phnx-labs/agents-cli 1.22.65 → 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.
Files changed (112) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +7 -2
  3. package/dist/bootstrap.js +3 -3
  4. package/dist/commands/apply.js +9 -2
  5. package/dist/commands/browser.js +46 -21
  6. package/dist/commands/daemon-test-harness.d.ts +1 -1
  7. package/dist/commands/daemon-test-harness.js +2 -2
  8. package/dist/commands/daemon.js +21 -22
  9. package/dist/commands/exec.js +40 -0
  10. package/dist/commands/monitors.js +2 -2
  11. package/dist/commands/repo.js +57 -19
  12. package/dist/commands/resume.d.ts +16 -0
  13. package/dist/commands/resume.js +41 -8
  14. package/dist/commands/routines.js +42 -21
  15. package/dist/commands/sessions-optimize.js +1 -1
  16. package/dist/commands/sessions.js +4 -11
  17. package/dist/commands/share.d.ts +5 -3
  18. package/dist/commands/share.js +33 -9
  19. package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
  20. package/dist/lib/accounting/account-pool-collect.js +6 -4
  21. package/dist/lib/accounting/usage-ingest.js +4 -2
  22. package/dist/lib/accounting/usage-sync.d.ts +38 -93
  23. package/dist/lib/accounting/usage-sync.js +66 -210
  24. package/dist/lib/accounting/usage.d.ts +11 -3
  25. package/dist/lib/accounting/usage.js +23 -10
  26. package/dist/lib/auth-health.d.ts +8 -0
  27. package/dist/lib/auth-health.js +4 -4
  28. package/dist/lib/browser/caller-identity.d.ts +12 -0
  29. package/dist/lib/browser/caller-identity.js +19 -0
  30. package/dist/lib/browser/ipc.d.ts +37 -32
  31. package/dist/lib/browser/ipc.js +146 -94
  32. package/dist/lib/browser/task-index.d.ts +10 -2
  33. package/dist/lib/browser/task-index.js +22 -3
  34. package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
  35. package/dist/lib/daemon/account-state-daemon-service.js +81 -18
  36. package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
  37. package/dist/lib/daemon/auth-sync-service.js +17 -6
  38. package/dist/lib/daemon/catchup-service.d.ts +51 -0
  39. package/dist/lib/daemon/catchup-service.js +51 -0
  40. package/dist/lib/daemon/daemon.d.ts +12 -22
  41. package/dist/lib/daemon/daemon.js +463 -176
  42. package/dist/lib/daemon/runner.js +2 -0
  43. package/dist/lib/daemon/service.d.ts +22 -4
  44. package/dist/lib/daemon/service.js +2 -2
  45. package/dist/lib/daemon/supervisor.d.ts +55 -15
  46. package/dist/lib/daemon/supervisor.js +119 -29
  47. package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
  48. package/dist/lib/daemon/usage-sync-service.js +22 -18
  49. package/dist/lib/daemon-health.js +36 -31
  50. package/dist/lib/daemon-services.d.ts +1 -1
  51. package/dist/lib/daemon-services.js +12 -2
  52. package/dist/lib/daemon-ticks.d.ts +9 -6
  53. package/dist/lib/daemon-ticks.js +14 -8
  54. package/dist/lib/devices/known-hosts.d.ts +15 -0
  55. package/dist/lib/devices/known-hosts.js +18 -0
  56. package/dist/lib/devices/registry.js +2 -0
  57. package/dist/lib/exec.js +11 -0
  58. package/dist/lib/feed/watch.d.ts +3 -0
  59. package/dist/lib/feed/watch.js +13 -3
  60. package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
  61. package/dist/lib/fleet-shared-repo-sync.js +333 -0
  62. package/dist/lib/fleet-shared-state.d.ts +38 -0
  63. package/dist/lib/fleet-shared-state.js +105 -0
  64. package/dist/lib/hosts/remote-cmd.d.ts +2 -0
  65. package/dist/lib/hosts/remote-cmd.js +12 -3
  66. package/dist/lib/lock-compromise.d.ts +8 -0
  67. package/dist/lib/lock-compromise.js +12 -0
  68. package/dist/lib/monitors/engine.d.ts +2 -1
  69. package/dist/lib/monitors/engine.js +9 -2
  70. package/dist/lib/refresh-coordinator.js +2 -0
  71. package/dist/lib/run-trace-sync.d.ts +13 -0
  72. package/dist/lib/run-trace-sync.js +77 -0
  73. package/dist/lib/secrets/push.d.ts +10 -0
  74. package/dist/lib/secrets/push.js +86 -7
  75. package/dist/lib/secrets/remote.d.ts +18 -6
  76. package/dist/lib/secrets/remote.js +29 -4
  77. package/dist/lib/secrets/reserved-sync.d.ts +28 -27
  78. package/dist/lib/secrets/reserved-sync.js +120 -102
  79. package/dist/lib/session/active.d.ts +3 -1
  80. package/dist/lib/session/active.js +2 -0
  81. package/dist/lib/session/actor-sidecar.d.ts +12 -0
  82. package/dist/lib/session/actor-sidecar.js +2 -0
  83. package/dist/lib/session/db.d.ts +5 -2
  84. package/dist/lib/session/db.js +54 -22
  85. package/dist/lib/session/discover.d.ts +12 -3
  86. package/dist/lib/session/discover.js +106 -25
  87. package/dist/lib/session/prompt.d.ts +11 -0
  88. package/dist/lib/session/prompt.js +25 -0
  89. package/dist/lib/session/recovery.d.ts +30 -2
  90. package/dist/lib/session/recovery.js +56 -4
  91. package/dist/lib/session/remote/remote-list.d.ts +17 -1
  92. package/dist/lib/session/remote/remote-list.js +29 -4
  93. package/dist/lib/session/remote/watch.d.ts +25 -2
  94. package/dist/lib/session/remote/watch.js +185 -11
  95. package/dist/lib/session/session-cache.d.ts +2 -1
  96. package/dist/lib/session/session-cache.js +1 -0
  97. package/dist/lib/session/types.d.ts +2 -0
  98. package/dist/lib/share/publish.d.ts +55 -9
  99. package/dist/lib/share/publish.js +87 -9
  100. package/dist/lib/share/worker-template.js +100 -8
  101. package/dist/lib/ssh-exec.d.ts +2 -0
  102. package/dist/lib/ssh-exec.js +20 -4
  103. package/dist/lib/teams/registry.js +2 -0
  104. package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
  105. package/dist/lib/testdata/daemon-health-writer.js +8 -0
  106. package/dist/lib/traces/sync.d.ts +7 -0
  107. package/dist/lib/traces/sync.js +9 -0
  108. package/dist/lib/usage-refresh.d.ts +8 -2
  109. package/dist/lib/usage-refresh.js +3 -3
  110. package/package.json +1 -1
  111. package/dist/lib/account-state-service.d.ts +0 -21
  112. 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>;
@@ -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
- tty?: boolean;
58
- input?: string;
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 { DeviceProbe } from '../fleet/types.js';
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
- remoteHasAuth: boolean;
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
- * Map a `fleet apply` device probe onto the auth-sync skip/push inputs.
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>;