@phnx-labs/agents-cli 1.20.65 → 1.20.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +119 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.d.ts +48 -0
- package/dist/commands/exec.js +65 -0
- package/dist/commands/monitors.js +8 -0
- package/dist/commands/plugins.js +28 -7
- package/dist/commands/sessions-browser.d.ts +82 -0
- package/dist/commands/sessions-browser.js +320 -0
- package/dist/commands/sessions.d.ts +1 -0
- package/dist/commands/sessions.js +121 -4
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +150 -0
- package/dist/commands/ssh.js +9 -1
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +28 -0
- package/dist/lib/agents.js +68 -0
- package/dist/lib/devices/connect.d.ts +18 -1
- package/dist/lib/devices/connect.js +10 -2
- package/dist/lib/devices/known-hosts.d.ts +62 -0
- package/dist/lib/devices/known-hosts.js +137 -0
- package/dist/lib/hosts/dispatch.js +20 -2
- package/dist/lib/hosts/remote-cmd.js +8 -4
- package/dist/lib/monitors/engine.js +4 -0
- package/dist/lib/monitors/sources/device.js +13 -3
- package/dist/lib/picker.d.ts +53 -0
- package/dist/lib/picker.js +214 -1
- package/dist/lib/plugins.d.ts +31 -1
- package/dist/lib/plugins.js +74 -13
- package/dist/lib/secrets/agent.d.ts +35 -0
- package/dist/lib/secrets/agent.js +114 -55
- package/dist/lib/secrets/filestore.d.ts +9 -0
- package/dist/lib/secrets/filestore.js +21 -8
- package/dist/lib/secrets/index.d.ts +7 -0
- package/dist/lib/secrets/index.js +26 -6
- package/dist/lib/share/capture.d.ts +29 -0
- package/dist/lib/share/capture.js +140 -0
- package/dist/lib/share/config.d.ts +35 -0
- package/dist/lib/share/config.js +100 -0
- package/dist/lib/share/og.d.ts +25 -0
- package/dist/lib/share/og.js +84 -0
- package/dist/lib/share/provision.d.ts +10 -0
- package/dist/lib/share/provision.js +91 -0
- package/dist/lib/share/publish.d.ts +57 -0
- package/dist/lib/share/publish.js +145 -0
- package/dist/lib/share/worker-template.d.ts +2 -0
- package/dist/lib/share/worker-template.js +82 -0
- package/dist/lib/shims.d.ts +13 -0
- package/dist/lib/shims.js +42 -2
- package/dist/lib/ssh-exec.d.ts +24 -0
- package/dist/lib/ssh-exec.js +15 -3
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/types.d.ts +10 -0
- package/package.json +1 -1
package/dist/lib/picker.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* display, and keyboard navigation. Used by sessions, teams, and other
|
|
13
13
|
* interactive pickers throughout the CLI.
|
|
14
14
|
*/
|
|
15
|
-
import { createPrompt, useState, useKeypress, useEffect, useMemo, usePagination, usePrefix, makeTheme, isEnterKey, isUpKey, isDownKey, isSpaceKey, Separator, } from '@inquirer/core';
|
|
15
|
+
import { createPrompt, useState, useKeypress, useEffect, useMemo, useRef, usePagination, usePrefix, makeTheme, isEnterKey, isUpKey, isDownKey, isSpaceKey, isBackspaceKey, Separator, } from '@inquirer/core';
|
|
16
16
|
import chalk from 'chalk';
|
|
17
17
|
import { stripVTControlCharacters } from 'node:util';
|
|
18
18
|
const DEFAULT_TERMINAL_ROWS = 24;
|
|
@@ -324,3 +324,216 @@ export function multiItemPicker(config) {
|
|
|
324
324
|
});
|
|
325
325
|
return prompt(config);
|
|
326
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Async-refetch variant of {@link itemPicker}. Holds a `filter` object in state and
|
|
329
|
+
* re-runs `load(filter)` whenever a keybinding mutates it (with a loading placeholder
|
|
330
|
+
* while the fetch — e.g. an SSH fleet fan-out — is in flight). A separate `S` search
|
|
331
|
+
* mode filters the loaded pool client-side. `enter` returns the active row + the live
|
|
332
|
+
* filter; `esc` cancels (from search mode, `esc` first exits search).
|
|
333
|
+
*
|
|
334
|
+
* Same render/pagination/preview machinery as the static pickers — only the data
|
|
335
|
+
* source and keymap are dynamic.
|
|
336
|
+
*/
|
|
337
|
+
export function dynamicPicker(config) {
|
|
338
|
+
const prompt = createPrompt((cfg, done) => {
|
|
339
|
+
const theme = makeTheme({});
|
|
340
|
+
const [status, setStatus] = useState('idle');
|
|
341
|
+
const [filter, setFilter] = useState(() => cfg.initialFilter);
|
|
342
|
+
const [items, setItems] = useState([]);
|
|
343
|
+
const [loading, setLoading] = useState(true);
|
|
344
|
+
const [query, setQuery] = useState('');
|
|
345
|
+
const [mode, setMode] = useState('nav');
|
|
346
|
+
const [previewOpen, setPreviewOpen] = useState(false);
|
|
347
|
+
const [active, setActive] = useState(0);
|
|
348
|
+
const [flash, setFlash] = useState('');
|
|
349
|
+
const prefix = usePrefix({ status, theme });
|
|
350
|
+
// Guards against a slow load resolving after a newer filter superseded it.
|
|
351
|
+
const gen = useRef(0);
|
|
352
|
+
useEffect(() => {
|
|
353
|
+
const my = ++gen.current;
|
|
354
|
+
setLoading(true);
|
|
355
|
+
Promise.resolve(cfg.load(filter))
|
|
356
|
+
.then((rows) => {
|
|
357
|
+
if (my !== gen.current)
|
|
358
|
+
return;
|
|
359
|
+
setItems(rows);
|
|
360
|
+
setLoading(false);
|
|
361
|
+
setActive(0);
|
|
362
|
+
})
|
|
363
|
+
.catch(() => {
|
|
364
|
+
if (my !== gen.current)
|
|
365
|
+
return;
|
|
366
|
+
setItems([]);
|
|
367
|
+
setLoading(false);
|
|
368
|
+
});
|
|
369
|
+
}, [filter]);
|
|
370
|
+
const results = useMemo(() => {
|
|
371
|
+
const q = query.trim();
|
|
372
|
+
const pool = q && cfg.matches ? items.filter((it) => cfg.matches(it, q)) : items;
|
|
373
|
+
return pool.slice(0, 200).map((item) => ({
|
|
374
|
+
value: item,
|
|
375
|
+
label: cfg.labelFor(item, q),
|
|
376
|
+
}));
|
|
377
|
+
}, [items, query]);
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
if (active >= results.length)
|
|
380
|
+
setActive(0);
|
|
381
|
+
}, [results]);
|
|
382
|
+
const selected = results[active];
|
|
383
|
+
const finish = () => {
|
|
384
|
+
if (!selected)
|
|
385
|
+
return;
|
|
386
|
+
setStatus('done');
|
|
387
|
+
done({ item: selected.value, filter });
|
|
388
|
+
};
|
|
389
|
+
useKeypress((key, rl) => {
|
|
390
|
+
if (isEnterKey(key)) {
|
|
391
|
+
finish();
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// Search mode: we own the query buffer (readline's line doesn't survive
|
|
395
|
+
// across renders here, so we build it from key events). Clear readline
|
|
396
|
+
// every keystroke so nothing leaks, then append the typed character.
|
|
397
|
+
if (mode === 'search') {
|
|
398
|
+
if (key.name === 'escape') {
|
|
399
|
+
// Exit search but KEEP the query as an active filter, so hotkeys (and
|
|
400
|
+
// the y copy-cmd) operate on the searched view. A second esc in nav
|
|
401
|
+
// clears it. Enter also confirms the highlighted row directly.
|
|
402
|
+
rl.clearLine(0);
|
|
403
|
+
setMode('nav');
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (isUpKey(key)) {
|
|
407
|
+
rl.clearLine(0);
|
|
408
|
+
if (results.length > 0)
|
|
409
|
+
setActive((active - 1 + results.length) % results.length);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (isDownKey(key)) {
|
|
413
|
+
rl.clearLine(0);
|
|
414
|
+
if (results.length > 0)
|
|
415
|
+
setActive((active + 1) % results.length);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (isBackspaceKey(key)) {
|
|
419
|
+
rl.clearLine(0);
|
|
420
|
+
setQuery(query.slice(0, -1));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const seq = key.sequence;
|
|
424
|
+
rl.clearLine(0);
|
|
425
|
+
if (seq && seq.length === 1 && seq >= ' ' && !key.ctrl) {
|
|
426
|
+
setQuery(query + seq);
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
// Nav mode: single keys are hotkeys — clear the readline buffer so a hotkey
|
|
431
|
+
// letter (r/b/c/…) never accumulates as stray input.
|
|
432
|
+
rl.clearLine(0);
|
|
433
|
+
if (key.name === 'escape') {
|
|
434
|
+
// First esc clears an active search filter; a second (no filter) cancels.
|
|
435
|
+
if (query) {
|
|
436
|
+
setQuery('');
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
done(null);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (isUpKey(key)) {
|
|
443
|
+
if (results.length > 0)
|
|
444
|
+
setActive((active - 1 + results.length) % results.length);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (isDownKey(key)) {
|
|
448
|
+
if (results.length > 0)
|
|
449
|
+
setActive((active + 1) % results.length);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (flash)
|
|
453
|
+
setFlash('');
|
|
454
|
+
if (key.name === (cfg.searchKey ?? 's')) {
|
|
455
|
+
setMode('search');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (cfg.buildPreview && key.name === (cfg.previewKey ?? 'tab')) {
|
|
459
|
+
setPreviewOpen(!previewOpen);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const binding = cfg.keyBindings?.[key.name ?? ''];
|
|
463
|
+
if (binding) {
|
|
464
|
+
const next = binding(filter);
|
|
465
|
+
if (!Object.is(next, filter))
|
|
466
|
+
setFilter(next);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (cfg.onKey) {
|
|
470
|
+
const msg = cfg.onKey(key.name ?? '', filter, selected?.value, query);
|
|
471
|
+
if (msg)
|
|
472
|
+
setFlash(msg);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
const message = theme.style.message(cfg.message, status);
|
|
476
|
+
if (status === 'done') {
|
|
477
|
+
return `${prefix} ${message}`;
|
|
478
|
+
}
|
|
479
|
+
const headerBits = [prefix, message];
|
|
480
|
+
if (cfg.headerFor)
|
|
481
|
+
headerBits.push(chalk.gray(cfg.headerFor(filter)));
|
|
482
|
+
if (mode === 'search') {
|
|
483
|
+
headerBits.push(query ? chalk.cyan('/' + query) : chalk.gray('/ (type to filter)'));
|
|
484
|
+
}
|
|
485
|
+
else if (query) {
|
|
486
|
+
headerBits.push(chalk.cyan('/' + query));
|
|
487
|
+
}
|
|
488
|
+
const header = headerBits.filter(Boolean).join(' ');
|
|
489
|
+
const page = usePagination({
|
|
490
|
+
items: results,
|
|
491
|
+
active,
|
|
492
|
+
renderItem({ item, isActive }) {
|
|
493
|
+
if (Separator.isSeparator(item))
|
|
494
|
+
return ` ${item.separator}`;
|
|
495
|
+
const cursor = isActive ? chalk.cyan('>') : ' ';
|
|
496
|
+
const row = isActive ? chalk.bold(item.label) : item.label;
|
|
497
|
+
return `${cursor} ${row}`;
|
|
498
|
+
},
|
|
499
|
+
pageSize: cfg.pageSize ?? 12,
|
|
500
|
+
loop: false,
|
|
501
|
+
});
|
|
502
|
+
const help = chalk.gray(cfg.helpFor
|
|
503
|
+
? cfg.helpFor(filter, mode)
|
|
504
|
+
: mode === 'search'
|
|
505
|
+
? '↑↓ navigate · esc exit search · ⏎ ' + (cfg.enterHint ?? 'select')
|
|
506
|
+
: 's search · ↑↓ navigate · ⏎ ' + (cfg.enterHint ?? 'select') + ' · esc cancel');
|
|
507
|
+
const parts = [header];
|
|
508
|
+
if (loading) {
|
|
509
|
+
parts.push(chalk.gray(` ${cfg.loadingMessage ?? 'Loading…'}`));
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
parts.push(page);
|
|
513
|
+
if (results.length === 0) {
|
|
514
|
+
parts.push(chalk.gray(` ${cfg.emptyMessage ?? 'No matches.'}`));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (previewOpen && selected && cfg.buildPreview && !loading) {
|
|
518
|
+
const width = terminalWidth();
|
|
519
|
+
const separator = chalk.gray('─'.repeat(Math.min(width, 80)));
|
|
520
|
+
const flashRows = flash ? renderedRows(flash, width) : 0;
|
|
521
|
+
const fixedRows = renderedRows(header, width) +
|
|
522
|
+
renderedRows(parts.slice(1).join('\n'), width) +
|
|
523
|
+
renderedRows(separator, width) +
|
|
524
|
+
renderedRows(help, width) +
|
|
525
|
+
flashRows;
|
|
526
|
+
const availablePreviewRows = terminalRows() - fixedRows;
|
|
527
|
+
const preview = limitPreviewHeight(cfg.buildPreview(selected.value), availablePreviewRows, width);
|
|
528
|
+
if (preview) {
|
|
529
|
+
parts.push(separator);
|
|
530
|
+
parts.push(preview);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (flash)
|
|
534
|
+
parts.push(chalk.green(flash));
|
|
535
|
+
parts.push(help);
|
|
536
|
+
return [header, parts.slice(1).join('\n')];
|
|
537
|
+
});
|
|
538
|
+
return prompt(config);
|
|
539
|
+
}
|
package/dist/lib/plugins.d.ts
CHANGED
|
@@ -316,7 +316,37 @@ export declare function getUpstreamManifestVersion(info: PluginSourceInfo): stri
|
|
|
316
316
|
* Update an installed plugin by re-pulling from its original source.
|
|
317
317
|
* Returns true if the update succeeded.
|
|
318
318
|
*/
|
|
319
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Labels of exec surfaces present in `after` that were NOT present in `before`.
|
|
321
|
+
* Used by updatePlugin to distinguish a newly-appearing execution surface
|
|
322
|
+
* (upstream compromise → renewed consent required) from one the user already
|
|
323
|
+
* trusted (leave enablement alone).
|
|
324
|
+
*/
|
|
325
|
+
export declare function newExecSurfaceLabels(before: PluginCapabilities, after: PluginCapabilities): string[];
|
|
326
|
+
/**
|
|
327
|
+
* Re-fetch a plugin from its recorded source and apply the update to disk.
|
|
328
|
+
*
|
|
329
|
+
* Security (RUSH-1757): a plugin's upstream is mutable. `updatePlugin` never
|
|
330
|
+
* mutates the live plugin tree before it has inspected the incoming content —
|
|
331
|
+
* the new revision is fetched into a **quarantine** dir first, its capabilities
|
|
332
|
+
* are diffed against the current on-disk baseline, and the update is applied to
|
|
333
|
+
* `plugin.root` only after the trust decision. If the update introduces a NEW
|
|
334
|
+
* executable surface (hooks/, .mcp.json, bin/, scripts/, settings.json,
|
|
335
|
+
* permissions/) that the current revision did not carry, the update is refused
|
|
336
|
+
* unless `options.allowExecSurfaces` is set — the last-good content is kept in
|
|
337
|
+
* place, so a benign-then-compromised upstream can never execute on the next
|
|
338
|
+
* update without renewed consent. A surface the user already trusted is not a
|
|
339
|
+
* "new" surface and does not re-trigger the gate.
|
|
340
|
+
*/
|
|
341
|
+
export declare function updatePlugin(name: string, options?: {
|
|
342
|
+
allowExecSurfaces?: boolean;
|
|
343
|
+
}): Promise<{
|
|
320
344
|
success: boolean;
|
|
321
345
|
error?: string;
|
|
346
|
+
/** True when the update was refused because it introduced new exec surfaces. */
|
|
347
|
+
blockedByExecSurfaces?: boolean;
|
|
348
|
+
/** Labels of exec surfaces newly introduced by this update, if any. */
|
|
349
|
+
newExecSurfaces?: string[];
|
|
350
|
+
/** Whether the applied revision carries executable surfaces (for the caller's re-sync). */
|
|
351
|
+
hasExecSurfaces?: boolean;
|
|
322
352
|
}>;
|
package/dist/lib/plugins.js
CHANGED
|
@@ -1739,7 +1739,33 @@ export function getUpstreamManifestVersion(info) {
|
|
|
1739
1739
|
* Update an installed plugin by re-pulling from its original source.
|
|
1740
1740
|
* Returns true if the update succeeded.
|
|
1741
1741
|
*/
|
|
1742
|
-
|
|
1742
|
+
/**
|
|
1743
|
+
* Labels of exec surfaces present in `after` that were NOT present in `before`.
|
|
1744
|
+
* Used by updatePlugin to distinguish a newly-appearing execution surface
|
|
1745
|
+
* (upstream compromise → renewed consent required) from one the user already
|
|
1746
|
+
* trusted (leave enablement alone).
|
|
1747
|
+
*/
|
|
1748
|
+
export function newExecSurfaceLabels(before, after) {
|
|
1749
|
+
return Object.keys(PLUGIN_EXEC_SURFACE_LABELS)
|
|
1750
|
+
.filter((key) => after[key] && !before[key])
|
|
1751
|
+
.map((key) => PLUGIN_EXEC_SURFACE_LABELS[key]);
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Re-fetch a plugin from its recorded source and apply the update to disk.
|
|
1755
|
+
*
|
|
1756
|
+
* Security (RUSH-1757): a plugin's upstream is mutable. `updatePlugin` never
|
|
1757
|
+
* mutates the live plugin tree before it has inspected the incoming content —
|
|
1758
|
+
* the new revision is fetched into a **quarantine** dir first, its capabilities
|
|
1759
|
+
* are diffed against the current on-disk baseline, and the update is applied to
|
|
1760
|
+
* `plugin.root` only after the trust decision. If the update introduces a NEW
|
|
1761
|
+
* executable surface (hooks/, .mcp.json, bin/, scripts/, settings.json,
|
|
1762
|
+
* permissions/) that the current revision did not carry, the update is refused
|
|
1763
|
+
* unless `options.allowExecSurfaces` is set — the last-good content is kept in
|
|
1764
|
+
* place, so a benign-then-compromised upstream can never execute on the next
|
|
1765
|
+
* update without renewed consent. A surface the user already trusted is not a
|
|
1766
|
+
* "new" surface and does not re-trigger the gate.
|
|
1767
|
+
*/
|
|
1768
|
+
export async function updatePlugin(name, options = {}) {
|
|
1743
1769
|
const plugin = getPlugin(name);
|
|
1744
1770
|
if (!plugin) {
|
|
1745
1771
|
return { success: false, error: `Plugin '${name}' not found` };
|
|
@@ -1755,33 +1781,68 @@ export async function updatePlugin(name) {
|
|
|
1755
1781
|
catch {
|
|
1756
1782
|
return { success: false, error: `Could not read source info for '${name}'` };
|
|
1757
1783
|
}
|
|
1784
|
+
// Baseline: what the live (already-trusted) revision ships today.
|
|
1785
|
+
const before = inspectPluginCapabilities(plugin.root);
|
|
1786
|
+
// Quarantine dir lives beside plugin.root (same filesystem) so the final
|
|
1787
|
+
// apply can be a rename. The dot-prefix keeps it out of plugin discovery.
|
|
1788
|
+
const quarantine = path.join(path.dirname(plugin.root), `.${path.basename(plugin.root)}.update-quarantine`);
|
|
1789
|
+
const cleanupQuarantine = () => {
|
|
1790
|
+
try {
|
|
1791
|
+
fs.rmSync(quarantine, { recursive: true, force: true });
|
|
1792
|
+
}
|
|
1793
|
+
catch { /* best effort */ }
|
|
1794
|
+
};
|
|
1795
|
+
cleanupQuarantine();
|
|
1758
1796
|
try {
|
|
1797
|
+
// 1. Fetch the incoming revision into quarantine — never touch plugin.root yet.
|
|
1759
1798
|
if (sourceInfo.isGit) {
|
|
1760
|
-
|
|
1799
|
+
// Copy the working checkout (with its .git) and fast-forward the copy, so a
|
|
1800
|
+
// hostile upstream diff lands in the quarantine, not the live tree.
|
|
1801
|
+
fs.cpSync(plugin.root, quarantine, { recursive: true });
|
|
1802
|
+
execFileSync('git', ['-C', quarantine, 'pull', '--ff-only'], { stdio: 'pipe' });
|
|
1761
1803
|
}
|
|
1762
1804
|
else {
|
|
1763
1805
|
const resolvedSource = sourceInfo.source.replace(/^~/, homeDir());
|
|
1764
1806
|
if (!fs.existsSync(resolvedSource)) {
|
|
1807
|
+
cleanupQuarantine();
|
|
1765
1808
|
return { success: false, error: `Source path no longer exists: ${resolvedSource}` };
|
|
1766
1809
|
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1810
|
+
fs.cpSync(resolvedSource, quarantine, { recursive: true });
|
|
1811
|
+
}
|
|
1812
|
+
// 2. Diff capabilities of the incoming revision against the baseline.
|
|
1813
|
+
const after = inspectPluginCapabilities(quarantine);
|
|
1814
|
+
const newSurfaces = newExecSurfaceLabels(before, after);
|
|
1815
|
+
// 3. Refuse a surface-introducing update without renewed consent. The
|
|
1816
|
+
// last-good content stays in place untouched.
|
|
1817
|
+
if (newSurfaces.length > 0 && options.allowExecSurfaces !== true) {
|
|
1818
|
+
cleanupQuarantine();
|
|
1819
|
+
return {
|
|
1820
|
+
success: false,
|
|
1821
|
+
blockedByExecSurfaces: true,
|
|
1822
|
+
newExecSurfaces: newSurfaces,
|
|
1823
|
+
error: `Update refused: '${name}' introduces new executable surfaces (${newSurfaces.join(', ')}). ` +
|
|
1824
|
+
`Re-run with --allow-exec-surfaces if you trust the source.`,
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
// 4. Apply: swap the quarantined revision into plugin.root, preserving the
|
|
1828
|
+
// user config and re-stamping .source.
|
|
1829
|
+
const userConfigPath = path.join(plugin.root, USER_CONFIG_FILE);
|
|
1830
|
+
const userConfigBackup = fs.existsSync(userConfigPath)
|
|
1831
|
+
? fs.readFileSync(userConfigPath, 'utf-8')
|
|
1832
|
+
: null;
|
|
1833
|
+
fs.rmSync(plugin.root, { recursive: true, force: true });
|
|
1834
|
+
fs.renameSync(quarantine, plugin.root);
|
|
1835
|
+
if (userConfigBackup !== null) {
|
|
1836
|
+
fs.writeFileSync(userConfigPath, userConfigBackup, 'utf-8');
|
|
1777
1837
|
}
|
|
1778
1838
|
// Re-stamp .source with the freshly pulled manifest version so the baseline
|
|
1779
1839
|
// tracks what's now on disk (keeps the heal "unmodified?" check honest).
|
|
1780
1840
|
const freshVersion = loadPluginManifest(plugin.root)?.version;
|
|
1781
1841
|
fs.writeFileSync(path.join(plugin.root, SOURCE_FILE), JSON.stringify({ ...sourceInfo, version: freshVersion }), 'utf-8');
|
|
1842
|
+
return { success: true, newExecSurfaces: newSurfaces, hasExecSurfaces: hasPluginExecSurfaces(after) };
|
|
1782
1843
|
}
|
|
1783
1844
|
catch (err) {
|
|
1845
|
+
cleanupQuarantine();
|
|
1784
1846
|
return { success: false, error: err.message };
|
|
1785
1847
|
}
|
|
1786
|
-
return { success: true };
|
|
1787
1848
|
}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* macOS only: Linux libsecret has no biometry prompt, so there's nothing to
|
|
24
24
|
* deduplicate — every entry point here no-ops off darwin.
|
|
25
25
|
*/
|
|
26
|
+
import * as net from 'net';
|
|
26
27
|
import type { SecretsBundle } from './bundles.js';
|
|
27
28
|
/** Default lifetime of an unlocked bundle when `--ttl` is not given. */
|
|
28
29
|
export declare const DEFAULT_TTL_MS: number;
|
|
@@ -74,6 +75,13 @@ export interface AgentStatusEntry {
|
|
|
74
75
|
expiresAt: number;
|
|
75
76
|
keyCount: number;
|
|
76
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Read the current broker capability token, or null if none is present. Clients
|
|
80
|
+
* read it fresh per request and attach it to every non-ping command; a broker
|
|
81
|
+
* restart mints a new token, so a stale read simply fails authorization and the
|
|
82
|
+
* caller falls back to a direct keychain read (soft, never a hard error).
|
|
83
|
+
*/
|
|
84
|
+
export declare function readAgentToken(): string | null;
|
|
77
85
|
/** True if a legacy standalone-broker launchd plist is still installed. */
|
|
78
86
|
export declare function secretsAgentServiceInstalled(): boolean;
|
|
79
87
|
/**
|
|
@@ -97,17 +105,21 @@ export type Request = {
|
|
|
97
105
|
} | {
|
|
98
106
|
cmd: 'get';
|
|
99
107
|
name: string;
|
|
108
|
+
token?: string;
|
|
100
109
|
} | {
|
|
101
110
|
cmd: 'load';
|
|
102
111
|
name: string;
|
|
103
112
|
bundle: SecretsBundle;
|
|
104
113
|
env: Record<string, string>;
|
|
105
114
|
ttlMs: number;
|
|
115
|
+
token?: string;
|
|
106
116
|
} | {
|
|
107
117
|
cmd: 'lock';
|
|
108
118
|
name?: string;
|
|
119
|
+
token?: string;
|
|
109
120
|
} | {
|
|
110
121
|
cmd: 'status';
|
|
122
|
+
token?: string;
|
|
111
123
|
};
|
|
112
124
|
export type Response = {
|
|
113
125
|
ok: true;
|
|
@@ -164,6 +176,28 @@ export declare function handleAgentRequest(store: Map<string, StoredBundle>, req
|
|
|
164
176
|
* has direct regression coverage (the inline stdout handler isn't unit-testable).
|
|
165
177
|
*/
|
|
166
178
|
export declare function shouldWipeOnWatchEvent(chunk: string): boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Authorization gate applied to every request BEFORE handleAgentRequest touches
|
|
181
|
+
* the store (RUSH-1760). A bare same-UID socket connection is no longer trusted
|
|
182
|
+
* to load/get arbitrary bundle env: each command except the liveness `ping` must
|
|
183
|
+
* carry the per-broker capability token, which lives in a 0600 file inside the
|
|
184
|
+
* 0700 agent dir and so is readable only by the UID that owns the broker.
|
|
185
|
+
*
|
|
186
|
+
* Fail closed: a missing/empty expected token (no token file) rejects every
|
|
187
|
+
* command but ping. `ping` stays unauthenticated — it exposes only the protocol
|
|
188
|
+
* and cli version, and clients need it to detect a reachable broker before they
|
|
189
|
+
* have any reason to read the token. Pure + exported for direct unit testing.
|
|
190
|
+
*/
|
|
191
|
+
export declare function isRequestAuthorized(req: Request, expectedToken: string | null): boolean;
|
|
192
|
+
type BrokerConnectionHandler = (conn: net.Socket) => void;
|
|
193
|
+
/**
|
|
194
|
+
* Build the socket `connection` handler shared by both brokers (standalone and
|
|
195
|
+
* daemon-hosted): newline-framed JSON in, one response line out, with the
|
|
196
|
+
* authorization gate (isRequestAuthorized) applied before `handle`. `token`
|
|
197
|
+
* resolves the currently-expected capability token per request so a token
|
|
198
|
+
* rotation on broker restart is picked up without rebuilding the handler.
|
|
199
|
+
*/
|
|
200
|
+
export declare function makeConnectionHandler(handle: (req: Request) => Response, token: () => string | null): BrokerConnectionHandler;
|
|
167
201
|
/**
|
|
168
202
|
* Run the broker in the foreground. Spawned detached by ensureAgentRunning via
|
|
169
203
|
* `agents secrets _agent-run`. Holds the store in memory, serves the socket,
|
|
@@ -325,3 +359,4 @@ export declare function agentPing(): Promise<{
|
|
|
325
359
|
* gets starved under heavy load, so it's last.
|
|
326
360
|
*/
|
|
327
361
|
export declare function ensureAgentRunning(timeoutMs?: number): Promise<boolean>;
|
|
362
|
+
export {};
|