@blackbelt-technology/pi-agent-dashboard 0.5.0 → 0.5.2

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 (201) hide show
  1. package/AGENTS.md +26 -5
  2. package/README.md +49 -7
  3. package/docs/architecture.md +129 -1
  4. package/package.json +15 -15
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
  7. package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
  8. package/packages/extension/src/__tests__/command-handler.test.ts +78 -8
  9. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
  10. package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
  11. package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
  12. package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
  13. package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
  14. package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
  15. package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
  16. package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
  17. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
  18. package/packages/extension/src/ask-user-tool.ts +1 -1
  19. package/packages/extension/src/bridge-context.ts +68 -4
  20. package/packages/extension/src/bridge.ts +79 -11
  21. package/packages/extension/src/command-handler.ts +95 -15
  22. package/packages/extension/src/flow-event-wiring.ts +1 -1
  23. package/packages/extension/src/multiselect-list.ts +1 -1
  24. package/packages/extension/src/pi-env.d.ts +16 -9
  25. package/packages/extension/src/prompt-expander.ts +74 -63
  26. package/packages/extension/src/provider-register.ts +16 -9
  27. package/packages/extension/src/retry-tracker.ts +123 -0
  28. package/packages/extension/src/server-launcher.ts +31 -70
  29. package/packages/extension/src/session-sync.ts +10 -1
  30. package/packages/extension/src/slash-dispatch.ts +123 -0
  31. package/packages/extension/src/usage-limit-orderer.ts +76 -0
  32. package/packages/server/bin/pi-dashboard.mjs +84 -0
  33. package/packages/server/package.json +8 -7
  34. package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
  35. package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
  36. package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
  37. package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
  38. package/packages/server/src/__tests__/cli-parse.test.ts +16 -4
  39. package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
  40. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
  41. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
  42. package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
  43. package/packages/server/src/__tests__/directory-service.test.ts +2 -2
  44. package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
  45. package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
  46. package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +68 -1
  47. package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
  48. package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
  49. package/packages/server/src/__tests__/headless-pid-registry.test.ts +316 -0
  50. package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
  51. package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
  52. package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
  53. package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
  54. package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
  55. package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
  56. package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
  57. package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
  58. package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
  59. package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
  60. package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
  61. package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
  62. package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -0
  63. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
  64. package/packages/server/src/__tests__/package-routes.test.ts +1 -1
  65. package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
  66. package/packages/server/src/__tests__/pi-ai-shape.test.ts +147 -0
  67. package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
  68. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
  69. package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
  70. package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +62 -3
  71. package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
  72. package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
  73. package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
  74. package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
  75. package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
  76. package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
  77. package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +13 -23
  78. package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
  79. package/packages/server/src/__tests__/recommended-routes.test.ts +3 -3
  80. package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
  81. package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +84 -0
  82. package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
  83. package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
  84. package/packages/server/src/auth-plugin.ts +3 -0
  85. package/packages/server/src/bootstrap-state.ts +10 -0
  86. package/packages/server/src/browser-gateway.ts +27 -10
  87. package/packages/server/src/browser-handlers/handler-context.ts +9 -0
  88. package/packages/server/src/browser-handlers/session-action-handler.ts +128 -19
  89. package/packages/server/src/changelog-fs.ts +167 -0
  90. package/packages/server/src/changelog-parser.ts +321 -0
  91. package/packages/server/src/changelog-remote.ts +134 -0
  92. package/packages/server/src/cli.ts +62 -82
  93. package/packages/server/src/config-api.ts +14 -2
  94. package/packages/server/src/directory-service.ts +106 -4
  95. package/packages/server/src/event-wiring.ts +90 -6
  96. package/packages/server/src/headless-pid-registry.ts +344 -37
  97. package/packages/server/src/legacy-pi-cleanup.ts +151 -0
  98. package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
  99. package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
  100. package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
  101. package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
  102. package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
  103. package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
  104. package/packages/server/src/model-proxy/api-key-store.ts +87 -0
  105. package/packages/server/src/model-proxy/auth-gate.ts +116 -0
  106. package/packages/server/src/model-proxy/concurrency.ts +76 -0
  107. package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
  108. package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
  109. package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
  110. package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
  111. package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
  112. package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
  113. package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
  114. package/packages/server/src/model-proxy/convert/index.ts +8 -0
  115. package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
  116. package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
  117. package/packages/server/src/model-proxy/convert/types.ts +70 -0
  118. package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
  119. package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
  120. package/packages/server/src/model-proxy/internal-registry.ts +157 -0
  121. package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
  122. package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
  123. package/packages/server/src/model-proxy/request-log.ts +53 -0
  124. package/packages/server/src/model-proxy/streamer.ts +59 -0
  125. package/packages/server/src/openspec-group-store.ts +490 -0
  126. package/packages/server/src/pending-client-correlations.ts +73 -0
  127. package/packages/server/src/pending-fork-registry.ts +24 -12
  128. package/packages/server/src/pi-core-checker.ts +77 -17
  129. package/packages/server/src/pi-core-updater.ts +16 -6
  130. package/packages/server/src/pi-dev-version-check.ts +145 -0
  131. package/packages/server/src/pi-gateway.ts +4 -0
  132. package/packages/server/src/pi-version-skew.ts +12 -4
  133. package/packages/server/src/process-manager.ts +182 -11
  134. package/packages/server/src/provider-auth-storage.ts +29 -47
  135. package/packages/server/src/provider-catalogue-cache.ts +24 -18
  136. package/packages/server/src/restart-helper.ts +17 -16
  137. package/packages/server/src/routes/bootstrap-routes.ts +37 -0
  138. package/packages/server/src/routes/jj-routes.ts +3 -0
  139. package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
  140. package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
  141. package/packages/server/src/routes/model-proxy-routes.ts +330 -0
  142. package/packages/server/src/routes/openspec-group-routes.ts +231 -0
  143. package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
  144. package/packages/server/src/routes/pi-core-routes.ts +1 -1
  145. package/packages/server/src/routes/provider-auth-routes.ts +8 -1
  146. package/packages/server/src/routes/provider-routes.ts +28 -5
  147. package/packages/server/src/routes/system-routes.ts +44 -2
  148. package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
  149. package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
  150. package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
  151. package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
  152. package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
  153. package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
  154. package/packages/server/src/server.ts +254 -60
  155. package/packages/server/src/session-api.ts +63 -4
  156. package/packages/server/src/session-discovery.ts +1 -1
  157. package/packages/server/src/session-file-reader.ts +1 -1
  158. package/packages/server/src/spawn-register-watchdog.ts +62 -7
  159. package/packages/server/src/spawn-token.ts +20 -0
  160. package/packages/server/src/tunnel-watchdog.ts +230 -0
  161. package/packages/server/src/tunnel.ts +5 -1
  162. package/packages/shared/package.json +1 -1
  163. package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
  164. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +24 -17
  165. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
  166. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
  167. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
  168. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
  169. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
  170. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +5 -3
  171. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
  172. package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
  173. package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
  174. package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
  175. package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
  176. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
  177. package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
  178. package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
  179. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
  180. package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
  181. package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
  182. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
  183. package/packages/shared/src/bootstrap-install.ts +1 -1
  184. package/packages/shared/src/browser-protocol.ts +70 -0
  185. package/packages/shared/src/changelog-types.ts +111 -0
  186. package/packages/shared/src/config.ts +172 -2
  187. package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
  188. package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
  189. package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
  190. package/packages/shared/src/platform/binary-lookup.ts +204 -0
  191. package/packages/shared/src/platform/node-spawn.ts +71 -26
  192. package/packages/shared/src/protocol.ts +27 -1
  193. package/packages/shared/src/recommended-extensions.ts +18 -0
  194. package/packages/shared/src/rest-api.ts +219 -1
  195. package/packages/shared/src/server-launcher.ts +277 -0
  196. package/packages/shared/src/skill-block-parser.ts +1 -1
  197. package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
  198. package/packages/shared/src/tool-registry/definitions.ts +15 -3
  199. package/packages/shared/src/types.ts +62 -0
  200. package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -53
  201. package/packages/shared/src/resolve-jiti.ts +0 -102
