@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.
Files changed (73) hide show
  1. package/README.md +13 -14
  2. package/package.json +1 -1
  3. package/src/__tests__/admin-agent-grants.test.ts +1547 -0
  4. package/src/__tests__/{admin-channel-token.test.ts → admin-agent-token.test.ts} +32 -32
  5. package/src/__tests__/admin-connections-credentials.test.ts +8 -4
  6. package/src/__tests__/admin-connections.test.ts +211 -57
  7. package/src/__tests__/admin-csrf-belt.test.ts +7 -7
  8. package/src/__tests__/admin-lock.test.ts +600 -0
  9. package/src/__tests__/admin-module-token.test.ts +36 -8
  10. package/src/__tests__/admin-vaults.test.ts +8 -8
  11. package/src/__tests__/api-modules-ops.test.ts +17 -16
  12. package/src/__tests__/api-modules.test.ts +35 -36
  13. package/src/__tests__/api-ready.test.ts +2 -2
  14. package/src/__tests__/clients.test.ts +91 -0
  15. package/src/__tests__/grants-store.test.ts +219 -0
  16. package/src/__tests__/hub-server.test.ts +9 -5
  17. package/src/__tests__/migrate.test.ts +1 -1
  18. package/src/__tests__/module-manifest.test.ts +11 -11
  19. package/src/__tests__/oauth-client.test.ts +446 -0
  20. package/src/__tests__/oauth-flows-store.test.ts +141 -0
  21. package/src/__tests__/oauth-handlers.test.ts +124 -26
  22. package/src/__tests__/operator-token.test.ts +2 -2
  23. package/src/__tests__/scope-explanations.test.ts +3 -3
  24. package/src/__tests__/serve-boot.test.ts +14 -14
  25. package/src/__tests__/serve.test.ts +26 -0
  26. package/src/__tests__/service-spec-discovery.test.ts +26 -18
  27. package/src/__tests__/services-manifest.test.ts +60 -48
  28. package/src/__tests__/setup-gate.test.ts +52 -3
  29. package/src/__tests__/setup-wizard.test.ts +86 -280
  30. package/src/__tests__/setup.test.ts +1 -1
  31. package/src/__tests__/upgrade.test.ts +276 -0
  32. package/src/__tests__/vault-remove.test.ts +393 -0
  33. package/src/admin-agent-grants.ts +1365 -0
  34. package/src/admin-agent-token.ts +147 -0
  35. package/src/admin-connections.ts +67 -50
  36. package/src/admin-host-admin-token.ts +14 -1
  37. package/src/admin-lock.ts +281 -0
  38. package/src/admin-module-token.ts +15 -7
  39. package/src/admin-vault-admin-token.ts +8 -1
  40. package/src/admin-vaults.ts +12 -12
  41. package/src/api-admin-lock.ts +335 -0
  42. package/src/api-modules-ops.ts +3 -2
  43. package/src/api-modules.ts +9 -9
  44. package/src/cli.ts +13 -1
  45. package/src/clients.ts +88 -0
  46. package/src/commands/install.ts +7 -0
  47. package/src/commands/serve-boot.ts +5 -4
  48. package/src/commands/serve.ts +45 -19
  49. package/src/commands/setup.ts +4 -3
  50. package/src/commands/upgrade.ts +118 -2
  51. package/src/commands/vault-remove.ts +361 -0
  52. package/src/commands/wizard.ts +4 -4
  53. package/src/connections-store.ts +3 -3
  54. package/src/grants-store.ts +272 -0
  55. package/src/help.ts +4 -1
  56. package/src/hub-server.ts +209 -27
  57. package/src/hub-settings.ts +23 -8
  58. package/src/jwt-sign.ts +5 -1
  59. package/src/module-manifest.ts +2 -2
  60. package/src/oauth-client.ts +497 -0
  61. package/src/oauth-flows-store.ts +163 -0
  62. package/src/oauth-handlers.ts +40 -13
  63. package/src/operator-token.ts +1 -1
  64. package/src/origin-check.ts +7 -2
  65. package/src/resource-binding.ts +4 -4
  66. package/src/scope-explanations.ts +3 -3
  67. package/src/service-spec.ts +56 -43
  68. package/src/setup-wizard.ts +56 -240
  69. package/web/ui/dist/assets/index-B5AUE359.js +61 -0
  70. package/web/ui/dist/assets/{index-E_9wqjEm.css → index-DR6R8EFf.css} +1 -1
  71. package/web/ui/dist/index.html +2 -2
  72. package/src/admin-channel-token.ts +0 -135
  73. package/web/ui/dist/assets/index-Cxtod68O.js +0 -61
