@openparachute/hub 0.6.3-rc.3 → 0.6.3
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/__tests__/account-home-ui.test.ts +97 -0
- package/src/__tests__/managed-unit.test.ts +23 -3
- package/src/__tests__/migrate-cutover.test.ts +60 -1
- package/src/__tests__/stale-module-units.test.ts +286 -0
- package/src/account-home-ui.ts +126 -12
- package/src/cloudflare/connector-service.ts +13 -2
- package/src/commands/migrate-cutover.ts +48 -0
- package/src/managed-unit.ts +24 -4
- package/src/stale-module-units.ts +374 -0
- package/web/ui/dist/assets/{index-D_6AFvZy.js → index-D_0TRjeo.js} +1 -1
- package/web/ui/dist/index.html +1 -1
package/src/account-home-ui.ts
CHANGED
|
@@ -174,6 +174,15 @@ export function renderAccountHome(opts: RenderAccountHomeOpts): string {
|
|
|
174
174
|
)}</div>`
|
|
175
175
|
: "";
|
|
176
176
|
|
|
177
|
+
// Suppress the "Get started with your AI" card on the no-vault branch:
|
|
178
|
+
// that branch tells the user "You don't have a vault yet" + "ask the operator
|
|
179
|
+
// to assign you one," so a do-the-thing card alongside reads as contradictory
|
|
180
|
+
// (do-this vs you-lack-the-prerequisite). The admin (isFirstAdmin) and
|
|
181
|
+
// assigned-vault branches both have a vault to act against, so the card
|
|
182
|
+
// belongs there.
|
|
183
|
+
const hasNoVault = !isFirstAdmin && assignedVaults.length === 0;
|
|
184
|
+
const startedCard = hasNoVault ? "" : renderGetStartedCard();
|
|
185
|
+
|
|
177
186
|
const vaultCard = renderVaultCard({
|
|
178
187
|
assignedVaults,
|
|
179
188
|
trimmedOrigin,
|
|
@@ -197,12 +206,51 @@ export function renderAccountHome(opts: RenderAccountHomeOpts): string {
|
|
|
197
206
|
</div>
|
|
198
207
|
${mintedBanner}
|
|
199
208
|
${mintErrorBanner}
|
|
209
|
+
${startedCard}
|
|
200
210
|
${vaultCard}
|
|
201
211
|
${accountCard}
|
|
202
212
|
</div>${COPY_SCRIPT}`;
|
|
203
213
|
return baseDocument(`${username} — Parachute`, body);
|
|
204
214
|
}
|
|
205
215
|
|
|
216
|
+
/**
|
|
217
|
+
* The "Get started with your AI" card — the real first stop for a friend
|
|
218
|
+
* landing on `/account/`. Mirrors the operator setup-wizard's
|
|
219
|
+
* `renderStarterPromptsSection` (same two parachute.computer/onboarding/*
|
|
220
|
+
* links + copy) so friends and operators get the same on-ramp. The prompts
|
|
221
|
+
* live on parachute.computer rather than embedded here so they iterate
|
|
222
|
+
* without a hub release; this card just links.
|
|
223
|
+
*
|
|
224
|
+
* Placed near the top of the page (after any banners, before the vault card)
|
|
225
|
+
* because "what do I actually do with this?" is the friend's first question —
|
|
226
|
+
* the connect details below answer "how", this answers "what next".
|
|
227
|
+
*/
|
|
228
|
+
function renderGetStartedCard(): string {
|
|
229
|
+
return `
|
|
230
|
+
<section class="section get-started" data-testid="get-started-card">
|
|
231
|
+
<h2>Get started with your AI</h2>
|
|
232
|
+
<p>Two ready-made prompts to paste into Claude (or another AI assistant)
|
|
233
|
+
once your vault is connected — they walk you through it, no setup
|
|
234
|
+
knowledge needed.</p>
|
|
235
|
+
<div class="starter-grid">
|
|
236
|
+
<a class="starter-tile" href="https://parachute.computer/onboarding/vault-setup/"
|
|
237
|
+
target="_blank" rel="noopener" data-testid="starter-vault-setup">
|
|
238
|
+
<h3>Set up your vault</h3>
|
|
239
|
+
<p>Your AI interviews you about where your notes live now and suggests
|
|
240
|
+
a structure that fits how you think.</p>
|
|
241
|
+
<span class="starter-cta">Open prompt ↗</span>
|
|
242
|
+
</a>
|
|
243
|
+
<a class="starter-tile" href="https://parachute.computer/onboarding/surface-build/"
|
|
244
|
+
target="_blank" rel="noopener" data-testid="starter-surface-build">
|
|
245
|
+
<h3>Build a custom UI</h3>
|
|
246
|
+
<p>Your AI builds you a little web app for your vault — your own way to
|
|
247
|
+
see and add to it.</p>
|
|
248
|
+
<span class="starter-cta">Open prompt ↗</span>
|
|
249
|
+
</a>
|
|
250
|
+
</div>
|
|
251
|
+
</section>`;
|
|
252
|
+
}
|
|
253
|
+
|
|
206
254
|
interface VaultCardOpts {
|
|
207
255
|
assignedVaults: string[];
|
|
208
256
|
trimmedOrigin: string;
|
|
@@ -303,9 +351,9 @@ function renderVaultCard(opts: VaultCardOpts): string {
|
|
|
303
351
|
approve. (Your hub must be reachable from the web for this.)</p>
|
|
304
352
|
</div>
|
|
305
353
|
|
|
306
|
-
<p class="mcp-connect-hint" data-testid="connect-any-client-hint">
|
|
307
|
-
|
|
308
|
-
|
|
354
|
+
<p class="mcp-connect-hint" data-testid="connect-any-client-hint">Using something
|
|
355
|
+
else? Point any MCP client at the same endpoint above. (ChatGPT and some other
|
|
356
|
+
web UIs call these "connectors.")</p>
|
|
309
357
|
</div>
|
|
310
358
|
<p class="vault-notes-cta">
|
|
311
359
|
<a class="btn btn-primary" href="https://notes.parachute.computer/add?url=${vaultUrlForAdd}"
|
|
@@ -478,19 +526,24 @@ function renderAccountCard(opts: AccountCardOpts): string {
|
|
|
478
526
|
<dl class="kv">
|
|
479
527
|
<dt>Username</dt>
|
|
480
528
|
<dd><code>${username}</code></dd>
|
|
481
|
-
<dt>Two-factor authentication</dt>
|
|
482
|
-
${twoFactorStatus}
|
|
483
529
|
</dl>
|
|
484
|
-
<p>
|
|
485
|
-
<a class="account-action" href="/account/change-password" data-testid="change-password-link">Change password →</a>
|
|
486
|
-
</p>
|
|
487
|
-
<p>
|
|
488
|
-
${twoFactorLink}
|
|
489
|
-
</p>
|
|
490
530
|
<form method="POST" action="/logout" class="signout-form" data-testid="signout-form">
|
|
491
531
|
${renderCsrfHiddenInput(csrfToken)}
|
|
492
532
|
<button type="submit" class="btn btn-secondary">Sign out</button>
|
|
493
533
|
</form>
|
|
534
|
+
<details class="account-security" data-testid="account-security">
|
|
535
|
+
<summary>Security & password</summary>
|
|
536
|
+
<dl class="kv">
|
|
537
|
+
<dt>Two-factor authentication</dt>
|
|
538
|
+
${twoFactorStatus}
|
|
539
|
+
</dl>
|
|
540
|
+
<p>
|
|
541
|
+
<a class="account-action" href="/account/change-password" data-testid="change-password-link">Change password →</a>
|
|
542
|
+
</p>
|
|
543
|
+
<p>
|
|
544
|
+
${twoFactorLink}
|
|
545
|
+
</p>
|
|
546
|
+
</details>
|
|
494
547
|
</section>`;
|
|
495
548
|
}
|
|
496
549
|
|
|
@@ -604,6 +657,60 @@ const STYLES = `
|
|
|
604
657
|
margin-top: 1.25rem;
|
|
605
658
|
}
|
|
606
659
|
.section p { margin: 0.4rem 0; }
|
|
660
|
+
|
|
661
|
+
.get-started h3 {
|
|
662
|
+
font-family: ${FONT_SERIF};
|
|
663
|
+
font-weight: 400;
|
|
664
|
+
font-size: 1rem;
|
|
665
|
+
margin: 0 0 0.3rem;
|
|
666
|
+
color: ${PALETTE.fg};
|
|
667
|
+
}
|
|
668
|
+
.starter-grid {
|
|
669
|
+
display: grid;
|
|
670
|
+
grid-template-columns: 1fr 1fr;
|
|
671
|
+
gap: 0.75rem;
|
|
672
|
+
margin: 0.75rem 0 0.2rem;
|
|
673
|
+
}
|
|
674
|
+
.starter-tile {
|
|
675
|
+
display: block;
|
|
676
|
+
border: 1px solid ${PALETTE.borderLight};
|
|
677
|
+
border-radius: 8px;
|
|
678
|
+
padding: 0.8rem 0.9rem;
|
|
679
|
+
background: ${PALETTE.bgSoft};
|
|
680
|
+
text-decoration: none;
|
|
681
|
+
color: inherit;
|
|
682
|
+
transition: border-color 0.15s ease, background 0.15s ease;
|
|
683
|
+
}
|
|
684
|
+
.starter-tile:hover { border-color: ${PALETTE.accent}; background: ${PALETTE.accentSoft}; }
|
|
685
|
+
.starter-tile p {
|
|
686
|
+
font-size: 0.84rem;
|
|
687
|
+
color: ${PALETTE.fgMuted};
|
|
688
|
+
margin: 0 0 0.5rem;
|
|
689
|
+
}
|
|
690
|
+
.starter-cta {
|
|
691
|
+
font-size: 0.85rem;
|
|
692
|
+
font-weight: 500;
|
|
693
|
+
color: ${PALETTE.accent};
|
|
694
|
+
}
|
|
695
|
+
@media (max-width: 480px) {
|
|
696
|
+
.starter-grid { grid-template-columns: 1fr; }
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.account-security {
|
|
700
|
+
margin: 0.9rem 0 0;
|
|
701
|
+
padding-top: 0.6rem;
|
|
702
|
+
border-top: 1px solid ${PALETTE.borderLight};
|
|
703
|
+
}
|
|
704
|
+
.account-security > summary {
|
|
705
|
+
cursor: pointer;
|
|
706
|
+
font-size: 0.88rem;
|
|
707
|
+
font-weight: 600;
|
|
708
|
+
color: ${PALETTE.fgMuted};
|
|
709
|
+
list-style: revert;
|
|
710
|
+
}
|
|
711
|
+
.account-security > summary:hover { color: ${PALETTE.fg}; }
|
|
712
|
+
.account-security .kv { margin-top: 0.6rem; }
|
|
713
|
+
|
|
607
714
|
.vault-name {
|
|
608
715
|
font-family: ${FONT_MONO};
|
|
609
716
|
font-size: 1rem;
|
|
@@ -879,7 +986,14 @@ const STYLES = `
|
|
|
879
986
|
code { background: #1f1c18; color: #e8e4dc; }
|
|
880
987
|
.copy-row code { background: transparent; }
|
|
881
988
|
.section { border-top-color: #3a362f; }
|
|
882
|
-
.mcp-method, .vault-notes-cta, .token-mint
|
|
989
|
+
.mcp-method, .vault-notes-cta, .token-mint,
|
|
990
|
+
.account-security { border-top-color: #3a362f; }
|
|
991
|
+
.get-started h3 { color: #f0ece4; }
|
|
992
|
+
.starter-tile { border-color: #3a362f; background: #1f1c18; }
|
|
993
|
+
.starter-tile:hover { border-color: ${PALETTE.accent}; }
|
|
994
|
+
.starter-tile p { color: #a8a29a; }
|
|
995
|
+
.account-security > summary { color: #a8a29a; }
|
|
996
|
+
.account-security > summary:hover { color: #f0ece4; }
|
|
883
997
|
.brand-tag { border-color: #3a362f; color: #a8a29a; }
|
|
884
998
|
.copy-row { background: #1f1c18; border-color: #3a362f; }
|
|
885
999
|
.btn-secondary, .btn-copy { color: #e8e4dc; border-color: #3a362f; }
|
|
@@ -69,10 +69,21 @@ export type ConnectorServiceDeps = ManagedUnitDeps;
|
|
|
69
69
|
|
|
70
70
|
export const defaultServiceDeps: ConnectorServiceDeps = defaultManagedUnitDeps;
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Reverse-DNS prefix for the launchd label + plist filename. Exported so the
|
|
74
|
+
* migrate/teardown stale-per-module-autostart sweep (`src/stale-module-units.ts`,
|
|
75
|
+
* hub#522) can reuse it as a SKIP-list anchor — the connector unit is owned by
|
|
76
|
+
* the supervised model (`expose off --cloudflare` tears it down), and the sweep
|
|
77
|
+
* must never touch it. Reusing the constant keeps the skip-list from drifting if
|
|
78
|
+
* this prefix ever changes.
|
|
79
|
+
*/
|
|
80
|
+
export const CLOUDFLARED_LAUNCHD_LABEL_PREFIX = "computer.parachute.cloudflared";
|
|
81
|
+
/** systemd unit name prefix. Exported for the same skip-list reason as above. */
|
|
82
|
+
export const CLOUDFLARED_SYSTEMD_UNIT_PREFIX = "parachute-cloudflared-";
|
|
72
83
|
/** Reverse-DNS prefix for the launchd label + plist filename. */
|
|
73
|
-
const LAUNCHD_LABEL_PREFIX =
|
|
84
|
+
const LAUNCHD_LABEL_PREFIX = CLOUDFLARED_LAUNCHD_LABEL_PREFIX;
|
|
74
85
|
/** systemd unit name prefix. */
|
|
75
|
-
const SYSTEMD_UNIT_PREFIX =
|
|
86
|
+
const SYSTEMD_UNIT_PREFIX = CLOUDFLARED_SYSTEMD_UNIT_PREFIX;
|
|
76
87
|
/** Provenance comment baked into every rendered connector unit file. */
|
|
77
88
|
const CONNECTOR_HEADER = "Generated by parachute expose public --cloudflare — do not edit by hand.";
|
|
78
89
|
|
|
@@ -86,6 +86,11 @@ import { type PortListeningFn, defaultPortListening } from "../port-probe.ts";
|
|
|
86
86
|
import { type AliveFn, clearPid, readPid } from "../process-state.ts";
|
|
87
87
|
import { shortNameForManifest } from "../service-spec.ts";
|
|
88
88
|
import { type ServiceEntry, readManifestLenient } from "../services-manifest.ts";
|
|
89
|
+
import {
|
|
90
|
+
type DisableStaleModuleUnitsOpts,
|
|
91
|
+
type DisableStaleModuleUnitsResult,
|
|
92
|
+
disableStaleModuleUnits,
|
|
93
|
+
} from "../stale-module-units.ts";
|
|
89
94
|
|
|
90
95
|
/**
|
|
91
96
|
* Absolute path to this hub checkout's `src/cli.ts` — the entry the hub unit's
|
|
@@ -180,6 +185,19 @@ export interface CutoverDeps {
|
|
|
180
185
|
sleep: (ms: number) => Promise<void>;
|
|
181
186
|
/** The hub-unit deps for install / detect / manager calls. */
|
|
182
187
|
hubUnitDeps: HubUnitDeps;
|
|
188
|
+
/**
|
|
189
|
+
* Detect + DISABLE any stale per-module autostart unit (#522 — the load-bearing
|
|
190
|
+
* fix). A leftover standalone `parachute-<short>.service` (systemd KeepAlive) /
|
|
191
|
+
* `computer.parachute.<short>` (launchd KeepAlive) from the pre-supervisor era
|
|
192
|
+
* keeps RESPAWNING an unsupervised module that binds the module's port — the
|
|
193
|
+
* supervised child then EADDRINUSE-crash-loops. Killing the process is
|
|
194
|
+
* whack-a-mole (the unit resurrects it); we must disable the UNIT. Run in the
|
|
195
|
+
* STOP phase (after the per-module detached stop, before the port-free verify)
|
|
196
|
+
* so the freed port lets the supervised module bind. Ownership-safe (known
|
|
197
|
+
* module shorts only; hub + cloudflared skipped), idempotent, non-fatal.
|
|
198
|
+
* Injectable so tests never touch real systemctl/launchctl.
|
|
199
|
+
*/
|
|
200
|
+
disableStaleModuleUnits: (opts?: DisableStaleModuleUnitsOpts) => DisableStaleModuleUnitsResult;
|
|
183
201
|
}
|
|
184
202
|
|
|
185
203
|
export interface WriteUnitOpts {
|
|
@@ -329,6 +347,7 @@ export const defaultCutoverDeps: CutoverDeps = {
|
|
|
329
347
|
probeHealth: defaultHubUnitDeps.probeHealth,
|
|
330
348
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
331
349
|
hubUnitDeps: defaultHubUnitDeps,
|
|
350
|
+
disableStaleModuleUnits,
|
|
332
351
|
};
|
|
333
352
|
|
|
334
353
|
export interface CutoverOpts {
|
|
@@ -673,6 +692,22 @@ export async function cutoverToSupervised(opts: CutoverOpts = {}): Promise<Cutov
|
|
|
673
692
|
await stopDetachedModule(target, configDir, deps, timeoutMs, pollMs, log);
|
|
674
693
|
}
|
|
675
694
|
|
|
695
|
+
// --- Step 3b (#522): DISABLE stale per-module autostart UNITS. ---
|
|
696
|
+
// The load-bearing fix for the recurring "port 1940 taken" crash-loop: a
|
|
697
|
+
// leftover standalone `parachute-<short>.service` (systemd KeepAlive) or
|
|
698
|
+
// `computer.parachute.<short>` (launchd KeepAlive) from the pre-supervisor era
|
|
699
|
+
// keeps RESPAWNING an unsupervised module that binds the port — so the
|
|
700
|
+
// per-module stop above (and the orphan sweep below) is whack-a-mole: the unit
|
|
701
|
+
// resurrects the process within seconds, serving OLD code. We must DISABLE the
|
|
702
|
+
// UNIT so the port stays free for the supervised child. MUST run HERE — after
|
|
703
|
+
// the detached stop, BEFORE the verify-ports-free + unit start — so the freed
|
|
704
|
+
// port lets the supervised module bind. Ownership-safe (known module shorts
|
|
705
|
+
// only; hub + cloudflared skipped), idempotent, non-fatal (a failed disable
|
|
706
|
+
// warns + continues; a system-level unit it can't disable → warn with the
|
|
707
|
+
// manual sudo command). Every disabled unit is reported.
|
|
708
|
+
log("Checking for stale per-module autostart units to disable…");
|
|
709
|
+
deps.disableStaleModuleUnits({ deps: deps.hubUnitDeps, log: (l) => log(l) });
|
|
710
|
+
|
|
676
711
|
// --- Step 4: §7.2 ORPHAN SWEEP — per services.json port + the hub port. ---
|
|
677
712
|
// The HUB port keeps the pre-existing blind-adopt (mirrors stopHub's 1939
|
|
678
713
|
// orphan-adoption — out of scope for MUST-FIX 2). The MODULE ports get the
|
|
@@ -797,6 +832,13 @@ export interface TeardownOpts {
|
|
|
797
832
|
removedLaunchdMessage: (label: string) => string;
|
|
798
833
|
removedSystemdMessage: (unitName: string) => string;
|
|
799
834
|
}) => ManagedUnitRemoveResult;
|
|
835
|
+
/**
|
|
836
|
+
* Test seam: the stale-per-module-autostart disable (#522). Teardown also
|
|
837
|
+
* disables any leftover standalone module autostart unit so a rollback to
|
|
838
|
+
* foreground `serve` doesn't leave a competing module respawning at boot.
|
|
839
|
+
* Injectable so tests never touch real systemctl/launchctl.
|
|
840
|
+
*/
|
|
841
|
+
disableStaleModuleUnits?: (opts?: DisableStaleModuleUnitsOpts) => DisableStaleModuleUnitsResult;
|
|
800
842
|
}
|
|
801
843
|
|
|
802
844
|
/**
|
|
@@ -815,6 +857,7 @@ export function teardownHubUnit(opts: TeardownOpts = {}): { removed: boolean; me
|
|
|
815
857
|
const log = opts.log ?? ((line) => console.log(line));
|
|
816
858
|
const deps = opts.deps ?? defaultHubUnitDeps;
|
|
817
859
|
const remove = opts.remove ?? removeManagedUnit;
|
|
860
|
+
const disableStale = opts.disableStaleModuleUnits ?? disableStaleModuleUnits;
|
|
818
861
|
const res = remove({
|
|
819
862
|
launchdLabel: HUB_LAUNCHD_LABEL,
|
|
820
863
|
systemdUnitName: HUB_SYSTEMD_UNIT_NAME,
|
|
@@ -824,6 +867,11 @@ export function teardownHubUnit(opts: TeardownOpts = {}): { removed: boolean; me
|
|
|
824
867
|
removedSystemdMessage: (unitName) =>
|
|
825
868
|
`Removed systemd unit ${unitName} — the hub no longer starts on boot.`,
|
|
826
869
|
});
|
|
870
|
+
// #522: also disable any leftover standalone per-module autostart unit so a
|
|
871
|
+
// rollback to foreground `serve` doesn't leave a competing module respawning at
|
|
872
|
+
// boot to race whatever the operator brings up next. Ownership-safe (known
|
|
873
|
+
// module shorts only; hub + cloudflared skipped), idempotent, non-fatal.
|
|
874
|
+
disableStale({ deps, log });
|
|
827
875
|
if (res.removed) {
|
|
828
876
|
for (const m of res.messages) log(m);
|
|
829
877
|
log("");
|
package/src/managed-unit.ts
CHANGED
|
@@ -648,10 +648,25 @@ export interface BuildHubManagedUnitOpts {
|
|
|
648
648
|
*
|
|
649
649
|
* Resolves the absolute `bun` path via the `which` seam (launchd/systemd don't
|
|
650
650
|
* search `$PATH` — mirrors how the connector resolves cloudflared). The env
|
|
651
|
-
* carries `
|
|
652
|
-
* OMITS `PARACHUTE_HUB_ORIGIN`: baking a stale
|
|
653
|
-
* iss-mismatch class; `resolveStartupIssuer`
|
|
654
|
-
* the operator token + vault `.env` to the
|
|
651
|
+
* carries `PARACHUTE_BIND_HOST` / `PARACHUTE_HOME` / `PORT` / `PATH` /
|
|
652
|
+
* `BUN_INSTALL` — and INTENTIONALLY OMITS `PARACHUTE_HUB_ORIGIN`: baking a stale
|
|
653
|
+
* origin here would re-create the iss-mismatch class; `resolveStartupIssuer`
|
|
654
|
+
* derives it and start-hub self-heals the operator token + vault `.env` to the
|
|
655
|
+
* current origin (design §4.1 comment).
|
|
656
|
+
*
|
|
657
|
+
* BIND HOST — `PARACHUTE_BIND_HOST=127.0.0.1` is forced here so every
|
|
658
|
+
* self-hosted supervised hub binds loopback. `parachute serve` itself defaults
|
|
659
|
+
* the bind host to `0.0.0.0` (serve.ts), which is correct for the container
|
|
660
|
+
* shape (the platform's HTTP forwarder must reach the hub) but WRONG for a
|
|
661
|
+
* self-hosted box — bare `serve` would expose the admin/OAuth surfaces on every
|
|
662
|
+
* interface, contradicting the pre-supervisor detached behavior and the trust
|
|
663
|
+
* model `layerOf` (hub-server.ts) assumes (header-absent ⇒ "loopback"). The
|
|
664
|
+
* container path never calls this builder (the Dockerfile pins
|
|
665
|
+
* `ENV PARACHUTE_BIND_HOST=0.0.0.0` + runs `serve` directly), so it stays
|
|
666
|
+
* 0.0.0.0. The canonical expose path is unaffected: cloudflared/tailscale dial
|
|
667
|
+
* `127.0.0.1:<port>` from the same host, and the hub's own proxy targets
|
|
668
|
+
* `http://127.0.0.1:<port>` (hub-server.ts). An operator who genuinely wants
|
|
669
|
+
* all-interfaces can override the generated unit; the default is loopback.
|
|
655
670
|
*
|
|
656
671
|
* NOT called by any command in this PR (additive — Phase 3 wires it into `init`).
|
|
657
672
|
*/
|
|
@@ -676,6 +691,11 @@ export function buildHubManagedUnit(opts: BuildHubManagedUnitOpts): ManagedUnit
|
|
|
676
691
|
systemdDescription: "Parachute hub (serve + supervisor)",
|
|
677
692
|
execStart: [bunPath, opts.cliPath, "serve"],
|
|
678
693
|
env: {
|
|
694
|
+
// Force loopback on every self-hosted supervised hub. serve.ts defaults
|
|
695
|
+
// to 0.0.0.0 (container-first); a self-hosted box must NOT bare-serve
|
|
696
|
+
// all-interfaces. Container path bypasses this builder (Dockerfile pins
|
|
697
|
+
// its own 0.0.0.0). See the docstring for the full trust-model rationale.
|
|
698
|
+
PARACHUTE_BIND_HOST: "127.0.0.1",
|
|
679
699
|
// PARACHUTE_HOME captured at install time (design §4.2) — NOT the default.
|
|
680
700
|
PARACHUTE_HOME: opts.parachuteHome,
|
|
681
701
|
PORT: String(port),
|