@@ -0,0 +1,194 @@
1
+ /**
2
+ * REST route for `GET /api/pi-core/changelog`.
3
+ *
4
+ * Returns the parsed `CHANGELOG.md` for a core package filtered to
5
+ * a `(from, to]` half-open version range, plus a derived `hasBreaking`
6
+ * flag and a public GitHub URL for the full changelog.
7
+ *
8
+ * Gated identically to other `/api/pi-core/*` routes via the shared
9
+ * bootstrap-status gate.
10
+ *
11
+ * See change: pi-update-whats-new-panel.
12
+ */
13
+ import type { FastifyInstance } from "fastify";
14
+ import type {
15
+ ChangelogResponse,
16
+ } from "@blackbelt-technology/pi-dashboard-shared/changelog-types.js";
17
+ import { CORE_PACKAGE_NAMES } from "../pi-core-checker.js";
18
+ import { parseVersion, compareVersions } from "../pi-version-skew.js";
19
+ import {
20
+ findChangelogPath,
21
+ readPackageJson,
22
+ deriveChangelogUrl,
23
+ } from "../changelog-fs.js";
24
+ import {
25
+ parseChangelog,
26
+ readAndParseChangelog,
27
+ getCachedRemoteChangelog,
28
+ setRemoteChangelog,
29
+ refreshRemoteChangelogTtl,
30
+ } from "../changelog-parser.js";
31
+ import {
32
+ deriveChangelogRawUrl,
33
+ fetchRemoteChangelog,
34
+ } from "../changelog-remote.js";
35
+ import type { ChangelogRelease } from "@blackbelt-technology/pi-dashboard-shared/changelog-types.js";
36
+ import type { BootstrapStateStore } from "../bootstrap-state.js";
37
+
38
+ export interface PiChangelogRouteDeps {
39
+ /** When provided, route returns 503 unless bootstrap status is "ready". */
40
+ bootstrapState?: BootstrapStateStore;
41
+ }
42
+
43
+ interface QueryShape {
44
+ pkg?: string;
45
+ from?: string;
46
+ to?: string;
47
+ }
48
+
49
+ export function registerPiChangelogRoutes(
50
+ fastify: FastifyInstance,
51
+ deps: PiChangelogRouteDeps,
52
+ ): void {
53
+ const { bootstrapState } = deps;
54
+
55
+ const bootstrapGate = async (
56
+ _req: unknown,
57
+ reply: { code: (n: number) => { send: (body: unknown) => unknown } },
58
+ ): Promise<unknown> => {
59
+ if (!bootstrapState) return undefined;
60
+ const status = bootstrapState.get().status;
61
+ if (status === "ready") return undefined;
62
+ return reply.code(503).send({
63
+ success: false,
64
+ error: `pi not yet installed (bootstrap status: ${status})`,
65
+ bootstrap: status,
66
+ });
67
+ };
68
+
69
+ fastify.get<{ Querystring: QueryShape }>(
70
+ "/api/pi-core/changelog",
71
+ { preHandler: bootstrapGate as any },
72
+ async (request, reply) => {
73
+ const pkg = (request.query.pkg ?? "").trim();
74
+ const from = (request.query.from ?? "").trim();
75
+ const to = (request.query.to ?? "").trim();
76
+
77
+ // Validate `pkg` against the core whitelist BEFORE touching
78
+ // the filesystem — prevents arbitrary path reads via crafted
79
+ // input.
80
+ if (!pkg || !CORE_PACKAGE_NAMES.includes(pkg)) {
81
+ return reply.code(400).send({
82
+ success: false,
83
+ error: `pkg must be one of: ${CORE_PACKAGE_NAMES.join(", ")}`,
84
+ });
85
+ }
86
+
87
+ // Validate version range using the existing parseVersion helper
88
+ // from pi-version-skew. Both endpoints are required.
89
+ if (!from || !to) {
90
+ return reply.code(400).send({
91
+ success: false,
92
+ error: "from and to query params are required",
93
+ });
94
+ }
95
+ if (!parseVersion(from) || !parseVersion(to)) {
96
+ return reply.code(400).send({
97
+ success: false,
98
+ error: "from and to must be parseable semver versions",
99
+ });
100
+ }
101
+
102
+ const located = findChangelogPath(pkg);
103
+
104
+ // Spec: package not installed / CHANGELOG missing → 200 with empty body.
105
+ if (!located) {
106
+ const body: ChangelogResponse = {
107
+ pkg,
108
+ from,
109
+ to,
110
+ releases: [],
111
+ hasBreaking: false,
112
+ changelogUrl: null,
113
+ parsedAt: new Date().toISOString(),
114
+ };
115
+ return body;
116
+ }
117
+
118
+ // Read package.json once for both URLs (raw for the parser,
119
+ // human for the dialog footer link).
120
+ const pkgJson = readPackageJson(located.packageDir);
121
+ const rawUrl = pkgJson ? deriveChangelogRawUrl(pkgJson.repository) : null;
122
+ const changelogUrl = pkgJson ? deriveChangelogUrl(pkgJson.repository) : null;
123
+
124
+ // Try remote first — the upstream CHANGELOG describes versions
125
+ // newer than the locally-installed tarball. Fall back to local
126
+ // on failure / offline / non-GitHub repo. See change:
127
+ // read-changelog-from-github.
128
+ let allReleases: ChangelogRelease[] | undefined;
129
+ let usedRemote = false;
130
+
131
+ if (rawUrl) {
132
+ const cached = getCachedRemoteChangelog(pkg);
133
+ if (cached && !cached.expired) {
134
+ // Within TTL — reuse cached result, no fetch.
135
+ allReleases = cached.releases;
136
+ usedRemote = true;
137
+ } else {
138
+ const fetchResult = await fetchRemoteChangelog(rawUrl, {
139
+ etag: cached?.etag ?? null,
140
+ });
141
+ if (fetchResult?.status === "ok") {
142
+ const parsed = parseChangelog(fetchResult.text);
143
+ setRemoteChangelog(pkg, parsed, fetchResult.etag);
144
+ allReleases = parsed;
145
+ usedRemote = true;
146
+ } else if (fetchResult?.status === "not-modified" && cached) {
147
+ refreshRemoteChangelogTtl(pkg);
148
+ allReleases = cached.releases;
149
+ usedRemote = true;
150
+ }
151
+ // null result (network error / offline / non-2xx): fall through
152
+ // to local read below.
153
+ }
154
+ }
155
+
156
+ if (!usedRemote) {
157
+ try {
158
+ allReleases = readAndParseChangelog(pkg, located.changelogPath);
159
+ } catch (err: any) {
160
+ request.log.warn(
161
+ { err: err?.message },
162
+ "[pi-changelog-routes] local read/parse failed; returning empty",
163
+ );
164
+ allReleases = [];
165
+ }
166
+ }
167
+
168
+ // Filter to (from, to]. Unparseable release versions are
169
+ // dropped — conservative.
170
+ const filtered = (allReleases ?? []).filter((r) => {
171
+ const rv = parseVersion(r.version);
172
+ if (!rv) return false;
173
+ return compareVersions(r.version, from) > 0 &&
174
+ compareVersions(r.version, to) <= 0;
175
+ });
176
+
177
+ const hasBreaking = filtered.some((r) => r.breaking.length > 0);
178
+
179
+ const body: ChangelogResponse = {
180
+ pkg,
181
+ from,
182
+ to,
183
+ releases: filtered,
184
+ hasBreaking,
185
+ changelogUrl,
186
+ parsedAt: new Date().toISOString(),
187
+ };
188
+ return body;
189
+ },
190
+ );
191
+ }
192
+
193
+ // Re-export for tests that need to bypass the cache.
194
+ export { parseChangelog };
@@ -5,7 +5,7 @@
5
5
  * POST /api/pi-core/update { packages?: string[] }