@@ -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
- * services.json entry that means "installed." Vault + hub are core and always
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, service: string]> = [
399
- ["scribe:", "parachute-scribe"],
400
- ["channel:", "parachute-channel"],
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:*` + `channel:send`. Discovery clients list the advertised catalog
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 + Channel access the hub can't even honor. So advertise an
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 installed = new Set(manifest.services.map((s) => s.name));
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, service] of OPTIONAL_MODULE_SCOPES) {
422
- if (scope.startsWith(prefix) && !installed.has(service)) return false;
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:*`, `channel:send`, `hub:admin`) outright: the token this
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; `channel:send` is meaningless to them).
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,
@@ -104,7 +104,7 @@ export const OPERATOR_TOKEN_SCOPE_SETS: Readonly<Record<OperatorScopeSet, readon
104
104
  "parachute:host:vault",
105
105
  "vault:admin",
106
106
  "scribe:admin",
107
- "channel:send",
107
+ "agent:send",
108
108
  ],
109
109
  };
110
110
 
@@ -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 channel's admin page and the hub SPA, both same-origin
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/channel-token`,
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).
@@ -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:*`, `channel:send`, `hub:admin`, …) are
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/channel/hub scope inside it is unusable — keeping it only
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` + `channel:send` on the consent screen.
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:/channel:/hub:/… — foreign to a vault-audience token
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 / channel). These are in FIRST_PARTY_SCOPES
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
- "channel:send": {
63
- label: "Post messages to your Channel.",
62
+ "agent:send": {
63
+ label: "Post messages to your Agent.",
64
64
  level: "send",
65
65
  },
66
66
  "hub:admin": {
@@ -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-channel exploration (may retire)
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-channel", status: "assigned" },
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/channel; conservatively false for scribe until its auth-gate ships.
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). Channel
264
- // followed in the 2026-06-09 hub-module-boundary migration (D3) — it ships
265
- // `.parachute/module.json` and self-registers (channel#34 era), so its
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/channel now self-register and ship their own
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; channel as of the
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
- channel: {
461
- short: "channel",
462
- package: "@openparachute/channel",
463
- manifestName: "parachute-channel",
461
+ agent: {
462
+ short: "agent",
463
+ package: "@openparachute/agent",
464
+ manifestName: "parachute-agent",
464
465
  canonicalPort: 1941,
465
- displayName: "Channel",
466
- // Mirrors channel's own module.json (the canonical fields below do too —
467
- // keep in sync if channel's declaration changes). The retired FALLBACK's
468
- // copy ("Notification fan-out across modules.", health "/channel/health",
469
- // startCmd ["parachute-channel", "daemon"]) predated channel's
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: ["/channel"],
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 (channel's
478
- // package.json bin maps `parachute-channel` → src/daemon.ts); the old
479
- // vendored `["parachute-channel", "daemon"]` subcommand is stale.
480
- startCmd: () => ["parachute-channel"],
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
- retiredAt: "2026-05-20",
540
- replacement: "parachute-surface or parachute-runner (depending on use case)",
541
- },
542
- // 2026-05-20 retirement caught both forms of legacy rows.
543
- "parachute-agent": {
544
- retiredAt: "2026-05-20",
545
- replacement: "parachute-surface or parachute-runner (depending on use case)",
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 (channel / runner / notes / unknown third-party) defaults to
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
- channel: "experimental",
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 / channel / surface).
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 / channel / surface — post
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 / channel / surface — post-FALLBACK-retirement).
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. `"channel"`), resolving each
845
- * row's manifest name (`parachute-channel`) back through `shortNameForManifest`.
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 === "channel"` vs a `parachute-channel` row) is what
850
- * surfaced a spurious "channel module is not installed" when wiring the channel
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 (channel / scribe / runner / surface).
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[],