@openparachute/hub 0.7.1 → 0.7.2-rc.1
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 +13 -14
- package/package.json +1 -1
- package/src/__tests__/admin-agent-grants.test.ts +1547 -0
- package/src/__tests__/{admin-channel-token.test.ts → admin-agent-token.test.ts} +32 -32
- package/src/__tests__/admin-connections-credentials.test.ts +8 -4
- package/src/__tests__/admin-connections.test.ts +211 -57
- package/src/__tests__/admin-csrf-belt.test.ts +7 -7
- package/src/__tests__/admin-lock.test.ts +600 -0
- package/src/__tests__/admin-module-token.test.ts +36 -8
- package/src/__tests__/admin-vaults.test.ts +8 -8
- package/src/__tests__/api-modules-ops.test.ts +17 -16
- package/src/__tests__/api-modules.test.ts +35 -36
- package/src/__tests__/api-ready.test.ts +2 -2
- package/src/__tests__/clients.test.ts +91 -0
- package/src/__tests__/grants-store.test.ts +219 -0
- package/src/__tests__/hub-server.test.ts +9 -5
- package/src/__tests__/migrate.test.ts +1 -1
- package/src/__tests__/module-manifest.test.ts +11 -11
- package/src/__tests__/oauth-client.test.ts +446 -0
- package/src/__tests__/oauth-flows-store.test.ts +141 -0
- package/src/__tests__/oauth-handlers.test.ts +124 -26
- package/src/__tests__/operator-token.test.ts +2 -2
- package/src/__tests__/scope-explanations.test.ts +3 -3
- package/src/__tests__/serve-boot.test.ts +14 -14
- package/src/__tests__/serve.test.ts +26 -0
- package/src/__tests__/service-spec-discovery.test.ts +26 -18
- package/src/__tests__/services-manifest.test.ts +60 -48
- package/src/__tests__/setup-gate.test.ts +52 -3
- package/src/__tests__/setup-wizard.test.ts +86 -280
- package/src/__tests__/setup.test.ts +1 -1
- package/src/__tests__/upgrade.test.ts +276 -0
- package/src/__tests__/vault-remove.test.ts +393 -0
- package/src/admin-agent-grants.ts +1365 -0
- package/src/admin-agent-token.ts +147 -0
- package/src/admin-connections.ts +67 -50
- package/src/admin-host-admin-token.ts +14 -1
- package/src/admin-lock.ts +281 -0
- package/src/admin-module-token.ts +15 -7
- package/src/admin-vault-admin-token.ts +8 -1
- package/src/admin-vaults.ts +12 -12
- package/src/api-admin-lock.ts +335 -0
- package/src/api-modules-ops.ts +3 -2
- package/src/api-modules.ts +9 -9
- package/src/cli.ts +13 -1
- package/src/clients.ts +88 -0
- package/src/commands/install.ts +7 -0
- package/src/commands/serve-boot.ts +5 -4
- package/src/commands/serve.ts +45 -19
- package/src/commands/setup.ts +4 -3
- package/src/commands/upgrade.ts +118 -2
- package/src/commands/vault-remove.ts +361 -0
- package/src/commands/wizard.ts +4 -4
- package/src/connections-store.ts +3 -3
- package/src/grants-store.ts +272 -0
- package/src/help.ts +4 -1
- package/src/hub-server.ts +209 -27
- package/src/hub-settings.ts +23 -8
- package/src/jwt-sign.ts +5 -1
- package/src/module-manifest.ts +2 -2
- package/src/oauth-client.ts +497 -0
- package/src/oauth-flows-store.ts +163 -0
- package/src/oauth-handlers.ts +40 -13
- package/src/operator-token.ts +1 -1
- package/src/origin-check.ts +7 -2
- package/src/resource-binding.ts +4 -4
- package/src/scope-explanations.ts +3 -3
- package/src/service-spec.ts +56 -43
- package/src/setup-wizard.ts +56 -240
- package/web/ui/dist/assets/index-B5AUE359.js +61 -0
- package/web/ui/dist/assets/{index-E_9wqjEm.css → index-DR6R8EFf.css} +1 -1
- package/web/ui/dist/index.html +2 -2
- package/src/admin-channel-token.ts +0 -135
- package/web/ui/dist/assets/index-Cxtod68O.js +0 -61
package/src/oauth-handlers.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
type OAuthClient,
|
|
39
39
|
type RegisteredClient,
|
|
40
40
|
approveClient,
|
|
41
|
+
expandRedirectUrisForHubOrigins,
|
|
41
42
|
getClient,
|
|
42
43
|
isValidRedirectUri,
|
|
43
44
|
registerClient,
|
|
@@ -71,6 +72,7 @@ import { isHttpsRequest } from "./request-protocol.ts";
|
|
|
71
72
|
import { narrowResourceVaultScopes, resolveResourceVault } from "./resource-binding.ts";
|
|
72
73
|
import { isNonRequestableScope, isRequestableScope, scopeIsAdmin } from "./scope-explanations.ts";
|
|
73
74
|
import { findUnknownScopes, loadDeclaredScopes } from "./scope-registry.ts";
|
|
75
|
+
import { shortNameForManifest } from "./service-spec.ts";
|
|
74
76
|
import {
|
|
75
77
|
type ServicesManifest,
|
|
76
78
|
// Hot-path OAuth flows use the lenient reader so a single malformed
|
|
@@ -391,13 +393,20 @@ function oauthErrorRedirect(
|
|
|
391
393
|
|
|
392
394
|
/**
|
|
393
395
|
* Optional first-party modules whose scopes `FIRST_PARTY_SCOPES` carries
|
|
394
|
-
* statically (it's `Object.keys(SCOPE_EXPLANATIONS)`), paired with the
|
|
395
|
-
*
|
|
396
|
+
* statically (it's `Object.keys(SCOPE_EXPLANATIONS)`), paired with the module
|
|
397
|
+
* SHORT name that means "installed." Vault + hub are core and always
|
|
396
398
|
* advertised; these are the modules a hub may not have.
|
|
399
|
+
*
|
|
400
|
+
* Keyed on the SHORT name (resolved from each services.json row via
|
|
401
|
+
* `shortNameForManifest`) rather than the raw manifest name so a row written
|
|
402
|
+
* under a legacy manifest name still counts as installed — e.g. an un-upgraded
|
|
403
|
+
* `parachute-channel` row aliases to short `agent` (channel→agent rename
|
|
404
|
+
* 2026-06-17), so `agent:send` is still correctly advertised until the daemon
|
|
405
|
+
* re-registers under `parachute-agent`.
|
|
397
406
|
*/
|
|
398
|
-
const OPTIONAL_MODULE_SCOPES: ReadonlyArray<readonly [prefix: string,
|
|
399
|
-
["scribe:", "
|
|
400
|
-
["
|
|
407
|
+
const OPTIONAL_MODULE_SCOPES: ReadonlyArray<readonly [prefix: string, short: string]> = [
|
|
408
|
+
["scribe:", "scribe"],
|
|
409
|
+
["agent:", "agent"],
|
|
401
410
|
];
|
|
402
411
|
|
|
403
412
|
/**
|
|
@@ -406,20 +415,22 @@ const OPTIONAL_MODULE_SCOPES: ReadonlyArray<readonly [prefix: string, service: s
|
|
|
406
415
|
* module isn't installed.
|
|
407
416
|
*
|
|
408
417
|
* Why: `FIRST_PARTY_SCOPES` is static, so a vault-only hub still advertised
|
|
409
|
-
* `scribe:*` + `
|
|
418
|
+
* `scribe:*` + `agent:send`. Discovery clients list the advertised catalog
|
|
410
419
|
* verbatim — claude.ai's connector UI showed a friend connecting ONE vault a
|
|
411
|
-
* request for Scribe +
|
|
420
|
+
* request for Scribe + Agent access the hub can't even honor. So advertise an
|
|
412
421
|
* optional module's scopes only when its service is present in services.json.
|
|
413
422
|
* (Trims the ADVERTISEMENT only; issuance/validation still use the full
|
|
414
423
|
* `loadDeclaredScopes` set, and the per-vault PRM stays vault-narrowed.)
|
|
415
424
|
*/
|
|
416
425
|
function advertisedScopes(declared: ReadonlySet<string>, manifest: ServicesManifest): string[] {
|
|
417
|
-
const
|
|
426
|
+
const installedShorts = new Set(
|
|
427
|
+
manifest.services.map((s) => shortNameForManifest(s.name) ?? s.name),
|
|
428
|
+
);
|
|
418
429
|
return Array.from(declared)
|
|
419
430
|
.filter(isRequestableScope)
|
|
420
431
|
.filter((scope) => {
|
|
421
|
-
for (const [prefix,
|
|
422
|
-
if (scope.startsWith(prefix) && !
|
|
432
|
+
for (const [prefix, short] of OPTIONAL_MODULE_SCOPES) {
|
|
433
|
+
if (scope.startsWith(prefix) && !installedShorts.has(short)) return false;
|
|
423
434
|
}
|
|
424
435
|
return true;
|
|
425
436
|
});
|
|
@@ -888,11 +899,11 @@ export function handleAuthorizeGet(db: Database, req: Request, deps: OAuthDeps):
|
|
|
888
899
|
// 1. The consent screen — and the session-less "App not yet approved"
|
|
889
900
|
// page (`pendingClientResponse`) — shows ONLY that vault's scopes
|
|
890
901
|
// instead of the whole-hub catalog. Narrowing DROPS non-vault scopes
|
|
891
|
-
// (`scribe:*`, `
|
|
902
|
+
// (`scribe:*`, `agent:send`, `hub:admin`) outright: the token this
|
|
892
903
|
// flow mints is stamped `aud=vault.<name>`, so they're unusable inside
|
|
893
904
|
// it and only inflate the consent surface — the exact "scary consent"
|
|
894
905
|
// a friend hit connecting Claude to ONE vault (scribe isn't even
|
|
895
|
-
// installed; `
|
|
906
|
+
// installed; `agent:send` is meaningless to them).
|
|
896
907
|
// 2. The minted token carries the named scope, so `inferAudience` stamps
|
|
897
908
|
// `aud=vault.<name>` and a current-line vault accepts it (an unnamed
|
|
898
909
|
// `vault:read` token is rejected by `findBroadVaultScopes`).
|
|
@@ -2546,10 +2557,26 @@ export async function handleRegister(
|
|
|
2546
2557
|
}
|
|
2547
2558
|
const confidential = body.token_endpoint_auth_method === "client_secret_post";
|
|
2548
2559
|
const scopes = (body.scope ?? "").split(" ").filter((s) => s.length > 0);
|
|
2560
|
+
// Cross-hub-origin expansion (surface#118). A hub-served module (surface,
|
|
2561
|
+
// notes) registers at install time knowing only the loopback hub origin;
|
|
2562
|
+
// once exposed, the browser computes its redirect_uri from the PUBLIC hub
|
|
2563
|
+
// origin, which strict authorize-time matching would reject. We expand each
|
|
2564
|
+
// submitted URI rooted at one of THIS hub's known origins onto every other
|
|
2565
|
+
// known hub origin, so a loopback-registered hub-served client is valid on
|
|
2566
|
+
// the public origin too. FOREIGN-origin redirect_uris (a separate-origin
|
|
2567
|
+
// surface on its own domain) are stored verbatim — never expanded onto hub
|
|
2568
|
+
// origins, never dropped — preserving the strict anti-open-redirect
|
|
2569
|
+
// invariant. Authorize-time matching stays exact-match
|
|
2570
|
+
// (`requireRegisteredRedirectUri`); this only ever ADDS correct hub-origin
|
|
2571
|
+
// URIs the operator already controls.
|
|
2572
|
+
const expandedRedirectUris = expandRedirectUrisForHubOrigins(
|
|
2573
|
+
redirectUris,
|
|
2574
|
+
resolveBoundOrigins(deps),
|
|
2575
|
+
);
|
|
2549
2576
|
let registered: RegisteredClient;
|
|
2550
2577
|
try {
|
|
2551
2578
|
registered = registerClient(db, {
|
|
2552
|
-
redirectUris,
|
|
2579
|
+
redirectUris: expandedRedirectUris,
|
|
2553
2580
|
scopes,
|
|
2554
2581
|
clientName: body.client_name,
|
|
2555
2582
|
confidential,
|
package/src/operator-token.ts
CHANGED
package/src/origin-check.ts
CHANGED
|
@@ -183,17 +183,22 @@ const MUTATION_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
|
183
183
|
* - `POST /admin/connections` + `DELETE /admin/connections/<id>` +
|
|
184
184
|
* `POST /admin/connections/<id>/approve`
|
|
185
185
|
* (connection provision/teardown/claim-approval — the seam's canonical
|
|
186
|
-
* consumers are
|
|
186
|
+
* consumers are the agent module's admin page and the hub SPA, both same-origin
|
|
187
187
|
* `fetch()` with `credentials: "include"`. The Bearer-authed
|
|
188
188
|
* `/<id>/renew` + `/<id>/claim` siblings pass the belt via the
|
|
189
189
|
* Authorization carve-out below.)
|
|
190
|
+
* - `POST /admin/grants/<id>/approve` + `POST /admin/grants/<id>/revoke`
|
|
191
|
+
* (agent-connector grant approval/revoke — operator cookie-authed in the
|
|
192
|
+
* hub SPA, 4b-1. The `PUT /admin/grants` + `GET /admin/grants[/...]`
|
|
193
|
+
* siblings are host-admin-Bearer-authed and pass via the Authorization
|
|
194
|
+
* carve-out below.)
|
|
190
195
|
*
|
|
191
196
|
* (The legacy `POST/DELETE /admin/channels` pair was belted here until
|
|
192
197
|
* boundary D1 retired the endpoint — superseded by `/admin/connections`.)
|
|
193
198
|
*
|
|
194
199
|
* NOT belted, and why:
|
|
195
200
|
* - GET/HEAD/OPTIONS — read-shaped; the mint GETs
|
|
196
|
-
* (`/admin/host-admin-token`, `/admin/
|
|
201
|
+
* (`/admin/host-admin-token`, `/admin/agent-token`,
|
|
197
202
|
* `/admin/module-token/<short>`, `/admin/vault-admin-token/<name>`)
|
|
198
203
|
* enforce GET-only with a 405 and their response bodies are unreadable
|
|
199
204
|
* cross-origin (no CORS on these routes).
|
package/src/resource-binding.ts
CHANGED
|
@@ -117,15 +117,15 @@ function decodeVaultName(segment: string): string | null {
|
|
|
117
117
|
* - already-named `vault:<other>:<verb>` is LEFT UNTOUCHED — a client that
|
|
118
118
|
* explicitly named a different vault is not silently re-pointed; the
|
|
119
119
|
* downstream picker / assignment defenses decide whether that's allowed.
|
|
120
|
-
* - non-vault scopes (`scribe:*`, `
|
|
120
|
+
* - non-vault scopes (`scribe:*`, `agent:send`, `hub:admin`, …) are
|
|
121
121
|
* DROPPED. This flow mints a token stamped `aud=vault.<name>` (RFC 8707),
|
|
122
|
-
* so a scribe/
|
|
122
|
+
* so a scribe/agent/hub scope inside it is unusable — keeping it only
|
|
123
123
|
* inflates the consent surface a friend sees when connecting ONE vault.
|
|
124
124
|
* That "scary consent" is the failure mode this module exists to kill
|
|
125
125
|
* (see the header docstring): the verb-narrowing alone left the foreign
|
|
126
126
|
* scopes riding through, so a client that over-requests the whole-hub
|
|
127
127
|
* catalog (claude.ai reads it from the AS-metadata `scopes_supported`)
|
|
128
|
-
* still surfaced `scribe:admin` + `
|
|
128
|
+
* still surfaced `scribe:admin` + `agent:send` on the consent screen.
|
|
129
129
|
* A client that genuinely wants a scribe token runs a separate flow
|
|
130
130
|
* naming the scribe resource.
|
|
131
131
|
*
|
|
@@ -137,7 +137,7 @@ export function narrowResourceVaultScopes(scopes: readonly string[], vaultName:
|
|
|
137
137
|
const out: string[] = [];
|
|
138
138
|
for (const s of scopes) {
|
|
139
139
|
const parts = s.split(":");
|
|
140
|
-
if (parts[0] !== "vault") continue; // drop scribe:/
|
|
140
|
+
if (parts[0] !== "vault") continue; // drop scribe:/agent:/hub:/… — foreign to a vault-audience token
|
|
141
141
|
const verb = parts[1];
|
|
142
142
|
if (parts.length === 2 && verb && VAULT_VERBS.has(verb)) {
|
|
143
143
|
out.push(`vault:${vaultName}:${verb}`);
|
|
@@ -45,7 +45,7 @@ export const SCOPE_EXPLANATIONS: Record<string, ScopeExplanation> = {
|
|
|
45
45
|
label: "Full vault access plus configuration changes (rotate tokens, change settings).",
|
|
46
46
|
level: "admin",
|
|
47
47
|
},
|
|
48
|
-
// Optional-module scopes (scribe /
|
|
48
|
+
// Optional-module scopes (scribe / agent). These are in FIRST_PARTY_SCOPES
|
|
49
49
|
// (= Object.keys(this map)) but the modules may not be installed — so they're
|
|
50
50
|
// GATED in `OPTIONAL_MODULE_SCOPES` (oauth-handlers.ts) and only advertised in
|
|
51
51
|
// `scopes_supported` when the service is in services.json. If you add scopes
|
|
@@ -59,8 +59,8 @@ export const SCOPE_EXPLANATIONS: Record<string, ScopeExplanation> = {
|
|
|
59
59
|
label: "Manage Scribe configuration (provider keys, models, quotas).",
|
|
60
60
|
level: "admin",
|
|
61
61
|
},
|
|
62
|
-
"
|
|
63
|
-
label: "Post messages to your
|
|
62
|
+
"agent:send": {
|
|
63
|
+
label: "Post messages to your Agent.",
|
|
64
64
|
level: "send",
|
|
65
65
|
},
|
|
66
66
|
"hub:admin": {
|
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 parachute-
|
|
11
|
+
* 1941 parachute-agent exploration (renamed from parachute-channel)
|
|
12
12
|
* 1942 parachute-notes committed core (PWA bundle)
|
|
13
13
|
* 1943 parachute-scribe committed core
|
|
14
14
|
* 1944–1949 unassigned
|
|
@@ -64,7 +64,7 @@ export interface PortReservation {
|
|
|
64
64
|
export const PORT_RESERVATIONS: readonly PortReservation[] = [
|
|
65
65
|
{ port: 1939, name: "parachute-hub", status: "assigned" },
|
|
66
66
|
{ port: 1940, name: "parachute-vault", status: "assigned" },
|
|
67
|
-
{ port: 1941, name: "parachute-
|
|
67
|
+
{ port: 1941, name: "parachute-agent", status: "assigned" },
|
|
68
68
|
{ port: 1942, name: "parachute-notes", status: "assigned" },
|
|
69
69
|
{ port: 1943, name: "parachute-scribe", status: "assigned" },
|
|
70
70
|
{ port: 1944, name: "unassigned", status: "reserved" },
|
|
@@ -110,7 +110,7 @@ export interface FirstPartyExtras {
|
|
|
110
110
|
/**
|
|
111
111
|
* Does the service gate its endpoints behind auth today? Drives
|
|
112
112
|
* `effectivePublicExposure`'s default for api/tool services. True for
|
|
113
|
-
* vault/
|
|
113
|
+
* vault/agent; conservatively false for scribe until its auth-gate ships.
|
|
114
114
|
*/
|
|
115
115
|
readonly hasAuth?: boolean;
|
|
116
116
|
/**
|
|
@@ -260,9 +260,10 @@ export function composeServiceSpec(opts: {
|
|
|
260
260
|
//
|
|
261
261
|
// As of 2026-05-21 (hub#310), vault / scribe / runner have all retired their
|
|
262
262
|
// FALLBACK entries: each ships `module.json` AND self-registers its
|
|
263
|
-
// services.json row at boot (vault#356, scribe#50, runner#3).
|
|
264
|
-
// followed in the 2026-06-09 hub-module-
|
|
265
|
-
// `.parachute/module.json` and
|
|
263
|
+
// services.json row at boot (vault#356, scribe#50, runner#3). Agent
|
|
264
|
+
// (renamed from channel 2026-06-17) followed in the 2026-06-09 hub-module-
|
|
265
|
+
// boundary migration (D3) — it ships `.parachute/module.json` and
|
|
266
|
+
// self-registers (channel#34 era, pre-rename), so its
|
|
266
267
|
// vendored manifest retired to a KNOWN_MODULES row. Hub reads the
|
|
267
268
|
// canonical fields from services.json (operator-authoritative) and falls
|
|
268
269
|
// through to `<installDir>/.parachute/module.json` when a lifecycle command
|
|
@@ -321,7 +322,7 @@ const NOTES_FALLBACK: FirstPartyFallback = {
|
|
|
321
322
|
* Indexed by short name (the `parachute install <X>` token).
|
|
322
323
|
*
|
|
323
324
|
* Only notes remains — see the block comment above for the rationale
|
|
324
|
-
* (vault/scribe/runner/
|
|
325
|
+
* (vault/scribe/runner/agent now self-register and ship their own
|
|
325
326
|
* module.json). Other code paths consult both this table AND `KNOWN_MODULES`
|
|
326
327
|
* (which carries the post-self-register-retirement entries) via the helpers
|
|
327
328
|
* in this file (`shortNameForManifest`, `knownServices`, …).
|
|
@@ -332,7 +333,7 @@ export const FIRST_PARTY_FALLBACKS: Record<string, FirstPartyFallback> = {
|
|
|
332
333
|
|
|
333
334
|
/**
|
|
334
335
|
* Minimal install-time registry for first-party modules whose FALLBACK has
|
|
335
|
-
* retired (vault / scribe / runner as of hub#310;
|
|
336
|
+
* retired (vault / scribe / runner as of hub#310; agent as of the
|
|
336
337
|
* 2026-06-09 hub-module-boundary migration, D3). Hub uses this for:
|
|
337
338
|
*
|
|
338
339
|
* 1. **Install bootstrap**: mapping `parachute install <short>` to the npm
|
|
@@ -457,28 +458,26 @@ export const KNOWN_MODULES: Record<string, KnownModule> = {
|
|
|
457
458
|
hasAuth: true,
|
|
458
459
|
},
|
|
459
460
|
},
|
|
460
|
-
|
|
461
|
-
short: "
|
|
462
|
-
package: "@openparachute/
|
|
463
|
-
manifestName: "parachute-
|
|
461
|
+
agent: {
|
|
462
|
+
short: "agent",
|
|
463
|
+
package: "@openparachute/agent",
|
|
464
|
+
manifestName: "parachute-agent",
|
|
464
465
|
canonicalPort: 1941,
|
|
465
|
-
displayName: "
|
|
466
|
-
// Mirrors
|
|
467
|
-
// keep in sync if
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
// module.json + self-registration and had drifted on all three.
|
|
466
|
+
displayName: "Agent",
|
|
467
|
+
// Mirrors agent's own module.json (the canonical fields below do too —
|
|
468
|
+
// keep in sync if agent's declaration changes). Renamed from `channel`
|
|
469
|
+
// 2026-06-17 (parachute-channel → parachute-agent); the module is the
|
|
470
|
+
// same webhook-fan-out + MCP bridge daemon, on the same port 1941.
|
|
471
471
|
tagline: "Chat with your Claude Code sessions — a channel per session.",
|
|
472
|
-
canonicalPaths: ["/
|
|
472
|
+
canonicalPaths: ["/agent"],
|
|
473
473
|
canonicalHealth: "/health",
|
|
474
474
|
canonicalStripPrefix: true,
|
|
475
475
|
extras: {
|
|
476
476
|
// Backward-compat startCmd for rows without installDir — same rationale
|
|
477
|
-
// as scribe / vault / runner. The bare binary IS the daemon (
|
|
478
|
-
// package.json bin maps `parachute-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
// Channel gates its endpoints behind hub-issued JWTs (channel:* scopes).
|
|
477
|
+
// as scribe / vault / runner. The bare binary IS the daemon (agent's
|
|
478
|
+
// package.json bin maps `parachute-agent` → src/daemon.ts).
|
|
479
|
+
startCmd: () => ["parachute-agent"],
|
|
480
|
+
// Agent gates its endpoints behind hub-issued JWTs (agent:* scopes).
|
|
482
481
|
hasAuth: true,
|
|
483
482
|
},
|
|
484
483
|
},
|
|
@@ -535,15 +534,20 @@ export const KNOWN_MODULES: Record<string, KnownModule> = {
|
|
|
535
534
|
* reappear silently on legacy installs.
|
|
536
535
|
*/
|
|
537
536
|
export const RETIRED_MODULES: Record<string, { retiredAt: string; replacement?: string }> = {
|
|
538
|
-
agent:
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
//
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
537
|
+
// NOTE (2026-06-17, channel→agent rename): the `agent` / `parachute-agent`
|
|
538
|
+
// names were retired here on 2026-05-20 (the Claude-in-containers module).
|
|
539
|
+
// They are NOW RE-ASSIGNED to the renamed channel module (parachute-channel
|
|
540
|
+
// → parachute-agent). Keeping these as RETIRED entries would make
|
|
541
|
+
// `dropRetiredModuleRows` drop the NEW agent module's services.json row on
|
|
542
|
+
// load — breaking the live module. The names are no longer retired; they
|
|
543
|
+
// belong to the active agent module. The retired containers code now lives
|
|
544
|
+
// under `parachute-agent-legacy` (GitHub) and is not installable, so there
|
|
545
|
+
// is no live `agent` daemon to GC. (The historical `claw → agent`
|
|
546
|
+
// migration in services-manifest.ts still rewrites stale paraclaw rows to
|
|
547
|
+
// `name: "agent"` — they now resolve to the LIVE agent module's mount,
|
|
548
|
+
// which is harmless / arguably correct since paraclaw was the original
|
|
549
|
+
// "agent".)
|
|
550
|
+
//
|
|
547
551
|
// The `parachute-app` row name retires 2026-05-27 along with the
|
|
548
552
|
// app → surface rename (patterns#102). Operators upgrading from
|
|
549
553
|
// 0.5.13-stable will have a `parachute-app` row in services.json
|
|
@@ -637,7 +641,7 @@ export function knownServices(): string[] {
|
|
|
637
641
|
* The hub PREFERS a module's manifest-declared `focus`; this map is the
|
|
638
642
|
* fallback when `module.json` omits it (and the bootstrap value before a
|
|
639
643
|
* module is installed). vault / scribe / hub / surface are `core`; everything
|
|
640
|
-
* else (
|
|
644
|
+
* else (agent / runner / notes / unknown third-party) defaults to
|
|
641
645
|
* `experimental`. **Show all; never hide** — `focus` only groups + labels.
|
|
642
646
|
*/
|
|
643
647
|
const FOCUS_DEFAULTS: Record<string, ModuleFocus> = {
|
|
@@ -645,7 +649,7 @@ const FOCUS_DEFAULTS: Record<string, ModuleFocus> = {
|
|
|
645
649
|
scribe: "core",
|
|
646
650
|
hub: "core",
|
|
647
651
|
surface: "core",
|
|
648
|
-
|
|
652
|
+
agent: "experimental",
|
|
649
653
|
runner: "experimental",
|
|
650
654
|
notes: "experimental",
|
|
651
655
|
};
|
|
@@ -668,7 +672,7 @@ export function focusForShort(short: string, declared?: ModuleFocus): ModuleFocu
|
|
|
668
672
|
* `CURATED_MODULES` whitelist (2026-06-09 modular-UI architecture, P2): every
|
|
669
673
|
* module the hub can resolve a package/manifest for is discoverable + installable,
|
|
670
674
|
* regardless of `focus` tier. Deduped, with FIRST_PARTY_FALLBACKS shorts first
|
|
671
|
-
* (notes) then KNOWN_MODULES (vault / scribe / runner /
|
|
675
|
+
* (notes) then KNOWN_MODULES (vault / scribe / runner / agent / surface).
|
|
672
676
|
*
|
|
673
677
|
* `notes` is intentionally included — it's still resolvable (vendored fallback)
|
|
674
678
|
* for legacy installs; it surfaces as `experimental` and isn't pushed as a
|
|
@@ -727,7 +731,7 @@ export function canonicalPortForManifest(manifestName: string): number | undefin
|
|
|
727
731
|
* spec with embedded manifest + extras — the vendored manifest is the
|
|
728
732
|
* source of truth pre-install and the install path preserves it through.
|
|
729
733
|
*
|
|
730
|
-
* KNOWN_MODULES shorts (vault / scribe / runner /
|
|
734
|
+
* KNOWN_MODULES shorts (vault / scribe / runner / agent / surface — post
|
|
731
735
|
* FALLBACK retirement) return a **minimal** spec carrying `package`, `manifestName`,
|
|
732
736
|
* and the imperative `extras` fields
|
|
733
737
|
* (`init`, `hasAuth`, `urlForEntry`, `postInstallFooter`). They do NOT carry
|
|
@@ -821,14 +825,23 @@ export async function getSpecFromInstallDir(
|
|
|
821
825
|
* their install — without this alias, `parachute start/stop/logs/status`
|
|
822
826
|
* silently skip those rows. Remove after launch, alongside the `lens →
|
|
823
827
|
* notes` install alias.
|
|
828
|
+
*
|
|
829
|
+
* `parachute-channel` → `agent` (2026-06-17): the channel module was renamed
|
|
830
|
+
* to agent (parachute-channel → parachute-agent). Operators with an
|
|
831
|
+
* un-upgraded `services.json` carry a `name: "parachute-channel"` row; this
|
|
832
|
+
* alias keeps `shortNameForManifest("parachute-channel") === "agent"` so the
|
|
833
|
+
* legacy row still resolves to the live agent module for routing + lifecycle
|
|
834
|
+
* until the daemon re-registers under `parachute-agent`. Remove after one
|
|
835
|
+
* release cycle, alongside the `channel → agent` install alias.
|
|
824
836
|
*/
|
|
825
837
|
const LEGACY_MANIFEST_ALIASES: Record<string, string> = {
|
|
826
838
|
"parachute-lens": "notes",
|
|
839
|
+
"parachute-channel": "agent",
|
|
827
840
|
};
|
|
828
841
|
|
|
829
842
|
/** Short name for a given manifest name, e.g. `parachute-vault` → `vault`.
|
|
830
843
|
* Consults both FIRST_PARTY_FALLBACKS (notes) and KNOWN_MODULES
|
|
831
|
-
* (vault / scribe / runner /
|
|
844
|
+
* (vault / scribe / runner / agent / surface — post-FALLBACK-retirement).
|
|
832
845
|
* Returns undefined for unknown manifests. */
|
|
833
846
|
export function shortNameForManifest(manifestName: string): string | undefined {
|
|
834
847
|
for (const [short, fb] of Object.entries(FIRST_PARTY_FALLBACKS)) {
|
|
@@ -841,13 +854,13 @@ export function shortNameForManifest(manifestName: string): string | undefined {
|
|
|
841
854
|
}
|
|
842
855
|
|
|
843
856
|
/**
|
|
844
|
-
* Find a services.json row by its SHORT name (e.g. `"
|
|
845
|
-
* row's manifest name (`parachute-
|
|
857
|
+
* Find a services.json row by its SHORT name (e.g. `"agent"`), resolving each
|
|
858
|
+
* row's manifest name (`parachute-agent`) back through `shortNameForManifest`.
|
|
846
859
|
*
|
|
847
860
|
* services.json rows carry the MANIFEST name, not the bare short — so a direct
|
|
848
861
|
* `s.name === short` comparison silently misses every first-party module. That
|
|
849
|
-
* exact mismatch (`s.name === "
|
|
850
|
-
* surfaced a spurious "
|
|
862
|
+
* exact mismatch (`s.name === "agent"` vs a `parachute-agent` row) is what
|
|
863
|
+
* surfaced a spurious "agent module is not installed" when wiring the agent
|
|
851
864
|
* connection sink. Resolve through the short↔manifest map instead. Returns the
|
|
852
865
|
* first match, or undefined when no installed row maps to `short`.
|
|
853
866
|
*
|
|
@@ -855,7 +868,7 @@ export function shortNameForManifest(manifestName: string): string | undefined {
|
|
|
855
868
|
* here — `shortNameForManifest` only knows the canonical `parachute-vault`, so
|
|
856
869
|
* `findServiceByShort(services, "vault")` returns undefined even when a vault is
|
|
857
870
|
* installed. Vault rows are resolved by mount path via `findVaultUpstream`; this
|
|
858
|
-
* helper is for single-instance modules (
|
|
871
|
+
* helper is for single-instance modules (agent / scribe / runner / surface).
|
|
859
872
|
*/
|
|
860
873
|
export function findServiceByShort<T extends { name: string }>(
|
|
861
874
|
services: readonly T[],
|