6
6
  *
7
7
  * Complements /api/packages/* (extension management): this endpoint covers
8
- * globally-installed pi CLI packages like @mariozechner/pi-coding-agent,
8
+ * globally-installed pi CLI packages like @earendil-works/pi-coding-agent,
9
9
  * pi-dashboard itself, pi-model-proxy, etc.
10
10
  */
11
11
  import type { FastifyInstance } from "fastify";
@@ -22,6 +22,7 @@ import { getLatestCatalogue } from "../provider-catalogue-cache.js";
22
22
  import { startCallbackServer } from "../oauth-callback-server.js";
23
23
  import type { PiGateway } from "../pi-gateway.js";
24
24
  import type { BrowserGateway } from "../browser-gateway.js";
25
+ import { refreshModelRegistry } from "../model-proxy/registry-singleton.js";
25
26
 
26
27
  // ── In-memory flow store (short-lived PKCE + device code state) ──────────────
27
28
 
@@ -88,8 +89,14 @@ export function registerProviderAuthRoutes(
88
89
  const { piGateway, browserGateway } = deps;
89
90
 
90
91
  function notifyBridges() {
92
+ // Tell every bridge to reload auth.json + refresh its model registry.
93
+ // Each bridge will then push a fresh per-session models_list (and
94
+ // providers_list); browsers pick those up via the existing per-session
95
+ // broadcast and update modelsMap / catalogue cache without needing a
96
+ // global wipe. See change: simplify-model-selection-channels.
91
97
  piGateway.broadcast({ type: "credentials_updated" });
92
- browserGateway.broadcastToAll({ type: "models_refreshed" });
98
+ // Eager-refresh model proxy registry so /v1/models reflects the change.
99
+ refreshModelRegistry().catch(() => {});
93
100
  }
94
101
 
95
102
  // List OAuth providers
@@ -9,6 +9,9 @@ import type { NetworkGuard } from "./route-deps.js";
9
9
  import type { PiGateway } from "../pi-gateway.js";
10
10
  import type { BrowserGateway } from "../browser-gateway.js";
11
11
  import { probeProvider, resolveProbeApiKey, type ProbeApi } from "../provider-probe.js";
12
+ import { refreshModelRegistry } from "../model-proxy/registry-singleton.js";
13
+ import { isSelfPointing, collectDashboardOrigins } from "../model-proxy/recursion-guard.js";
14
+ import { getTunnelUrl } from "../tunnel.js";
12
15
 
13
16
  const REDACTED = "***";
14
17
  const CONFIG_PATH = join(homedir(), ".pi", "agent", "providers.json");
@@ -47,7 +50,7 @@ function redactProviders(
47
50
  return redacted;
48
51
  }
49
52
 
50
- export function registerProviderRoutes(fastify: FastifyInstance, deps: { networkGuard: NetworkGuard; piGateway?: PiGateway; browserGateway?: BrowserGateway }): void {
53
+ export function registerProviderRoutes(fastify: FastifyInstance, deps: { networkGuard: NetworkGuard; piGateway?: PiGateway; browserGateway?: BrowserGateway; port?: number }): void {
51
54
  const { networkGuard, piGateway } = deps;
52
55
  fastify.get(
53
56
  "/api/providers",
@@ -68,6 +71,23 @@ export function registerProviderRoutes(fastify: FastifyInstance, deps: { network
68
71
  }
69
72
 
70
73
  const incoming = body.providers as Record<string, ProviderEntry>;
74
+
75
+ // Recursion guard: reject providers pointing back at the dashboard
76
+ const dashboardPort = deps.port ?? 8000;
77
+ const tunnelUrl = getTunnelUrl();
78
+ const tunnelHostname = tunnelUrl ? new URL(tunnelUrl).hostname : undefined;
79
+ const origins = collectDashboardOrigins(dashboardPort, { tunnelHostname });
80
+ for (const [name, entry] of Object.entries(incoming)) {
81
+ if (entry.baseUrl && isSelfPointing(entry.baseUrl, origins)) {
82
+ return reply.code(400).send({
83
+ success: false,
84
+ code: "RECURSIVE_PROXY",
85
+ message: `Provider "${name}" baseUrl points back at this dashboard`,
86
+ offendingBaseUrl: entry.baseUrl,
87
+ });
88
+ }
89
+ }
90
+
71
91
  const existing = readProvidersRaw();
72
92
 
73
93
  // Merge: preserve redacted apiKey values from existing file
@@ -98,13 +118,16 @@ export function registerProviderRoutes(fastify: FastifyInstance, deps: { network
98
118
  mkdirSync(dir, { recursive: true });
99
119
  writeFileSync(CONFIG_PATH, JSON.stringify(fileData, null, 2) + "\n", "utf-8");
100
120
 
101
- // Broadcast credentials_updated so all sessions refresh their model registries
121
+ // Broadcast credentials_updated so each bridge re-reads providers.json
122
+ // and pushes a fresh per-session models_list. Browsers receive those
123
+ // pushes via the existing per-session broadcast — no global wipe.
124
+ // See change: simplify-model-selection-channels.
102
125
  if (piGateway) {
103
126
  piGateway.broadcast({ type: "credentials_updated" });
104
127
  }
105
- if (deps.browserGateway) {
106
- deps.browserGateway.broadcastToAll({ type: "models_refreshed" });
107
- }
128
+
129
+ // Eager-refresh model proxy registry so /v1/models reflects the change.
130
+ refreshModelRegistry().catch(() => {});
108
131
 
109
132
  return { success: true };
110
133
  },
@@ -13,7 +13,9 @@ import type { NetworkGuard } from "./route-deps.js";
13
13
  import { detectEditors, EDITORS } from "../editor-registry.js";
14
14
  import { detectCodeServerBinary, resetDetectionCache } from "../editor-detection.js";
15
15
  import { readConfigRedacted, writeConfigPartial } from "../config-api.js";
16
- import { createTunnel, deleteTunnel, getTunnelStatus } from "../tunnel.js";
16
+ import { createTunnel, deleteTunnel, getTunnelStatus, getTunnelUrl } from "../tunnel.js";
17
+ import { getModelProxyStatus } from "../model-proxy/registry-singleton.js";
18
+ import { startTunnelWatchdog, stopTunnelWatchdog } from "../tunnel-watchdog.js";
17
19
  import { spawnRestart } from "../restart-helper.js";
18
20
  import { spawn } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
19
21
  import path from "node:path";
@@ -152,6 +154,29 @@ export function registerSystemRoutes(
152
154
  if (partial.openspec !== undefined && directoryService) {
153
155
  directoryService.reconfigurePolling(reloaded.openspec);
154
156
  }
157
+ // Live-reload tunnel watchdog when its config changes (no restart needed).
158
+ // We always restart the watchdog when partial.tunnel is present and a
159
+ // tunnel is currently active — covers both watchdog flag changes and
160
+ // numeric tweaks. Cheap operation: stop + start with new config.
161
+ if (partial.tunnel !== undefined) {
162
+ config.tunnelWatchdog = reloaded.tunnel.watchdog;
163
+ if (getTunnelUrl()) {
164
+ stopTunnelWatchdog();
165
+ const wd = reloaded.tunnel.watchdog;
166
+ if (wd?.enabled !== false) {
167
+ startTunnelWatchdog(
168
+ {
169
+ getUrl: getTunnelUrl,
170
+ recycle: async () => {
171
+ await deleteTunnel(config.port);
172
+ return await createTunnel(config.port, config.tunnelReservedToken);
173
+ },
174
+ },
175
+ wd,
176
+ );
177
+ }
178
+ }
179
+ }
155
180
 
156
181
  return { success: true, restartRequired: result.restartRequired };
157
182
  },
@@ -167,13 +192,29 @@ export function registerSystemRoutes(
167
192
  if (status.status === "active") return { ok: true, url: status.url };
168
193
  if (status.status === "unavailable") return { ok: false, error: "zrok not installed" };
169
194
  const url = await createTunnel(config.port, config.tunnelReservedToken);
170
- if (url) return { ok: true, url };
195
+ if (url) {
196
+ const wd = config.tunnelWatchdog;
197
+ if (wd?.enabled !== false) {
198
+ startTunnelWatchdog(
199
+ {
200
+ getUrl: getTunnelUrl,
201
+ recycle: async () => {
202
+ await deleteTunnel(config.port);
203
+ return await createTunnel(config.port, config.tunnelReservedToken);
204
+ },
205
+ },
206
+ wd,
207
+ );
208
+ }
209
+ return { ok: true, url };
210
+ }
171
211
  return { ok: false, error: "Failed to create tunnel" };
172
212
  });
173
213
 
174
214
  fastify.post("/api/tunnel-disconnect", async () => {
175
215
  // Pass port so orphan zrok processes bound to this endpoint are also
176
216
  // swept (not just the one we tracked via pid-file).
217
+ stopTunnelWatchdog();
177
218
  await deleteTunnel(config.port);
178
219
  return { ok: true };
179
220
  });
@@ -206,6 +247,7 @@ export function registerSystemRoutes(
206
247
  },
207
248
  agents: agentMetrics,
208
249
  plugins: getPluginStatusStore().listAll(),
250
+ proxy: getModelProxyStatus(),
209
251
  };
210
252
  });
211
253
 
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ # PATH-shim used in keeper.test.ts to make `pi --mode rpc` invoke our
3
+ # mock-pi.cjs instead of the real pi binary.
4
+ #
5
+ # The keeper spawns `pi --mode rpc` from PATH; we prepend the dir
6
+ # containing this script (named `pi`) to PATH so this shim wins.
7
+ # The path to mock-pi.cjs is passed via env var so the shim can be
8
+ # placed anywhere without copying mock-pi.cjs alongside it.
9
+ exec node "${MOCK_PI_CJS_PATH:?MOCK_PI_CJS_PATH env var required}" "$@"
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mock pi for keeper integration tests.
4
+ *
5
+ * Reads JSON-line input from stdin and appends each line to the file at
6
+ * `process.env.MOCK_PI_LOG`. Exits 0 on stdin EOF.
7
+ *
8
+ * Behavior modes (via env):
9
+ * MOCK_PI_MODE=normal (default) — read until EOF, log lines, exit 0
10
+ * MOCK_PI_MODE=crash — exit non-zero immediately (tests
11
+ * keeper crash-detection window)
12
+ *
13
+ * CommonJS-pure, only Node built-ins.
14
+ */
15
+ "use strict";
16
+
17
+ const fs = require("fs");
18
+
19
+ const mode = process.env.MOCK_PI_MODE || "normal";
20
+ const logPath = process.env.MOCK_PI_LOG;
21
+
22
+ if (mode === "crash") {
23
+ process.stderr.write("[mock-pi] crash mode: exiting 1 immediately\n");
24
+ process.exit(1);
25
+ }
26
+
27
+ if (!logPath) {
28
+ process.stderr.write("[mock-pi] FATAL: MOCK_PI_LOG env var required\n");
29
+ process.exit(2);
30
+ }
31
+
32
+ let buf = "";
33
+ process.stdin.setEncoding("utf8");
34
+ process.stdin.on("data", (chunk) => {
35
+ buf += chunk;
36
+ let nl;
37
+ // eslint-disable-next-line no-cond-assign
38
+ while ((nl = buf.indexOf("\n")) !== -1) {
39
+ const line = buf.slice(0, nl);
40
+ buf = buf.slice(nl + 1);
41
+ fs.appendFileSync(logPath, line + "\n");
42
+ }
43
+ });
44
+ process.stdin.on("end", () => {
45
+ if (buf.length > 0) {
46
+ fs.appendFileSync(logPath, buf + "\n");
47
+ }
48
+ process.exit(0);
49
+ });
50
+ process.stdin.on("error", () => process.exit(0));