@phnx-labs/agents-cli 1.22.66 → 1.22.69
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 +56 -0
- package/README.md +21 -8
- package/dist/bootstrap.js +6 -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 +50 -0
- package/dist/commands/feed.js +20 -7
- package/dist/commands/monitors.js +5 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- 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/send.js +29 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-optimize.js +1 -1
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/sessions.js +4 -11
- package/dist/commands/share.d.ts +5 -3
- package/dist/commands/share.js +73 -20
- package/dist/commands/ssh.js +205 -2
- 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 +18 -5
- package/dist/lib/accounting/usage.js +165 -20
- package/dist/lib/auth-health.d.ts +8 -0
- package/dist/lib/auth-health.js +4 -4
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- 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/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/claude-account-token.js +108 -4
- 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/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.js +15 -0
- package/dist/lib/feed/watch.d.ts +3 -0
- package/dist/lib/feed/watch.js +13 -3
- package/dist/lib/feed-broadcast.d.ts +64 -5
- package/dist/lib/feed-broadcast.js +124 -22
- 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 +27 -2
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/owner-message.d.ts +12 -0
- package/dist/lib/owner-message.js +44 -0
- package/dist/lib/refresh-coordinator.js +2 -0
- package/dist/lib/run-trace-sync.d.ts +28 -0
- package/dist/lib/run-trace-sync.js +99 -0
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- 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 +13 -1
- package/dist/lib/session/active.js +5 -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 +16 -2
- package/dist/lib/session/db.js +73 -22
- package/dist/lib/session/discover.d.ts +12 -3
- package/dist/lib/session/discover.js +187 -26
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +17 -0
- package/dist/lib/session/prompt.js +35 -0
- package/dist/lib/session/recovery.d.ts +43 -6
- package/dist/lib/session/recovery.js +80 -10
- 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 +188 -11
- package/dist/lib/session/session-cache.d.ts +2 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +67 -11
- package/dist/lib/share/publish.js +98 -16
- package/dist/lib/share/worker-template.js +105 -9
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/ssh-exec.d.ts +2 -0
- package/dist/lib/ssh-exec.js +20 -4
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- 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/backend.js +13 -2
- 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/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
- package/dist/lib/account-state-service.d.ts +0 -21
- package/dist/lib/account-state-service.js +0 -60
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* item and its keychain twin carry identical names:
|
|
22
22
|
* `agents-cli.bundles.<name>` and `agents-cli.secrets.<bundle>.<key>`.
|
|
23
23
|
*/
|
|
24
|
-
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
|
|
24
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync } from 'crypto';
|
|
25
25
|
import * as fs from 'fs';
|
|
26
26
|
import * as os from 'os';
|
|
27
27
|
import * as path from 'path';
|
|
@@ -210,6 +210,128 @@ export function decryptForFallback(enc, passphrase) {
|
|
|
210
210
|
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
211
211
|
return plaintext.toString('utf8');
|
|
212
212
|
}
|
|
213
|
+
// ---------- bundle-metadata plaintext cache (PHNX-3585) ----------
|
|
214
|
+
//
|
|
215
|
+
// Enumerating file-backed bundles — `agents secrets list`, and the
|
|
216
|
+
// account-rotation `readAccountRegistry` on the `agents run` hot path — decrypts
|
|
217
|
+
// EVERY bundle's metadata item, and each decrypt runs a fresh scrypt KDF (~12ms)
|
|
218
|
+
// because every `.enc` file carries its own random salt. On a box with dozens of
|
|
219
|
+
// bundles that is hundreds of ms of pure KDF before the harness even starts (the
|
|
220
|
+
// ~0.55s New-Claude boot cost in PHNX-3585).
|
|
221
|
+
//
|
|
222
|
+
// Bundle METADATA is non-secret by contract (secrets/bundles.ts `writeBundle`:
|
|
223
|
+
// "Metadata is non-sensitive by contract", stored no-ACL so `secrets list`
|
|
224
|
+
// enumerates without Touch ID) — the vars hold `keychain:`/`env:`/literal REFS,
|
|
225
|
+
// never the secret bytes, which live in separate `agents-cli.secrets.<bundle>.*`
|
|
226
|
+
// value items. So its decrypted JSON is safe to cache in cleartext, at the SAME
|
|
227
|
+
// 0600 protection as the ciphertext beside it.
|
|
228
|
+
//
|
|
229
|
+
// The cache keys each entry on the `.enc` file's (mtimeMs,size) plus a
|
|
230
|
+
// passphrase fingerprint, so ANY bundle write (new mtime/size) or passphrase
|
|
231
|
+
// change (new fingerprint; rotation also re-writes every file) misses and
|
|
232
|
+
// re-derives — no staleness, self-healing on any miss. It is scoped to the
|
|
233
|
+
// metadata namespace ONLY (`agents-cli.bundles.`); secret VALUE items are never
|
|
234
|
+
// read through it, so no secret is ever written to the cache.
|
|
235
|
+
const META_ITEM_PREFIX = 'agents-cli.bundles.';
|
|
236
|
+
function isMetaItem(item) {
|
|
237
|
+
return item.startsWith(META_ITEM_PREFIX);
|
|
238
|
+
}
|
|
239
|
+
let metaCache = null;
|
|
240
|
+
let metaCacheDirty = false;
|
|
241
|
+
let metaCacheFlushRegistered = false;
|
|
242
|
+
function metaCachePath() {
|
|
243
|
+
// A SIBLING of the store dir, never inside it — the same rule the store lock
|
|
244
|
+
// above follows, so a rotation's store-dir rename / `.rotate-*` sweep never
|
|
245
|
+
// moves or reaps it. Under `~/.agents/.cache/`, so it is machine-local and
|
|
246
|
+
// never enters `agents repo push/pull`.
|
|
247
|
+
return `${fileDir()}.meta-cache.json`;
|
|
248
|
+
}
|
|
249
|
+
function passphraseFingerprint() {
|
|
250
|
+
// Non-reversible tag, never the passphrase itself — so a passphrase change
|
|
251
|
+
// invalidates the cache without the key ever landing on disk in the clear.
|
|
252
|
+
return createHash('sha256').update(getPassphrase()).digest('hex').slice(0, 16);
|
|
253
|
+
}
|
|
254
|
+
function loadMetaCache() {
|
|
255
|
+
if (metaCache)
|
|
256
|
+
return metaCache;
|
|
257
|
+
const keyFp = passphraseFingerprint();
|
|
258
|
+
try {
|
|
259
|
+
const doc = JSON.parse(fs.readFileSync(metaCachePath(), 'utf8'));
|
|
260
|
+
if (doc && doc.v === 1 && doc.keyFp === keyFp && doc.entries && typeof doc.entries === 'object') {
|
|
261
|
+
metaCache = doc;
|
|
262
|
+
return metaCache;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
/* absent or corrupt — rebuild from scratch */
|
|
267
|
+
}
|
|
268
|
+
metaCache = { v: 1, keyFp, entries: {} };
|
|
269
|
+
return metaCache;
|
|
270
|
+
}
|
|
271
|
+
/** Decrypted metadata for `item` if the cache holds a fresh entry, else null. */
|
|
272
|
+
function metaCacheGet(item, fp) {
|
|
273
|
+
let st;
|
|
274
|
+
try {
|
|
275
|
+
st = fs.statSync(fp);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
const e = loadMetaCache().entries[item];
|
|
281
|
+
return e && e.mtimeMs === st.mtimeMs && e.size === st.size ? e.plain : null;
|
|
282
|
+
}
|
|
283
|
+
/** Record freshly-decrypted metadata for `item`, flushed once at process exit.
|
|
284
|
+
* `snapshot` is the pre-readFileSync stat — the identity of the bytes just
|
|
285
|
+
* decrypted. Skip the put if a post-decrypt stat does not match: a concurrent
|
|
286
|
+
* writer during scrypt must not pin that plaintext to the new file identity. */
|
|
287
|
+
function metaCachePut(item, fp, plain, snapshot) {
|
|
288
|
+
try {
|
|
289
|
+
const now = fs.statSync(fp);
|
|
290
|
+
if (now.mtimeMs !== snapshot.mtimeMs || now.size !== snapshot.size)
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
loadMetaCache().entries[item] = { mtimeMs: snapshot.mtimeMs, size: snapshot.size, plain };
|
|
297
|
+
metaCacheDirty = true;
|
|
298
|
+
if (!metaCacheFlushRegistered) {
|
|
299
|
+
metaCacheFlushRegistered = true;
|
|
300
|
+
process.on('exit', flushMetaCache);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/** Drop a cached entry the moment its file is rewritten or removed, so a
|
|
304
|
+
* same-process write→read never returns pre-write metadata (mtime/size alone
|
|
305
|
+
* can collide within a millisecond on an unchanged-length rewrite). */
|
|
306
|
+
function metaCacheEvict(item) {
|
|
307
|
+
if (!isMetaItem(item))
|
|
308
|
+
return;
|
|
309
|
+
const cache = metaCache;
|
|
310
|
+
if (cache && cache.entries[item]) {
|
|
311
|
+
delete cache.entries[item];
|
|
312
|
+
metaCacheDirty = true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function flushMetaCache() {
|
|
316
|
+
if (!metaCacheDirty || !metaCache)
|
|
317
|
+
return;
|
|
318
|
+
metaCacheDirty = false;
|
|
319
|
+
// Prune entries whose `.enc` file is gone so churn can't grow the cache without
|
|
320
|
+
// bound; a surviving stale entry would re-validate against mtime/size anyway.
|
|
321
|
+
for (const item of Object.keys(metaCache.entries)) {
|
|
322
|
+
if (!fs.existsSync(fileFor(item)))
|
|
323
|
+
delete metaCache.entries[item];
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
ensureFileDir();
|
|
327
|
+
const tmp = `${metaCachePath()}.${process.pid}.tmp`;
|
|
328
|
+
fs.writeFileSync(tmp, JSON.stringify(metaCache), { mode: 0o600 });
|
|
329
|
+
fs.renameSync(tmp, metaCachePath());
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
/* the cache is advisory — a write failure just re-derives next run */
|
|
333
|
+
}
|
|
334
|
+
}
|
|
213
335
|
// ---------- file backend ----------
|
|
214
336
|
function fileFor(item) {
|
|
215
337
|
return path.join(fileDir(), `${item}.enc`);
|
|
@@ -226,6 +348,26 @@ function fileGet(item) {
|
|
|
226
348
|
if (!fs.existsSync(fp)) {
|
|
227
349
|
throw new Error(`Secret '${item}' not found in encrypted store.`);
|
|
228
350
|
}
|
|
351
|
+
// Non-secret metadata short-circuits the scrypt decrypt when the file is
|
|
352
|
+
// unchanged (see the cache block above). Secret VALUE items skip the cache
|
|
353
|
+
// entirely, so they always decrypt fresh and never touch cleartext storage.
|
|
354
|
+
const metaItem = isMetaItem(item);
|
|
355
|
+
if (metaItem) {
|
|
356
|
+
const hit = metaCacheGet(item, fp);
|
|
357
|
+
if (hit !== null)
|
|
358
|
+
return hit;
|
|
359
|
+
}
|
|
360
|
+
// Identity of the bytes we are about to decrypt. A post-decrypt stat would
|
|
361
|
+
// attribute this plaintext to a concurrent writer's new file.
|
|
362
|
+
let snapshot;
|
|
363
|
+
if (metaItem) {
|
|
364
|
+
try {
|
|
365
|
+
snapshot = fs.statSync(fp);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
snapshot = undefined;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
229
371
|
const raw = fs.readFileSync(fp, 'utf8');
|
|
230
372
|
let parsed;
|
|
231
373
|
try {
|
|
@@ -234,12 +376,16 @@ function fileGet(item) {
|
|
|
234
376
|
catch {
|
|
235
377
|
throw new Error(`Encrypted secret file ${fp} is corrupt (not valid JSON).`);
|
|
236
378
|
}
|
|
379
|
+
let plain;
|
|
237
380
|
try {
|
|
238
|
-
|
|
381
|
+
plain = decryptForFallback(parsed, getPassphrase());
|
|
239
382
|
}
|
|
240
383
|
catch {
|
|
241
384
|
throw new Error(`Failed to decrypt '${item}'. Wrong AGENTS_SECRETS_PASSPHRASE or tampered file.`);
|
|
242
385
|
}
|
|
386
|
+
if (metaItem && snapshot)
|
|
387
|
+
metaCachePut(item, fp, plain, snapshot);
|
|
388
|
+
return plain;
|
|
243
389
|
}
|
|
244
390
|
function fileGetBatch(items) {
|
|
245
391
|
const out = new Map();
|
|
@@ -262,12 +408,13 @@ function fileSet(item, value) {
|
|
|
262
408
|
const enc = encryptForFallback(value, getPassphrase());
|
|
263
409
|
fs.writeFileSync(fileFor(item), JSON.stringify(enc), { mode: 0o600 });
|
|
264
410
|
});
|
|
411
|
+
metaCacheEvict(item);
|
|
265
412
|
}
|
|
266
413
|
function fileDelete(item) {
|
|
267
414
|
const fp = fileFor(item);
|
|
268
415
|
if (!fs.existsSync(fp))
|
|
269
416
|
return true; // idempotent, matches secret-tool clear
|
|
270
|
-
|
|
417
|
+
const deleted = withStoreLock(() => {
|
|
271
418
|
// Re-check under the lock — a rotation may have swapped the dir since the
|
|
272
419
|
// pre-lock existence probe above.
|
|
273
420
|
if (!fs.existsSync(fp))
|
|
@@ -275,6 +422,8 @@ function fileDelete(item) {
|
|
|
275
422
|
fs.unlinkSync(fp);
|
|
276
423
|
return true;
|
|
277
424
|
});
|
|
425
|
+
metaCacheEvict(item);
|
|
426
|
+
return deleted;
|
|
278
427
|
}
|
|
279
428
|
function fileList(prefix) {
|
|
280
429
|
const dir = fileDir();
|
|
@@ -921,6 +1070,18 @@ export function _resetFileStoreForTest(opts = {}) {
|
|
|
921
1070
|
warnedAutoPassphrase = false;
|
|
922
1071
|
lockAcquireTimeoutMsOverride = null;
|
|
923
1072
|
lockStaleMsOverride = null;
|
|
1073
|
+
// Drop the in-memory metadata cache so a test that repoints the store dir or
|
|
1074
|
+
// passphrase never reads another fixture's cached plaintext (the on-disk cache
|
|
1075
|
+
// is per-dir and keyed by the passphrase fingerprint, so it self-invalidates
|
|
1076
|
+
// too, but the process-level map must be cleared explicitly).
|
|
1077
|
+
metaCache = null;
|
|
1078
|
+
metaCacheDirty = false;
|
|
1079
|
+
}
|
|
1080
|
+
/** Test-only: flush the in-memory metadata cache to disk now. Production flushes
|
|
1081
|
+
* once at process exit; a test needs the on-disk copy mid-run to simulate the
|
|
1082
|
+
* next `agents` process reading it. */
|
|
1083
|
+
export function _flushMetaCacheForTest() {
|
|
1084
|
+
flushMetaCache();
|
|
924
1085
|
}
|
|
925
1086
|
/** Test-only: shorten the store-lock acquire timeout so a contended-lock assertion
|
|
926
1087
|
* fails fast instead of waiting out the 30s production budget. */
|
|
@@ -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>;
|