@openparachute/vault 0.5.2-rc.4 → 0.5.2-rc.5
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/package.json +1 -1
- package/src/autostart.test.ts +75 -0
- package/src/autostart.ts +84 -0
- package/src/cli.ts +76 -28
- package/src/mcp-install.ts +9 -2
- package/web/ui/dist/assets/{index-C4Sth1Tq.js → index-D8nCVT1e.js} +11 -11
- package/web/ui/dist/index.html +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { decideAutostart } from "./autostart.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pure matrix for the autostart decision (ParachuteComputer/parachute-hub#580
|
|
6
|
+
* item 2). No launchd/systemd is touched — `decideAutostart` is side-effect
|
|
7
|
+
* free; the CLI consumes its result to register or skip.
|
|
8
|
+
*/
|
|
9
|
+
describe("decideAutostart", () => {
|
|
10
|
+
test("hub present, no flag, no persisted → default OFF (#580)", () => {
|
|
11
|
+
const d = decideAutostart({ flagOn: false, flagOff: false, persisted: undefined, hubPresent: true });
|
|
12
|
+
expect(d.enabled).toBe(false);
|
|
13
|
+
expect(d.reason).toBe("hub-default-off");
|
|
14
|
+
// Per-run inference — not persisted, so a later standalone re-run registers.
|
|
15
|
+
expect(d.persist).toBe(false);
|
|
16
|
+
expect(d.overrodeHub).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("hub absent, no flag, no persisted → default ON (standalone)", () => {
|
|
20
|
+
const d = decideAutostart({ flagOn: false, flagOff: false, persisted: undefined, hubPresent: false });
|
|
21
|
+
expect(d.enabled).toBe(true);
|
|
22
|
+
expect(d.reason).toBe("default-on");
|
|
23
|
+
expect(d.persist).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("explicit --autostart forces ON even under a hub (operator override + warn flag)", () => {
|
|
27
|
+
const d = decideAutostart({ flagOn: true, flagOff: false, persisted: undefined, hubPresent: true });
|
|
28
|
+
expect(d.enabled).toBe(true);
|
|
29
|
+
expect(d.reason).toBe("flag-on");
|
|
30
|
+
expect(d.persist).toBe(true);
|
|
31
|
+
expect(d.overrodeHub).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("explicit --autostart with no hub does not set overrodeHub", () => {
|
|
35
|
+
const d = decideAutostart({ flagOn: true, flagOff: false, persisted: undefined, hubPresent: false });
|
|
36
|
+
expect(d.enabled).toBe(true);
|
|
37
|
+
expect(d.overrodeHub).toBe(false);
|
|
38
|
+
expect(d.persist).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("explicit --no-autostart forces OFF and persists (even under a hub)", () => {
|
|
42
|
+
const d = decideAutostart({ flagOn: false, flagOff: true, persisted: undefined, hubPresent: true });
|
|
43
|
+
expect(d.enabled).toBe(false);
|
|
44
|
+
expect(d.reason).toBe("flag-off");
|
|
45
|
+
expect(d.persist).toBe(true);
|
|
46
|
+
expect(d.overrodeHub).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("--no-autostart wins over --autostart on the same line (safer default)", () => {
|
|
50
|
+
const d = decideAutostart({ flagOn: true, flagOff: true, persisted: undefined, hubPresent: false });
|
|
51
|
+
expect(d.enabled).toBe(false);
|
|
52
|
+
expect(d.reason).toBe("flag-off");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("persisted=false honored over hub-present default", () => {
|
|
56
|
+
const d = decideAutostart({ flagOn: false, flagOff: false, persisted: false, hubPresent: true });
|
|
57
|
+
expect(d.enabled).toBe(false);
|
|
58
|
+
expect(d.reason).toBe("persisted");
|
|
59
|
+
expect(d.persist).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("persisted=true honored even when a hub is present (prior explicit choice)", () => {
|
|
63
|
+
const d = decideAutostart({ flagOn: false, flagOff: false, persisted: true, hubPresent: true });
|
|
64
|
+
expect(d.enabled).toBe(true);
|
|
65
|
+
expect(d.reason).toBe("persisted");
|
|
66
|
+
expect(d.persist).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("flag beats persisted: --no-autostart over persisted=true", () => {
|
|
70
|
+
const d = decideAutostart({ flagOn: false, flagOff: true, persisted: true, hubPresent: false });
|
|
71
|
+
expect(d.enabled).toBe(false);
|
|
72
|
+
expect(d.reason).toBe("flag-off");
|
|
73
|
+
expect(d.persist).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
package/src/autostart.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether `parachute-vault init` should register a boot/restart daemon
|
|
3
|
+
* (launchd on macOS, systemd on Linux).
|
|
4
|
+
*
|
|
5
|
+
* Pure: extracted so the flag × persisted-config × hub-presence matrix can be
|
|
6
|
+
* unit-tested without spawning the CLI or touching launchd/systemd. The caller
|
|
7
|
+
* passes in the resolved `hubPresent` signal (from `detectHubPresence`) so this
|
|
8
|
+
* function stays side-effect-free.
|
|
9
|
+
*
|
|
10
|
+
* Why hub-presence flips the default (ParachuteComputer/parachute-hub#580):
|
|
11
|
+
* under hub-as-supervisor the hub owns the vault lifecycle — it spawns vault as
|
|
12
|
+
* a supervised child. If init *also* registers a launchd/systemd unit with
|
|
13
|
+
* `KeepAlive`/`RunAtLoad`, two lifecycles race for :1940: the supervisor's
|
|
14
|
+
* child and the platform manager's respawn. `parachute stop` kills the
|
|
15
|
+
* supervised one, launchd resurrects the other (EADDRINUSE crash-loop, pidfile
|
|
16
|
+
* records the loser, the rogue holds the port with no injected
|
|
17
|
+
* PARACHUTE_HUB_ORIGIN → iss mismatch). Defaulting autostart OFF when a hub is
|
|
18
|
+
* present makes the standalone daemon an explicit opt-in.
|
|
19
|
+
*
|
|
20
|
+
* Precedence (first match wins):
|
|
21
|
+
* 1. `--no-autostart` on this run → off (persisted; safer-default
|
|
22
|
+
* precedence beats --autostart)
|
|
23
|
+
* 2. `--autostart` on this run → on (persisted; operator
|
|
24
|
+
* override — caller warns if a
|
|
25
|
+
* supervised hub was detected)
|
|
26
|
+
* 3. Existing `config.autostart` (boolean) → that value (honor prior choice)
|
|
27
|
+
* 4. Hub present, no flag, no persisted value → off (the hub supervisor owns
|
|
28
|
+
* the lifecycle — #580)
|
|
29
|
+
* 5. Default → on (standalone deploys
|
|
30
|
+
* genuinely need a daemon)
|
|
31
|
+
*
|
|
32
|
+
* `persist` is true only when the choice came from an explicit flag (cases 1+2)
|
|
33
|
+
* — matching the prior behavior where a flagless re-run never rewrote the
|
|
34
|
+
* persisted value. The hub-present default (case 4) is intentionally NOT
|
|
35
|
+
* persisted: it's a per-run inference, so a later standalone re-run (no hub)
|
|
36
|
+
* falls back to the register default rather than being stuck off.
|
|
37
|
+
*
|
|
38
|
+
* `overrodeHub` is true only for case 2 when a hub was detected — the signal the
|
|
39
|
+
* caller uses to log the "supervised hub detected, registering anyway" warning.
|
|
40
|
+
*/
|
|
41
|
+
export interface AutostartDecisionInput {
|
|
42
|
+
/** `--autostart` present on this invocation. */
|
|
43
|
+
flagOn: boolean;
|
|
44
|
+
/** `--no-autostart` present on this invocation. */
|
|
45
|
+
flagOff: boolean;
|
|
46
|
+
/** Persisted `config.autostart` from a prior run, if a boolean. */
|
|
47
|
+
persisted?: boolean | undefined;
|
|
48
|
+
/** Whether a hub supervisor was detected (from `detectHubPresence`). */
|
|
49
|
+
hubPresent: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AutostartDecision {
|
|
53
|
+
/** Final resolved value. */
|
|
54
|
+
enabled: boolean;
|
|
55
|
+
/** Whether the caller should write `config.autostart` to disk. */
|
|
56
|
+
persist: boolean;
|
|
57
|
+
/** True when `--autostart` forced registration despite a detected hub. */
|
|
58
|
+
overrodeHub: boolean;
|
|
59
|
+
/** Which precedence rule decided the outcome (for testing + copy). */
|
|
60
|
+
reason: "flag-off" | "flag-on" | "persisted" | "hub-default-off" | "default-on";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function decideAutostart(input: AutostartDecisionInput): AutostartDecision {
|
|
64
|
+
const { flagOn, flagOff, persisted, hubPresent } = input;
|
|
65
|
+
|
|
66
|
+
if (flagOff) {
|
|
67
|
+
return { enabled: false, persist: true, overrodeHub: false, reason: "flag-off" };
|
|
68
|
+
}
|
|
69
|
+
if (flagOn) {
|
|
70
|
+
return {
|
|
71
|
+
enabled: true,
|
|
72
|
+
persist: true,
|
|
73
|
+
overrodeHub: hubPresent,
|
|
74
|
+
reason: "flag-on",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (typeof persisted === "boolean") {
|
|
78
|
+
return { enabled: persisted, persist: false, overrodeHub: false, reason: "persisted" };
|
|
79
|
+
}
|
|
80
|
+
if (hubPresent) {
|
|
81
|
+
return { enabled: false, persist: false, overrodeHub: false, reason: "hub-default-off" };
|
|
82
|
+
}
|
|
83
|
+
return { enabled: true, persist: false, overrodeHub: false, reason: "default-on" };
|
|
84
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -104,6 +104,7 @@ import { resolveBindHostname } from "./bind.ts";
|
|
|
104
104
|
import { listTokens, revokeToken, migrateVaultKeys } from "./token-store.ts";
|
|
105
105
|
import { VAULT_SCOPES } from "./scopes.ts";
|
|
106
106
|
import { validateVaultName, decideInitVaultName } from "./vault-name.ts";
|
|
107
|
+
import { decideAutostart } from "./autostart.ts";
|
|
107
108
|
import { getVaultStore } from "./vault-store.ts";
|
|
108
109
|
import {
|
|
109
110
|
defaultMirrorConfig,
|
|
@@ -275,12 +276,16 @@ async function cmdInit(args: string[] = []) {
|
|
|
275
276
|
const flagMcpOff = args.includes("--no-mcp");
|
|
276
277
|
const flagTokenOn = args.includes("--token");
|
|
277
278
|
const flagTokenOff = args.includes("--no-token");
|
|
278
|
-
// --autostart / --no-autostart toggle daemon registration.
|
|
279
|
-
// (
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
279
|
+
// --autostart / --no-autostart toggle daemon registration. The default is
|
|
280
|
+
// context-aware (resolved by decideAutostart below): ON for standalone
|
|
281
|
+
// deploys, but OFF when a hub supervisor is detected, since the hub owns
|
|
282
|
+
// vault's lifecycle and a launchd/systemd unit would race it for :1940
|
|
283
|
+
// (ParachuteComputer/parachute-hub#580). --no-autostart always skips
|
|
284
|
+
// registering AND removes any prior registration — for CI, dev sandboxes,
|
|
285
|
+
// Docker, or any environment where another supervisor manages the process.
|
|
286
|
+
// --autostart forces registration even under a hub (logged with a warning).
|
|
287
|
+
// --no-autostart wins over --autostart on the same command line
|
|
288
|
+
// (safer-default precedence).
|
|
284
289
|
const flagAutostartOn = args.includes("--autostart");
|
|
285
290
|
const flagAutostartOff = args.includes("--no-autostart");
|
|
286
291
|
|
|
@@ -417,37 +422,73 @@ async function cmdInit(args: string[] = []) {
|
|
|
417
422
|
// a folder move; this refreshes ~/.parachute/server-path and bounces the
|
|
418
423
|
// daemon so the new location takes effect immediately.
|
|
419
424
|
//
|
|
420
|
-
// Autostart precedence
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
// 3. Existing config.autostart
|
|
425
|
-
// 4.
|
|
425
|
+
// Autostart precedence is resolved by `decideAutostart` (pure, unit-tested):
|
|
426
|
+
// 1. --no-autostart on this run → false (and persisted)
|
|
427
|
+
// 2. --autostart on this run → true (and persisted; warns
|
|
428
|
+
// if a supervised hub was seen)
|
|
429
|
+
// 3. Existing config.autostart → that value
|
|
430
|
+
// 4. Hub present, no flag, no persisted val → false (the hub supervisor
|
|
431
|
+
// owns the lifecycle — #580)
|
|
432
|
+
// 5. Default → true (standalone deploys
|
|
433
|
+
// genuinely need a daemon)
|
|
426
434
|
// When false: skip register AND uninstall any prior registration so the
|
|
427
|
-
//
|
|
435
|
+
// decision's intent ("don't auto-start / don't auto-restart") matches reality
|
|
428
436
|
// even if a previous run had registered a daemon.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
437
|
+
//
|
|
438
|
+
// The hub probe runs only when neither flag was passed AND no value is
|
|
439
|
+
// persisted — i.e. only when the hub signal can actually change the outcome.
|
|
440
|
+
// It targets the hub's fixed loopback port (1939 / $PARACHUTE_HUB_PORT) and
|
|
441
|
+
// never throws (see detectHubPresence). We skip it when a flag/persisted
|
|
442
|
+
// value already decides, to avoid an 800ms wait on a flagged run.
|
|
443
|
+
//
|
|
444
|
+
// False-positive risk: a stale expose-state / leftover PARACHUTE_HUB_ORIGIN
|
|
445
|
+
// makes detectHubPresence return true on a genuinely hubless box, so init
|
|
446
|
+
// silently skips registering a daemon. Narrow + accepted — recover with
|
|
447
|
+
// `parachute-vault init --autostart`. The pre-decided guard below means any
|
|
448
|
+
// explicit flag or persisted value never even reaches the probe.
|
|
449
|
+
const autostartPreDecided =
|
|
450
|
+
flagAutostartOff || flagAutostartOn || typeof globalConfig.autostart === "boolean";
|
|
451
|
+
const hubPresentForAutostart = autostartPreDecided ? false : await detectHubPresence();
|
|
452
|
+
const autostartDecision = decideAutostart({
|
|
453
|
+
flagOn: flagAutostartOn,
|
|
454
|
+
flagOff: flagAutostartOff,
|
|
455
|
+
persisted: globalConfig.autostart,
|
|
456
|
+
hubPresent: hubPresentForAutostart,
|
|
457
|
+
});
|
|
458
|
+
const autostartEnabled = autostartDecision.enabled;
|
|
434
459
|
|
|
435
|
-
if (
|
|
460
|
+
if (autostartDecision.persist) {
|
|
436
461
|
globalConfig.autostart = autostartEnabled;
|
|
437
462
|
writeGlobalConfig(globalConfig);
|
|
438
463
|
}
|
|
439
464
|
|
|
440
465
|
let serverPath: string | null = null;
|
|
441
466
|
if (!autostartEnabled) {
|
|
442
|
-
|
|
467
|
+
if (autostartDecision.reason === "hub-default-off") {
|
|
468
|
+
console.log(
|
|
469
|
+
"Hub supervisor detected — not registering a separate daemon. The hub manages vault's lifecycle.",
|
|
470
|
+
);
|
|
471
|
+
console.log(" To force a standalone daemon anyway: parachute-vault init --autostart");
|
|
472
|
+
} else {
|
|
473
|
+
console.log("Autostart disabled — skipping daemon registration.");
|
|
474
|
+
}
|
|
443
475
|
if (isMac) {
|
|
444
476
|
await uninstallAgent();
|
|
445
477
|
} else if (isLinux && isSystemdAvailable()) {
|
|
446
478
|
await uninstallSystemdService();
|
|
447
479
|
}
|
|
448
480
|
console.log(" To run vault: parachute-vault serve (or use your own supervisor)");
|
|
449
|
-
|
|
481
|
+
if (autostartDecision.reason !== "hub-default-off") {
|
|
482
|
+
console.log(" To re-enable: parachute-vault init --autostart");
|
|
483
|
+
}
|
|
450
484
|
} else {
|
|
485
|
+
if (autostartDecision.overrodeHub) {
|
|
486
|
+
console.log(
|
|
487
|
+
"Warning: a supervised hub was detected, but --autostart was passed — registering a "
|
|
488
|
+
+ "standalone daemon anyway. This can race the hub supervisor for the vault port; "
|
|
489
|
+
+ "prefer letting the hub manage vault unless you know you need both.",
|
|
490
|
+
);
|
|
491
|
+
}
|
|
451
492
|
console.log("Installing daemon...");
|
|
452
493
|
if (isMac) {
|
|
453
494
|
({ serverPath } = await installAgent());
|
|
@@ -3657,13 +3698,20 @@ Setup:
|
|
|
3657
3698
|
--vault-name skips the prompt and names the vault
|
|
3658
3699
|
(lowercase alphanumeric, hyphens, underscores;
|
|
3659
3700
|
omit to be prompted interactively, default "default").
|
|
3660
|
-
--autostart
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3701
|
+
--autostart registers vault with launchd / systemd so
|
|
3702
|
+
it starts on boot AND auto-restarts on crash; it forces
|
|
3703
|
+
registration even when a hub supervisor is detected
|
|
3704
|
+
(logged with a warning). --no-autostart skips daemon
|
|
3705
|
+
registration AND uninstalls any prior registration — for
|
|
3706
|
+
CI, dev sandboxes, Docker, or environments where another
|
|
3707
|
+
supervisor manages the process. Default: register when
|
|
3708
|
+
standalone, but skip when a hub is detected (the hub
|
|
3709
|
+
supervisor owns vault's lifecycle). An explicit flag
|
|
3710
|
+
persists in config.yaml as 'autostart: true|false'.
|
|
3711
|
+
Upgrade note: a box with a persisted 'autostart: true'
|
|
3712
|
+
(from an earlier explicit --autostart) keeps registering
|
|
3713
|
+
even under a hub — run init --no-autostart once to clear
|
|
3714
|
+
it and let the hub manage vault.
|
|
3667
3715
|
parachute-vault doctor Diagnose install/config issues
|
|
3668
3716
|
parachute-vault uninstall [--wipe] [--yes]
|
|
3669
3717
|
Remove daemon + MCP entry; --wipe also removes vaults, .env,
|
package/src/mcp-install.ts
CHANGED
|
@@ -271,8 +271,15 @@ export async function detectHubPresence(opts: {
|
|
|
271
271
|
// signal — no need to probe. We pass `hubPort` purely as the loopback
|
|
272
272
|
// fallback arg; its only role here is the source discriminator.
|
|
273
273
|
const configured = chooseHubOrigin(hubPort, env);
|
|
274
|
-
// A stale expose-state
|
|
275
|
-
//
|
|
274
|
+
// A stale expose-state (or a leftover PARACHUTE_HUB_ORIGIN) can
|
|
275
|
+
// false-positive here. Originally this only selected guidance copy, but as
|
|
276
|
+
// of hub#580 it ALSO gates `vault init`'s daemon registration default
|
|
277
|
+
// (hub present → skip autostart). The false-positive failure mode is
|
|
278
|
+
// therefore: a genuinely hubless box with stale hub-origin state runs init
|
|
279
|
+
// without a flag and silently skips registering a daemon. Narrow + accepted
|
|
280
|
+
// — the operator can re-run with `--autostart`, and any explicit flag or a
|
|
281
|
+
// persisted `config.autostart` short-circuits the probe entirely. See the
|
|
282
|
+
// call site in cli.ts for the persisted-value guard.
|
|
276
283
|
if (configured.source !== "loopback") return true;
|
|
277
284
|
|
|
278
285
|
// 2. Live health probe against the hub's fixed loopback port.
|