@openparachute/hub 0.7.7-rc.9 → 0.7.7
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/README.md +7 -7
- package/package.json +4 -12
- package/src/__tests__/account-api.test.ts +29 -2
- package/src/__tests__/account-session.test.ts +9 -1
- package/src/__tests__/account-token.test.ts +26 -2
- package/src/__tests__/admin-connections-credentials.test.ts +41 -0
- package/src/__tests__/admin-lock.test.ts +3 -14
- package/src/__tests__/admin-module-token.test.ts +10 -30
- package/src/__tests__/admin-surfaces.test.ts +21 -0
- package/src/__tests__/api-hub-upgrade.test.ts +11 -0
- package/src/__tests__/api-mint-token.test.ts +25 -0
- package/src/__tests__/api-modules-ops.test.ts +34 -29
- package/src/__tests__/api-modules.test.ts +50 -58
- package/src/__tests__/api-revoke-token.test.ts +23 -0
- package/src/__tests__/api-settings-hub-origin.test.ts +12 -0
- package/src/__tests__/api-settings-root-redirect.test.ts +159 -4
- package/src/__tests__/api-tokens.test.ts +44 -0
- package/src/__tests__/audience-gate.test.ts +24 -0
- package/src/__tests__/bearer-scheme-casing.test.ts +110 -0
- package/src/__tests__/chrome-strip.test.ts +18 -1
- package/src/__tests__/doctor.test.ts +10 -17
- package/src/__tests__/hub-command.test.ts +70 -2
- package/src/__tests__/hub-server.test.ts +322 -1
- package/src/__tests__/hub-settings.test.ts +110 -6
- package/src/__tests__/hub.test.ts +29 -0
- package/src/__tests__/install.test.ts +210 -38
- package/src/__tests__/migrate.test.ts +3 -1
- package/src/__tests__/oauth-handlers.test.ts +19 -8
- package/src/__tests__/operator-token.test.ts +1 -2
- package/src/__tests__/port-assign.test.ts +17 -26
- package/src/__tests__/root-serve.test.ts +139 -0
- package/src/__tests__/scope-explanations.test.ts +0 -2
- package/src/__tests__/serve-boot.test.ts +25 -36
- package/src/__tests__/service-spec-discovery.test.ts +30 -35
- package/src/__tests__/services-manifest.test.ts +22 -92
- package/src/__tests__/setup-wizard.test.ts +301 -22
- package/src/__tests__/setup.test.ts +8 -13
- package/src/__tests__/status.test.ts +0 -5
- package/src/__tests__/surface-notes-alias.test.ts +296 -0
- package/src/__tests__/wizard-transcription.test.ts +35 -0
- package/src/__tests__/wizard.test.ts +79 -0
- package/src/account-api.ts +6 -0
- package/src/admin-connections.ts +4 -2
- package/src/admin-lock.ts +1 -2
- package/src/admin-module-token.ts +13 -8
- package/src/admin-surfaces.ts +2 -1
- package/src/api-hub-upgrade.ts +2 -1
- package/src/api-mint-token.ts +2 -1
- package/src/api-modules-ops.ts +2 -1
- package/src/api-modules.ts +9 -7
- package/src/api-revoke-token.ts +2 -1
- package/src/api-settings-hub-origin.ts +2 -1
- package/src/api-settings-root-redirect.ts +97 -20
- package/src/api-tokens.ts +2 -1
- package/src/audience-gate.ts +2 -1
- package/src/chrome-strip.ts +16 -4
- package/src/commands/hub.ts +104 -0
- package/src/commands/install.ts +72 -27
- package/src/commands/migrate.ts +5 -1
- package/src/commands/setup.ts +0 -2
- package/src/commands/wizard-transcription.ts +24 -0
- package/src/commands/wizard.ts +34 -1
- package/src/help.ts +0 -1
- package/src/hub-server.ts +111 -54
- package/src/hub-settings.ts +147 -0
- package/src/hub.ts +64 -31
- package/src/module-ops-client.ts +2 -1
- package/src/oauth-handlers.ts +1 -11
- package/src/operator-token.ts +0 -1
- package/src/origin-check.ts +2 -2
- package/src/root-serve.ts +156 -0
- package/src/scope-explanations.ts +2 -5
- package/src/service-spec.ts +18 -47
- package/src/services-manifest.ts +9 -42
- package/src/setup-wizard.ts +144 -31
- package/src/surface-notes-alias.ts +126 -0
- package/src/__tests__/admin-agent-token.test.ts +0 -173
- package/src/admin-agent-token.ts +0 -147
package/src/oauth-handlers.ts
CHANGED
|
@@ -418,13 +418,9 @@ function oauthErrorRedirect(
|
|
|
418
418
|
* Keyed on the SHORT name (resolved from each services.json row via
|
|
419
419
|
* `shortNameForManifest`) rather than the raw manifest name so a row written
|
|
420
420
|
* under a legacy manifest name still counts as installed — e.g. an un-upgraded
|
|
421
|
-
* `parachute-channel` row aliases to short `agent` (channel→agent rename
|
|
422
|
-
* 2026-06-17), so `agent:send` is still correctly advertised until the daemon
|
|
423
|
-
* re-registers under `parachute-agent`.
|
|
424
421
|
*/
|
|
425
422
|
const OPTIONAL_MODULE_SCOPES: ReadonlyArray<readonly [prefix: string, short: string]> = [
|
|
426
423
|
["scribe:", "scribe"],
|
|
427
|
-
["agent:", "agent"],
|
|
428
424
|
["surface:", "surface"],
|
|
429
425
|
];
|
|
430
426
|
|
|
@@ -1903,13 +1899,7 @@ export async function handleApproveClientPost(
|
|
|
1903
1899
|
// Headers logged are non-sensitive (Origin/Referer/Host are public);
|
|
1904
1900
|
// the bound set is hub's own configuration. Body content not logged.
|
|
1905
1901
|
console.warn(
|
|
1906
|
-
`[oauth] approve POST same-origin check failed. headers:
|
|
1907
|
-
`origin=${JSON.stringify(req.headers.get("origin"))} ` +
|
|
1908
|
-
`referer=${JSON.stringify(req.headers.get("referer"))} ` +
|
|
1909
|
-
`host=${JSON.stringify(req.headers.get("host"))} ` +
|
|
1910
|
-
`xff-host=${JSON.stringify(req.headers.get("x-forwarded-host"))} ` +
|
|
1911
|
-
`xff-proto=${JSON.stringify(req.headers.get("x-forwarded-proto"))}. ` +
|
|
1912
|
-
`bound origins: ${JSON.stringify(bound)}`,
|
|
1902
|
+
`[oauth] approve POST same-origin check failed. headers: origin=${JSON.stringify(req.headers.get("origin"))} referer=${JSON.stringify(req.headers.get("referer"))} host=${JSON.stringify(req.headers.get("host"))} xff-host=${JSON.stringify(req.headers.get("x-forwarded-host"))} xff-proto=${JSON.stringify(req.headers.get("x-forwarded-proto"))}. bound origins: ${JSON.stringify(bound)}`,
|
|
1913
1903
|
);
|
|
1914
1904
|
return htmlError(
|
|
1915
1905
|
"Cross-origin request rejected",
|
package/src/operator-token.ts
CHANGED
package/src/origin-check.ts
CHANGED
|
@@ -198,8 +198,8 @@ const MUTATION_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
|
198
198
|
*
|
|
199
199
|
* NOT belted, and why:
|
|
200
200
|
* - GET/HEAD/OPTIONS — read-shaped; the mint GETs
|
|
201
|
-
* (`/admin/host-admin-token`, `/admin/
|
|
202
|
-
* `/admin/
|
|
201
|
+
* (`/admin/host-admin-token`, `/admin/module-token/<short>`,
|
|
202
|
+
* `/admin/vault-admin-token/<name>`)
|
|
203
203
|
* enforce GET-only with a 405 and their response bodies are unreadable
|
|
204
204
|
* cross-origin (no CORS on these routes).
|
|
205
205
|
* - Bearer-authed requests — a cross-site page cannot attach an
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Serving the Parachute app AT the hub's origin root (`root_mode = serve-app`).
|
|
5
|
+
*
|
|
6
|
+
* When an operator flips the root mode to `serve-app` (or a fresh hub installs
|
|
7
|
+
* the app during setup), the hub answers its own origin root with the installed
|
|
8
|
+
* `@openparachute/parachute-app` bundle instead of 302-ing to `/admin`. This is
|
|
9
|
+
* the self-hosted mirror of the hosted door: hit your box's URL, land in the
|
|
10
|
+
* app — no redirect hop.
|
|
11
|
+
*
|
|
12
|
+
* Why this is asset-correct with no rebuild: the app's Vite build is ROOT-based
|
|
13
|
+
* (absolute `/assets/*`, PWA scope `/`, OAuth redirect URIs at the origin root).
|
|
14
|
+
* The `/app` service mount serves the SAME dist through the `notes-serve.ts`
|
|
15
|
+
* shim with a `/app` prefix-strip; here we serve that identical dist verbatim at
|
|
16
|
+
* `/`, which is exactly the origin the build assumes. So `serveAppAtRoot` and
|
|
17
|
+
* the `/app` mount hand back byte-identical bundle files — the only difference
|
|
18
|
+
* is the URL prefix.
|
|
19
|
+
*
|
|
20
|
+
* Precedence is the caller's job (hub-server.ts): the fresh-hub setup wizard
|
|
21
|
+
* funnel and the pre-admin 503 lockout run BEFORE the root `/` handler, and
|
|
22
|
+
* every hub-owned route (/admin, /oauth, /.well-known, /vault, /git, /api,
|
|
23
|
+
* service mounts) dispatches BEFORE the SPA-fallback tail. This module only
|
|
24
|
+
* decides "given an already-unclaimed GET, does the app dist answer it?".
|
|
25
|
+
*
|
|
26
|
+
* NO chrome injection ever rides a root-served response — the app owns its whole
|
|
27
|
+
* page (same posture as the public/surface chrome opt-out). We build the
|
|
28
|
+
* responses here directly rather than routing through `decorateWithChrome`.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { existsSync, statSync } from "node:fs";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { resolveNotesDistFrom } from "./notes-serve.ts";
|
|
34
|
+
|
|
35
|
+
/** The npm package whose built `dist/` is the app front door. */
|
|
36
|
+
export const APP_PACKAGE = "@openparachute/parachute-app";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Namespaces that keep the hub's branded 404 even in serve-app mode. These are
|
|
40
|
+
* hub / protocol surfaces, never app client-side routes, so an SPA shell there
|
|
41
|
+
* would be wrong — and would mask a genuine 404 for an API/OAuth/well-known
|
|
42
|
+
* typo. (Real dist files never live under these prefixes anyway; this makes the
|
|
43
|
+
* "don't shell it" decision explicit rather than incidental.)
|
|
44
|
+
*/
|
|
45
|
+
export const ROOT_SERVE_RESERVED_PREFIXES: readonly string[] = [
|
|
46
|
+
"/api/",
|
|
47
|
+
"/oauth/",
|
|
48
|
+
"/.well-known/",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build a resolver for the installed app's `dist/` directory.
|
|
53
|
+
*
|
|
54
|
+
* Resolution goes through the SAME `resolveNotesDistFrom` machinery the `/app`
|
|
55
|
+
* mount's static-serve shim uses (so root-serve and `/app` resolve to one dist).
|
|
56
|
+
* A SUCCESSFUL resolution is memoized for the process lifetime — a resolved dist
|
|
57
|
+
* path doesn't move while the hub runs, and re-walking `Bun.resolveSync` on every
|
|
58
|
+
* asset request would be needless work. A FAILURE is NOT cached: an operator who
|
|
59
|
+
* set `serve-app` before installing the app, then runs `parachute install app`,
|
|
60
|
+
* is picked up on the next request without a hub restart (dynamic recovery).
|
|
61
|
+
*
|
|
62
|
+
* `resolve` is the test seam (defaults to the real package resolution). Return
|
|
63
|
+
* value is the dist dir, or `null` when the app can't be resolved (not installed
|
|
64
|
+
* / ships no `dist/`).
|
|
65
|
+
*/
|
|
66
|
+
export function makeAppDistResolver(
|
|
67
|
+
resolve: () => string = () => resolveNotesDistFrom({ pkg: APP_PACKAGE }),
|
|
68
|
+
): () => string | null {
|
|
69
|
+
let cached: string | null = null;
|
|
70
|
+
return () => {
|
|
71
|
+
if (cached !== null) return cached;
|
|
72
|
+
try {
|
|
73
|
+
cached = resolve();
|
|
74
|
+
return cached;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** MIME overrides Bun.file doesn't infer (mirrors notes-serve.ts). */
|
|
82
|
+
function mimeFor(path: string): string | undefined {
|
|
83
|
+
// Without this the PWA install prompt sees text/html for the manifest + bails.
|
|
84
|
+
if (path.endsWith(".webmanifest")) return "application/manifest+json";
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Answer an already-unclaimed request from the app's root-based `dist/`, or
|
|
90
|
+
* return `null` to tell the caller to fall through to its normal handling
|
|
91
|
+
* (the `/` 302 fallback, or the branded 404 tail).
|
|
92
|
+
*
|
|
93
|
+
* Contract:
|
|
94
|
+
* - non-GET → null (fall through)
|
|
95
|
+
* - a reserved hub/protocol prefix → null (keep the branded 404)
|
|
96
|
+
* - GET + an existing file under dist → that file (asset-correct MIME)
|
|
97
|
+
* - GET `/` or a trailing-slash "dir" request → dist/index.html (SPA shell)
|
|
98
|
+
* - GET + no file + Accept: text/html → dist/index.html (SPA deep link)
|
|
99
|
+
* - GET + no file + non-HTML (API probe, etc.) → null (branded 404)
|
|
100
|
+
*
|
|
101
|
+
* `dist` is a resolved directory (from `makeAppDistResolver`); if its
|
|
102
|
+
* `index.html` has since vanished (app uninstalled after a successful resolve)
|
|
103
|
+
* we return `null` rather than serving a broken shell.
|
|
104
|
+
*/
|
|
105
|
+
export function serveAppAtRoot(dist: string, req: Request, pathname: string): Response | null {
|
|
106
|
+
// Only GET is served (HEAD/POST/etc. keep the caller's default — a non-GET to
|
|
107
|
+
// an unclaimed path stays a 404, and a non-GET `/` keeps its 302).
|
|
108
|
+
if (req.method !== "GET") return null;
|
|
109
|
+
|
|
110
|
+
// Hub/protocol namespaces are never the app's — leave their 404 branded.
|
|
111
|
+
for (const prefix of ROOT_SERVE_RESERVED_PREFIXES) {
|
|
112
|
+
if (pathname.startsWith(prefix)) return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const indexHtml = join(dist, "index.html");
|
|
116
|
+
// The resolved bundle lost its shell (uninstalled mid-flight) → fall through.
|
|
117
|
+
if (!existsSync(indexHtml)) return null;
|
|
118
|
+
|
|
119
|
+
const spaShell = () =>
|
|
120
|
+
new Response(Bun.file(indexHtml), {
|
|
121
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Bare root or a trailing-slash "directory" request → the SPA shell.
|
|
125
|
+
if (pathname === "/" || pathname.endsWith("/")) return spaShell();
|
|
126
|
+
|
|
127
|
+
// An existing dist file: /assets/*, /icon.svg, /manifest.webmanifest, /sw.js …
|
|
128
|
+
// Malformed percent-encoding (e.g. `/foo%`, `/assets/%ZZ`) makes
|
|
129
|
+
// `decodeURIComponent` throw a URIError. Catch it and fall through to the
|
|
130
|
+
// branded 404 (redirect-mode parity) — WITHOUT the guard the throw escapes to
|
|
131
|
+
// the dispatch outer catch, which classifies a non-DB error as "other" and
|
|
132
|
+
// re-throws → a bare 500. This path is reachable pre-auth on a public expose,
|
|
133
|
+
// so a garbage-encoded asset URL must 404, never 500.
|
|
134
|
+
let decodedPath: string;
|
|
135
|
+
try {
|
|
136
|
+
decodedPath = decodeURIComponent(pathname);
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
// Path-traversal guard: the joined path must stay strictly under dist/.
|
|
141
|
+
const filePath = join(dist, decodedPath);
|
|
142
|
+
if (filePath.startsWith(`${dist}/`) && existsSync(filePath) && statSync(filePath).isFile()) {
|
|
143
|
+
const mime = mimeFor(filePath);
|
|
144
|
+
return new Response(
|
|
145
|
+
Bun.file(filePath),
|
|
146
|
+
mime ? { headers: { "content-type": mime } } : undefined,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// No matching file. Serve the SPA shell ONLY for HTML navigations (a deep link
|
|
151
|
+
// like /some-note the app routes client-side). A browser navigation sends
|
|
152
|
+
// `Accept: text/html`; a non-HTML unclaimed request (API probe, a missing
|
|
153
|
+
// asset fetched with `Accept: */*`) falls through to the branded 404.
|
|
154
|
+
const wantsHtml = (req.headers.get("accept") ?? "").includes("text/html");
|
|
155
|
+
return wantsHtml ? spaShell() : null;
|
|
156
|
+
}
|
|
@@ -46,7 +46,7 @@ export const SCOPE_EXPLANATIONS: Record<string, ScopeExplanation> = {
|
|
|
46
46
|
"Read and write everything, plus admin: config & settings, triggers & automation, GitHub backup, and minting access tokens.",
|
|
47
47
|
level: "admin",
|
|
48
48
|
},
|
|
49
|
-
// Optional-module scopes (scribe /
|
|
49
|
+
// Optional-module scopes (scribe / surface). These are in
|
|
50
50
|
// FIRST_PARTY_SCOPES (= Object.keys(this map)) but the modules may not be
|
|
51
51
|
// installed — so they're GATED in `OPTIONAL_MODULE_SCOPES` (oauth-handlers.ts)
|
|
52
52
|
// and only advertised in `scopes_supported` when the service is in
|
|
@@ -61,10 +61,7 @@ export const SCOPE_EXPLANATIONS: Record<string, ScopeExplanation> = {
|
|
|
61
61
|
label: "Manage Scribe configuration (provider keys, models, quotas).",
|
|
62
62
|
level: "admin",
|
|
63
63
|
},
|
|
64
|
-
|
|
65
|
-
label: "Post messages to your Agent.",
|
|
66
|
-
level: "send",
|
|
67
|
-
},
|
|
64
|
+
|
|
68
65
|
// Surface Git Transport scopes (surface-host). `surface:read` = clone/fetch a
|
|
69
66
|
// surface's hub-hosted git repo; `surface:write` = push to it. Named forms
|
|
70
67
|
// (`surface:<name>:<verb>`) collapse to these via the 3→2-segment rule in
|
package/src/service-spec.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { ServiceEntry } from "./services-manifest.ts";
|
|
|
8
8
|
*
|
|
9
9
|
* 1939 parachute-hub internal static + proxy, CLI-managed
|
|
10
10
|
* 1940 parachute-vault committed core
|
|
11
|
-
* 1941
|
|
11
|
+
* 1941 unassigned
|
|
12
12
|
* 1942 parachute-notes committed core (PWA bundle)
|
|
13
13
|
* 1943 parachute-scribe committed core
|
|
14
14
|
* 1944 parachute-app the super-surface front door (PWA bundle,
|
|
@@ -70,7 +70,7 @@ export interface PortReservation {
|
|
|
70
70
|
export const PORT_RESERVATIONS: readonly PortReservation[] = [
|
|
71
71
|
{ port: 1939, name: "parachute-hub", status: "assigned" },
|
|
72
72
|
{ port: 1940, name: "parachute-vault", status: "assigned" },
|
|
73
|
-
{ port: 1941, name: "
|
|
73
|
+
{ port: 1941, name: "unassigned", status: "reserved" },
|
|
74
74
|
{ port: 1942, name: "parachute-notes", status: "assigned" },
|
|
75
75
|
{ port: 1943, name: "parachute-scribe", status: "assigned" },
|
|
76
76
|
// hub-parity P5 (2026-07-11): parachute-app's canonical slot — the NEW
|
|
@@ -382,8 +382,9 @@ const APP_FALLBACK: FirstPartyFallback = {
|
|
|
382
382
|
postInstallFooter: () => [
|
|
383
383
|
"",
|
|
384
384
|
"Open your Parachute at <origin>/app — it signs in with your hub account.",
|
|
385
|
-
"The hub's front page (`/`) now
|
|
386
|
-
"
|
|
385
|
+
"The hub's front page (`/`) now serves the app directly too, unless you've",
|
|
386
|
+
"already customized the root (`parachute hub set-root-mode` / `set-root-redirect`",
|
|
387
|
+
"or the admin SPA).",
|
|
387
388
|
],
|
|
388
389
|
},
|
|
389
390
|
};
|
|
@@ -510,8 +511,8 @@ export const KNOWN_MODULES: Record<string, KnownModule> = {
|
|
|
510
511
|
},
|
|
511
512
|
},
|
|
512
513
|
// NOTE (2026-07-01): `runner` was REMOVED from this registry (decision:
|
|
513
|
-
// Aaron 2026-07-01 — the module set of record is vault / hub /
|
|
514
|
-
//
|
|
514
|
+
// Aaron 2026-07-01 — the module set of record is vault / hub / scribe /
|
|
515
|
+
// surface). Runner is no longer offered, installable, or
|
|
515
516
|
// lifecycle-addressable by short name from the hub's bootstrap registries.
|
|
516
517
|
// Existing installs stay GRACEFUL: a legacy `parachute-runner` services.json
|
|
517
518
|
// row is handled exactly like any unknown/third-party row — `parachute
|
|
@@ -520,29 +521,7 @@ export const KNOWN_MODULES: Record<string, KnownModule> = {
|
|
|
520
521
|
// stamped and logs-and-skips otherwise. Deliberately NOT added to
|
|
521
522
|
// RETIRED_MODULES: that registry GC-drops rows on load, which would break
|
|
522
523
|
// routing for operators still running the runner daemon.
|
|
523
|
-
|
|
524
|
-
short: "agent",
|
|
525
|
-
package: "@openparachute/agent",
|
|
526
|
-
manifestName: "parachute-agent",
|
|
527
|
-
canonicalPort: 1941,
|
|
528
|
-
displayName: "Agent",
|
|
529
|
-
// Mirrors agent's own module.json (the canonical fields below do too —
|
|
530
|
-
// keep in sync if agent's declaration changes). Renamed from `channel`
|
|
531
|
-
// 2026-06-17 (parachute-channel → parachute-agent); the module is the
|
|
532
|
-
// same webhook-fan-out + MCP bridge daemon, on the same port 1941.
|
|
533
|
-
tagline: "Chat with your Claude Code sessions — a channel per session.",
|
|
534
|
-
canonicalPaths: ["/agent"],
|
|
535
|
-
canonicalHealth: "/health",
|
|
536
|
-
canonicalStripPrefix: true,
|
|
537
|
-
extras: {
|
|
538
|
-
// Backward-compat startCmd for rows without installDir — same rationale
|
|
539
|
-
// as scribe / vault. The bare binary IS the daemon (agent's
|
|
540
|
-
// package.json bin maps `parachute-agent` → src/daemon.ts).
|
|
541
|
-
startCmd: () => ["parachute-agent"],
|
|
542
|
-
// Agent gates its endpoints behind hub-issued JWTs (agent:* scopes).
|
|
543
|
-
hasAuth: true,
|
|
544
|
-
},
|
|
545
|
-
},
|
|
524
|
+
|
|
546
525
|
surface: {
|
|
547
526
|
short: "surface",
|
|
548
527
|
package: "@openparachute/surface",
|
|
@@ -603,20 +582,14 @@ export const KNOWN_MODULES: Record<string, KnownModule> = {
|
|
|
603
582
|
* entries (no successor reusing the name) still stay forever.
|
|
604
583
|
*/
|
|
605
584
|
export const RETIRED_MODULES: Record<string, { retiredAt: string; replacement?: string }> = {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
// is no live `agent` daemon to GC. (The historical `claw → agent`
|
|
615
|
-
// migration in services-manifest.ts still rewrites stale paraclaw rows to
|
|
616
|
-
// `name: "agent"` — they now resolve to the LIVE agent module's mount,
|
|
617
|
-
// which is harmless / arguably correct since paraclaw was the original
|
|
618
|
-
// "agent".)
|
|
619
|
-
//
|
|
585
|
+
agent: { retiredAt: "2026-07-15", replacement: "Vault and Surface" },
|
|
586
|
+
channel: { retiredAt: "2026-07-15", replacement: "Vault and Surface" },
|
|
587
|
+
"parachute-agent": { retiredAt: "2026-07-15", replacement: "Vault and Surface" },
|
|
588
|
+
"parachute-channel": { retiredAt: "2026-07-15", replacement: "Vault and Surface" },
|
|
589
|
+
// Agent was removed from Hub on 2026-07-15. All historical service-row
|
|
590
|
+
// identities are retired so normal manifest reads stop routing or supervising
|
|
591
|
+
// the deprecated daemon. Generic AI-agent authorization grants/tokens are a
|
|
592
|
+
// separate Hub capability and remain supported.
|
|
620
593
|
// NOTE (2026-07-11, hub-parity P5 — `app` / `parachute-app` UN-RETIRED):
|
|
621
594
|
// this table carried `app` + `parachute-app` (retired 2026-05-27, the
|
|
622
595
|
// app → surface rename, patterns#102) from hub#219 through hub-parity P4.
|
|
@@ -716,8 +689,7 @@ export function knownServices(): string[] {
|
|
|
716
689
|
*
|
|
717
690
|
* - `core` — vault / scribe / hub / surface / app (the product surface;
|
|
718
691
|
* `app` joined 2026-07-11, hub-parity P5 — the super-surface front door).
|
|
719
|
-
* - `experimental` —
|
|
720
|
-
* + any unlisted third-party short.
|
|
692
|
+
* - `experimental` — any unlisted third-party short.
|
|
721
693
|
* - `deprecated` — notes (notes-daemon deprecated 2026-05-22; notes-ui moved
|
|
722
694
|
* into parachute-surface). Still RESOLVABLE (discoverableShorts unchanged)
|
|
723
695
|
* and SHOWN-IF-INSTALLED so an existing operator can manage/uninstall, but
|
|
@@ -734,7 +706,7 @@ const FOCUS_DEFAULTS: Record<string, ModuleFocus> = {
|
|
|
734
706
|
hub: "core",
|
|
735
707
|
surface: "core",
|
|
736
708
|
app: "core",
|
|
737
|
-
|
|
709
|
+
|
|
738
710
|
notes: "deprecated",
|
|
739
711
|
};
|
|
740
712
|
|
|
@@ -931,7 +903,6 @@ export async function getSpecFromInstallDir(
|
|
|
931
903
|
*/
|
|
932
904
|
const LEGACY_MANIFEST_ALIASES: Record<string, string> = {
|
|
933
905
|
"parachute-lens": "notes",
|
|
934
|
-
"parachute-channel": "agent",
|
|
935
906
|
};
|
|
936
907
|
|
|
937
908
|
/** Short name for a given manifest name, e.g. `parachute-vault` → `vault`.
|
package/src/services-manifest.ts
CHANGED
|
@@ -692,10 +692,9 @@ export function readManifest(path: string = SERVICES_MANIFEST_PATH): ServicesMan
|
|
|
692
692
|
const afterRetired = dropRetiredModuleRows(healed.raw, path);
|
|
693
693
|
const cleaned = dropLegacyShortNameRows(afterRetired.raw, path);
|
|
694
694
|
const validated = validateManifest(cleaned.raw, path);
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
return migrated.manifest;
|
|
695
|
+
const changed = healed.changed || afterRetired.changed || cleaned.changed;
|
|
696
|
+
if (changed) writeManifest(validated, path);
|
|
697
|
+
return validated;
|
|
699
698
|
}
|
|
700
699
|
|
|
701
700
|
/**
|
|
@@ -732,7 +731,12 @@ function dropRetiredModuleRows(raw: unknown, where: string): { raw: unknown; cha
|
|
|
732
731
|
if (!row || typeof row !== "object") return true;
|
|
733
732
|
const name = (row as Record<string, unknown>).name;
|
|
734
733
|
if (typeof name !== "string") return true;
|
|
735
|
-
|
|
734
|
+
// `claw` is too generic to reserve globally, but the historical Agent row
|
|
735
|
+
// had a structural identity: name=claw mounted at /claw. Retire only that
|
|
736
|
+
// shape; an unrelated third-party `claw` on another mount remains valid.
|
|
737
|
+
const paths = (row as Record<string, unknown>).paths;
|
|
738
|
+
const retiredClawShape = name === "claw" && Array.isArray(paths) && paths[0] === "/claw";
|
|
739
|
+
const retired = RETIRED_MODULES[name] ?? (retiredClawShape ? RETIRED_MODULES.agent : undefined);
|
|
736
740
|
if (retired === undefined) return true;
|
|
737
741
|
dropped.push({ name, retiredAt: retired.retiredAt, replacement: retired.replacement });
|
|
738
742
|
return false;
|
|
@@ -929,43 +933,6 @@ function dropLegacyShortNameRows(raw: unknown, where: string): { raw: unknown; c
|
|
|
929
933
|
};
|
|
930
934
|
}
|
|
931
935
|
|
|
932
|
-
/**
|
|
933
|
-
* Migrate legacy `claw` entries to `agent` in-place. Paraclaw was renamed
|
|
934
|
-
* to parachute-agent across the ecosystem (npm package, mount path, short
|
|
935
|
-
* name); operators who upgraded hub but still have the old paraclaw row
|
|
936
|
-
* in services.json would otherwise see a tile labelled "Claw" and a hub
|
|
937
|
-
* route at `/claw` while their newly-upgraded daemon listens on `/agent`.
|
|
938
|
-
*
|
|
939
|
-
* Idempotent. Only rewrites when both `name === "claw"` AND the first path
|
|
940
|
-
* is `/claw` — narrow enough that a deliberately-named third-party module
|
|
941
|
-
* (e.g. `name: "claw"` on a different mount) is left alone. Health and any
|
|
942
|
-
* `/claw`-rooted paths are rewritten in lockstep.
|
|
943
|
-
*/
|
|
944
|
-
function migrateClawToAgent(manifest: ServicesManifest): {
|
|
945
|
-
manifest: ServicesManifest;
|
|
946
|
-
changed: boolean;
|
|
947
|
-
} {
|
|
948
|
-
let changed = false;
|
|
949
|
-
const services = manifest.services.map((entry) => {
|
|
950
|
-
if (entry.name !== "claw" || entry.paths[0] !== "/claw") return entry;
|
|
951
|
-
changed = true;
|
|
952
|
-
const next: ServiceEntry = {
|
|
953
|
-
...entry,
|
|
954
|
-
name: "agent",
|
|
955
|
-
paths: entry.paths.map((p) => rewriteClawPath(p)),
|
|
956
|
-
health: rewriteClawPath(entry.health),
|
|
957
|
-
};
|
|
958
|
-
return next;
|
|
959
|
-
});
|
|
960
|
-
return { manifest: { services }, changed };
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
function rewriteClawPath(p: string): string {
|
|
964
|
-
if (p === "/claw") return "/agent";
|
|
965
|
-
if (p.startsWith("/claw/")) return `/agent${p.slice("/claw".length)}`;
|
|
966
|
-
return p;
|
|
967
|
-
}
|
|
968
|
-
|
|
969
936
|
export function writeManifest(
|
|
970
937
|
manifest: ServicesManifest,
|
|
971
938
|
path: string = SERVICES_MANIFEST_PATH,
|