@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.
- package/AGENTS.md +26 -5
- package/README.md +49 -7
- package/docs/architecture.md +129 -1
- package/package.json +15 -15
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +78 -8
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
- package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
- package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
- package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
- package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
- package/packages/extension/src/ask-user-tool.ts +1 -1
- package/packages/extension/src/bridge-context.ts +68 -4
- package/packages/extension/src/bridge.ts +79 -11
- package/packages/extension/src/command-handler.ts +95 -15
- package/packages/extension/src/flow-event-wiring.ts +1 -1
- package/packages/extension/src/multiselect-list.ts +1 -1
- package/packages/extension/src/pi-env.d.ts +16 -9
- package/packages/extension/src/prompt-expander.ts +74 -63
- package/packages/extension/src/provider-register.ts +16 -9
- package/packages/extension/src/retry-tracker.ts +123 -0
- package/packages/extension/src/server-launcher.ts +31 -70
- package/packages/extension/src/session-sync.ts +10 -1
- package/packages/extension/src/slash-dispatch.ts +123 -0
- package/packages/extension/src/usage-limit-orderer.ts +76 -0
- package/packages/server/bin/pi-dashboard.mjs +84 -0
- package/packages/server/package.json +8 -7
- package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
- package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
- package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
- package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +16 -4
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service.test.ts +2 -2
- package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
- package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +68 -1
- package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
- package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +316 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
- package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
- package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
- package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
- package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
- package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
- package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
- package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
- package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
- package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
- package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
- package/packages/server/src/__tests__/package-routes.test.ts +1 -1
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
- package/packages/server/src/__tests__/pi-ai-shape.test.ts +147 -0
- package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
- package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +62 -3
- package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
- package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
- package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
- package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +13 -23
- package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +3 -3
- package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +84 -0
- package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
- package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
- package/packages/server/src/auth-plugin.ts +3 -0
- package/packages/server/src/bootstrap-state.ts +10 -0
- package/packages/server/src/browser-gateway.ts +27 -10
- package/packages/server/src/browser-handlers/handler-context.ts +9 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +128 -19
- package/packages/server/src/changelog-fs.ts +167 -0
- package/packages/server/src/changelog-parser.ts +321 -0
- package/packages/server/src/changelog-remote.ts +134 -0
- package/packages/server/src/cli.ts +62 -82
- package/packages/server/src/config-api.ts +14 -2
- package/packages/server/src/directory-service.ts +106 -4
- package/packages/server/src/event-wiring.ts +90 -6
- package/packages/server/src/headless-pid-registry.ts +344 -37
- package/packages/server/src/legacy-pi-cleanup.ts +151 -0
- package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
- package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
- package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
- package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
- package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
- package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
- package/packages/server/src/model-proxy/api-key-store.ts +87 -0
- package/packages/server/src/model-proxy/auth-gate.ts +116 -0
- package/packages/server/src/model-proxy/concurrency.ts +76 -0
- package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
- package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
- package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
- package/packages/server/src/model-proxy/convert/index.ts +8 -0
- package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
- package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
- package/packages/server/src/model-proxy/convert/types.ts +70 -0
- package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
- package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
- package/packages/server/src/model-proxy/internal-registry.ts +157 -0
- package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
- package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
- package/packages/server/src/model-proxy/request-log.ts +53 -0
- package/packages/server/src/model-proxy/streamer.ts +59 -0
- package/packages/server/src/openspec-group-store.ts +490 -0
- package/packages/server/src/pending-client-correlations.ts +73 -0
- package/packages/server/src/pending-fork-registry.ts +24 -12
- package/packages/server/src/pi-core-checker.ts +77 -17
- package/packages/server/src/pi-core-updater.ts +16 -6
- package/packages/server/src/pi-dev-version-check.ts +145 -0
- package/packages/server/src/pi-gateway.ts +4 -0
- package/packages/server/src/pi-version-skew.ts +12 -4
- package/packages/server/src/process-manager.ts +182 -11
- package/packages/server/src/provider-auth-storage.ts +29 -47
- package/packages/server/src/provider-catalogue-cache.ts +24 -18
- package/packages/server/src/restart-helper.ts +17 -16
- package/packages/server/src/routes/bootstrap-routes.ts +37 -0
- package/packages/server/src/routes/jj-routes.ts +3 -0
- package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
- package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
- package/packages/server/src/routes/model-proxy-routes.ts +330 -0
- package/packages/server/src/routes/openspec-group-routes.ts +231 -0
- package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
- package/packages/server/src/routes/pi-core-routes.ts +1 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -1
- package/packages/server/src/routes/provider-routes.ts +28 -5
- package/packages/server/src/routes/system-routes.ts +44 -2
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
- package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
- package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
- package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
- package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
- package/packages/server/src/server.ts +254 -60
- package/packages/server/src/session-api.ts +63 -4
- package/packages/server/src/session-discovery.ts +1 -1
- package/packages/server/src/session-file-reader.ts +1 -1
- package/packages/server/src/spawn-register-watchdog.ts +62 -7
- package/packages/server/src/spawn-token.ts +20 -0
- package/packages/server/src/tunnel-watchdog.ts +230 -0
- package/packages/server/src/tunnel.ts +5 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +24 -17
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +5 -3
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
- package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
- package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
- package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
- package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
- package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
- package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
- package/packages/shared/src/bootstrap-install.ts +1 -1
- package/packages/shared/src/browser-protocol.ts +70 -0
- package/packages/shared/src/changelog-types.ts +111 -0
- package/packages/shared/src/config.ts +172 -2
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
- package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
- package/packages/shared/src/platform/binary-lookup.ts +204 -0
- package/packages/shared/src/platform/node-spawn.ts +71 -26
- package/packages/shared/src/protocol.ts +27 -1
- package/packages/shared/src/recommended-extensions.ts +18 -0
- package/packages/shared/src/rest-api.ts +219 -1
- package/packages/shared/src/server-launcher.ts +277 -0
- package/packages/shared/src/skill-block-parser.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
- package/packages/shared/src/tool-registry/definitions.ts +15 -3
- package/packages/shared/src/types.ts +62 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -53
- package/packages/shared/src/resolve-jiti.ts +0 -102
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import os from "node:os";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
const _require = createRequire(import.meta.url);
|
|
15
|
+
const _lockfile = _require("proper-lockfile") as typeof import("proper-lockfile");
|
|
13
16
|
import type { ProviderAuthStatus } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
|
|
14
17
|
import type { ProviderInfo } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
15
18
|
import { getAllHandlers, type ProviderHandler } from "./provider-auth-handlers.js";
|
|
@@ -19,10 +22,6 @@ import { getLatestCatalogue } from "./provider-catalogue-cache.js";
|
|
|
19
22
|
|
|
20
23
|
const AUTH_DIR = path.join(os.homedir(), ".pi", "agent");
|
|
21
24
|
const AUTH_PATH = path.join(AUTH_DIR, "auth.json");
|
|
22
|
-
const LOCK_PATH = AUTH_PATH + ".lock";
|
|
23
|
-
const LOCK_STALE_MS = 10_000;
|
|
24
|
-
const LOCK_RETRY_MS = 50;
|
|
25
|
-
const LOCK_MAX_RETRIES = 40; // 40 × 50ms = 2s max wait
|
|
26
25
|
|
|
27
26
|
export type ApiKeyCredential = { type: "api_key"; key: string };
|
|
28
27
|
export type OAuthCredential = { type: "oauth"; refresh: string; access: string; expires: number; [k: string]: unknown };
|
|
@@ -35,46 +34,35 @@ interface OAuthProviderMeta {
|
|
|
35
34
|
flowType: "auth_code" | "device_code";
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
// ── Lock helpers
|
|
37
|
+
// ── Lock helpers (proper-lockfile) ───────────────────────────────────────────
|
|
38
|
+
//
|
|
39
|
+
// Upgraded from mkdir-based lock to proper-lockfile to match pi-coding-agent's
|
|
40
|
+
// AuthStorage lock convention. See change: add-dashboard-model-proxy task 2.5.
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Run `fn` while holding a proper-lockfile lock on auth.json.
|
|
44
|
+
* Ensures the file exists (lockfile requires the target to exist).
|
|
45
|
+
*/
|
|
46
|
+
function withLock<T>(fn: () => T): T {
|
|
42
47
|
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return;
|
|
47
|
-
} catch (err: any) {
|
|
48
|
-
if (err.code === "EEXIST") {
|
|
49
|
-
// Check for stale lock
|
|
50
|
-
try {
|
|
51
|
-
const stat = fs.statSync(LOCK_PATH);
|
|
52
|
-
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
53
|
-
fs.rmdirSync(LOCK_PATH);
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
} catch { /* stat failed, retry */ }
|
|
57
|
-
// Wait and retry
|
|
58
|
-
const waitMs = LOCK_RETRY_MS + Math.random() * LOCK_RETRY_MS;
|
|
59
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitMs);
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
throw err;
|
|
63
|
-
}
|
|
48
|
+
if (!fs.existsSync(AUTH_PATH)) {
|
|
49
|
+
// Create empty auth file so lockfile can lock it
|
|
50
|
+
try { fs.writeFileSync(AUTH_PATH, "{}\n", { flag: "wx" }); } catch { /* race-safe */ }
|
|
64
51
|
}
|
|
65
|
-
throw new Error("Failed to acquire auth.json lock after retries");
|
|
66
|
-
}
|
|
67
52
|
|
|
68
|
-
|
|
69
|
-
|
|
53
|
+
const release = _lockfile.lockSync(AUTH_PATH, {
|
|
54
|
+
stale: 10_000,
|
|
55
|
+
realpath: false,
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
return fn();
|
|
59
|
+
} finally {
|
|
60
|
+
try { release(); } catch { /* ignore cleanup errors */ }
|
|
61
|
+
}
|
|
70
62
|
}
|
|
71
63
|
|
|
72
64
|
// ── File operations ──────────────────────────────────────────────────────────
|
|
73
65
|
|
|
74
|
-
function ensureDir(): void {
|
|
75
|
-
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
66
|
export function readAuthJson(): AuthData {
|
|
79
67
|
try {
|
|
80
68
|
const raw = fs.readFileSync(AUTH_PATH, "utf-8");
|
|
@@ -86,7 +74,7 @@ export function readAuthJson(): AuthData {
|
|
|
86
74
|
}
|
|
87
75
|
|
|
88
76
|
function writeAuthJson(data: AuthData): void {
|
|
89
|
-
|
|
77
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
90
78
|
const tmp = AUTH_PATH + ".tmp";
|
|
91
79
|
const content = JSON.stringify(data, null, 2) + "\n";
|
|
92
80
|
|
|
@@ -104,25 +92,19 @@ function writeAuthJson(data: AuthData): void {
|
|
|
104
92
|
// ── Public API: write/remove ─────────────────────────────────────────────────
|
|
105
93
|
|
|
106
94
|
export function writeCredential(provider: string, credential: AuthCredential): void {
|
|
107
|
-
|
|
108
|
-
try {
|
|
95
|
+
withLock(() => {
|
|
109
96
|
const data = readAuthJson();
|
|
110
97
|
data[provider] = credential;
|
|
111
98
|
writeAuthJson(data);
|
|
112
|
-
}
|
|
113
|
-
releaseLock();
|
|
114
|
-
}
|
|
99
|
+
});
|
|
115
100
|
}
|
|
116
101
|
|
|
117
102
|
export function removeCredential(provider: string): void {
|
|
118
|
-
|
|
119
|
-
try {
|
|
103
|
+
withLock(() => {
|
|
120
104
|
const data = readAuthJson();
|
|
121
105
|
delete data[provider];
|
|
122
106
|
writeAuthJson(data);
|
|
123
|
-
}
|
|
124
|
-
releaseLock();
|
|
125
|
-
}
|
|
107
|
+
});
|
|
126
108
|
}
|
|
127
109
|
|
|
128
110
|
// ── Pure status builder (testable) ───────────────────────────────────────────
|
|
@@ -1,41 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* In-memory cache of
|
|
2
|
+
* In-memory cache of the most-recently-pushed provider catalogue.
|
|
3
3
|
*
|
|
4
4
|
* Each pi process pushes a `providers_list` over WS, derived from its
|
|
5
|
-
* `ModelRegistry`. The server caches
|
|
6
|
-
*
|
|
5
|
+
* `ModelRegistry`. The server caches the latest snapshot. `GET
|
|
6
|
+
* /api/provider-auth/status` reads `getLatestCatalogue()`.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* The catalogue is a property of the machine's auth + provider config,
|
|
9
|
+
* not of individual sessions: every bridge in the same process tree
|
|
10
|
+
* derives an identical catalogue from `~/.pi/agent/auth.json` +
|
|
11
|
+
* `~/.pi/agent/providers.json` + pi-ai's MODELS table. We therefore
|
|
12
|
+
* keep ONE global snapshot — the last push wins. A previous version
|
|
13
|
+
* kept a per-session Map plus a `changed` deep-equality gate to avoid
|
|
14
|
+
* spurious `models_refreshed` broadcasts; that broadcast is gone
|
|
15
|
+
* (see change: simplify-model-selection-channels), so the gate is
|
|
16
|
+
* unnecessary and the per-session split was redundant.
|
|
17
|
+
*
|
|
18
|
+
* See changes: replace-hardcoded-provider-lists,
|
|
19
|
+
* fix-providers-list-spurious-models-refreshed,
|
|
20
|
+
* simplify-model-selection-channels.
|
|
9
21
|
*/
|
|
10
22
|
import type { ProviderInfo } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
11
23
|
|
|
12
|
-
const bySession = new Map<string, ProviderInfo[]>();
|
|
13
24
|
let latest: ProviderInfo[] | null = null;
|
|
14
25
|
|
|
15
|
-
|
|
16
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Replace the cached catalogue. Called from event-wiring.ts on every
|
|
28
|
+
* `providers_list` arrival. No-op semantically beyond the assignment;
|
|
29
|
+
* no signal to callers because no caller needs one.
|
|
30
|
+
*/
|
|
31
|
+
export function setCatalogueForSession(_sessionId: string, providers: ProviderInfo[]): void {
|
|
17
32
|
latest = providers;
|
|
18
33
|
}
|
|
19
34
|
|
|
20
|
-
export function getCatalogueForSession(sessionId: string): ProviderInfo[] | undefined {
|
|
21
|
-
return bySession.get(sessionId);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
35
|
/**
|
|
25
36
|
* Most recent catalogue across any session. Returns [] when no bridge
|
|
26
|
-
* has pushed yet — callers should treat that as "waiting for pi"
|
|
37
|
+
* has pushed yet — callers should treat that as "waiting for pi" and
|
|
38
|
+
* may issue a `request_providers` nudge to fetch one synchronously.
|
|
27
39
|
*/
|
|
28
40
|
export function getLatestCatalogue(): ProviderInfo[] {
|
|
29
41
|
return latest ?? [];
|
|
30
42
|
}
|
|
31
43
|
|
|
32
|
-
export function clearForSession(sessionId: string): void {
|
|
33
|
-
bySession.delete(sessionId);
|
|
34
|
-
if (bySession.size === 0) latest = null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
44
|
/** Test-only: reset all cached state. */
|
|
38
45
|
export function _resetForTests(): void {
|
|
39
|
-
bySession.clear();
|
|
40
46
|
latest = null;
|
|
41
47
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* See change: fix-windows-server-parity.
|
|
13
13
|
*/
|
|
14
14
|
import { spawn } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
|
|
15
|
-
import { toFileUrl, shouldUrlWrapEntry } from "@blackbelt-technology/pi-dashboard-shared/platform/node-spawn.js";
|
|
15
|
+
import { buildNodeImportArgvParts, toFileUrl, shouldUrlWrapEntry } from "@blackbelt-technology/pi-dashboard-shared/platform/node-spawn.js";
|
|
16
16
|
import os from "node:os";
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
|
|
@@ -40,21 +40,22 @@ export function buildOrchestratorScript(params: RestartParams): string {
|
|
|
40
40
|
// so quoting/path-separator handling is correct on Windows.
|
|
41
41
|
// See change: fix-restart-bridge-auto-start-race.
|
|
42
42
|
const pidPath = path.join(os.homedir(), ".pi", "dashboard", "dashboard.pid");
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// See
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
43
|
+
// Argv shape (loader URL-wrapping + entry URL-wrapping rule) is
|
|
44
|
+
// owned by `buildNodeImportArgvParts` in `node-spawn.ts` — the same
|
|
45
|
+
// helper `spawnNodeScript` calls. Keeps the `--import` argv shape
|
|
46
|
+
// in exactly one place.
|
|
47
|
+
// See change: unify-server-launch-ts-loader.
|
|
48
|
+
const spawnArgs: string[] = params.loader
|
|
49
|
+
? buildNodeImportArgvParts({
|
|
50
|
+
loader: params.loader,
|
|
51
|
+
entry: params.cliPath,
|
|
52
|
+
args: ["start", ...params.extraArgs],
|
|
53
|
+
})
|
|
54
|
+
: [
|
|
55
|
+
shouldUrlWrapEntry(params.loader) ? toFileUrl(params.cliPath) : params.cliPath,
|
|
56
|
+
"start",
|
|
57
|
+
...params.extraArgs,
|
|
58
|
+
];
|
|
58
59
|
|
|
59
60
|
// The script runs in a fresh Node process. Keep it self-contained and use
|
|
60
61
|
// only built-ins (net, http, fs, child_process). JSON.stringify is used to
|
|
@@ -13,6 +13,10 @@ import type { FastifyInstance } from "fastify";
|
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
14
|
import type { BootstrapStateStore } from "../bootstrap-state.js";
|
|
15
15
|
import type { NetworkGuard } from "./route-deps.js";
|
|
16
|
+
import {
|
|
17
|
+
detectLegacyPiInstalls,
|
|
18
|
+
uninstallLegacyPi,
|
|
19
|
+
} from "../legacy-pi-cleanup.js";
|
|
16
20
|
|
|
17
21
|
export interface BootstrapRouteDeps {
|
|
18
22
|
bootstrapState: BootstrapStateStore;
|
|
@@ -85,4 +89,37 @@ export function registerBootstrapRoutes(
|
|
|
85
89
|
return reply.code(202).send({ ticketId, status: "accepted" });
|
|
86
90
|
},
|
|
87
91
|
);
|
|
92
|
+
|
|
93
|
+
// ── Legacy pi cleanup ──────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
// Refresh detection on demand (also runs at server startup).
|
|
96
|
+
fastify.get(
|
|
97
|
+
"/api/bootstrap/legacy-pi",
|
|
98
|
+
{ preHandler: networkGuard },
|
|
99
|
+
async () => {
|
|
100
|
+
const installs = detectLegacyPiInstalls();
|
|
101
|
+
bootstrapState.set({ legacyPiInstalls: installs });
|
|
102
|
+
return { installs };
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Remove all currently-detected legacy installs. Returns per-install
|
|
107
|
+
// result; partial failures are reported but do not abort the others.
|
|
108
|
+
fastify.post(
|
|
109
|
+
"/api/bootstrap/legacy-pi/cleanup",
|
|
110
|
+
{ preHandler: networkGuard },
|
|
111
|
+
async () => {
|
|
112
|
+
const before = detectLegacyPiInstalls();
|
|
113
|
+
if (before.length === 0) {
|
|
114
|
+
bootstrapState.set({ legacyPiInstalls: [] });
|
|
115
|
+
return { results: [], remaining: [] };
|
|
116
|
+
}
|
|
117
|
+
const results = uninstallLegacyPi(before);
|
|
118
|
+
// Re-scan so the UI shows any installs that survived (e.g. permission
|
|
119
|
+
// error). The store mirrors the post-cleanup state.
|
|
120
|
+
const remaining = detectLegacyPiInstalls();
|
|
121
|
+
bootstrapState.set({ legacyPiInstalls: remaining });
|
|
122
|
+
return { results, remaining };
|
|
123
|
+
},
|
|
124
|
+
);
|
|
88
125
|
}
|
|
@@ -22,6 +22,7 @@ import type { ApiResponse } from "@blackbelt-technology/pi-dashboard-shared/type
|
|
|
22
22
|
import type { BrowserGateway } from "../browser-gateway.js";
|
|
23
23
|
import type { PendingAttachRegistry } from "../pending-attach-registry.js";
|
|
24
24
|
import { spawnPiSession } from "../process-manager.js";
|
|
25
|
+
import { keeperOptsFromSpawnResult } from "../headless-pid-registry.js";
|
|
25
26
|
import type { NetworkGuard } from "./route-deps.js";
|
|
26
27
|
import { safeRealpathSync } from "../resolve-path.js";
|
|
27
28
|
|
|
@@ -218,6 +219,8 @@ export function registerJjRoutes(fastify: FastifyInstance, deps: JjRoutesDeps) {
|
|
|
218
219
|
spawnResult.pid,
|
|
219
220
|
realDestPath,
|
|
220
221
|
spawnResult.process,
|
|
222
|
+
spawnResult.spawnToken,
|
|
223
|
+
keeperOptsFromSpawnResult(spawnResult),
|
|
221
224
|
);
|
|
222
225
|
}
|
|
223
226
|
if (!spawnResult.success) {
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST routes for proxy API key management.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/model-proxy/api-keys → list keys (redacted)
|
|
5
|
+
* POST /api/model-proxy/api-keys → create key (cleartext returned ONCE)
|
|
6
|
+
* POST /api/model-proxy/api-keys/:id/revoke → soft-delete (set revokedAt)
|
|
7
|
+
* DELETE /api/model-proxy/api-keys/:id → hard-delete (purge)
|
|
8
|
+
*
|
|
9
|
+
* All routes are JWT-gated (NOT proxy-key-gated — this is the management surface).
|
|
10
|
+
*
|
|
11
|
+
* See change: add-dashboard-model-proxy.
|
|
12
|
+
*/
|
|
13
|
+
import crypto from "node:crypto";
|
|
14
|
+
import type { FastifyInstance } from "fastify";
|
|
15
|
+
import type { ProxyApiKey } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
16
|
+
import type { NetworkGuard } from "./route-deps.js";
|
|
17
|
+
import { generateKey, hashKey } from "../model-proxy/api-key-store.js";
|
|
18
|
+
|
|
19
|
+
export interface ApiKeyRoutesDeps {
|
|
20
|
+
networkGuard: NetworkGuard;
|
|
21
|
+
getModelProxyConfig: () => import("@blackbelt-technology/pi-dashboard-shared/config.js").ModelProxyConfig;
|
|
22
|
+
writeModelProxyApiKeys: (apiKeys: ProxyApiKey[]) => Promise<void>;
|
|
23
|
+
getAdminEmail?: () => string | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getUserEmail(request: any): string | undefined {
|
|
27
|
+
return request.user?.email;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function registerModelProxyApiKeyRoutes(
|
|
31
|
+
fastify: FastifyInstance,
|
|
32
|
+
deps: ApiKeyRoutesDeps,
|
|
33
|
+
): void {
|
|
34
|
+
const { networkGuard, getModelProxyConfig, writeModelProxyApiKeys, getAdminEmail } = deps;
|
|
35
|
+
|
|
36
|
+
// ── GET /api/model-proxy/api-keys ─────────────────────────────────
|
|
37
|
+
fastify.get(
|
|
38
|
+
"/api/model-proxy/api-keys",
|
|
39
|
+
{ preHandler: networkGuard },
|
|
40
|
+
async (request) => {
|
|
41
|
+
const config = getModelProxyConfig();
|
|
42
|
+
const userEmail = getUserEmail(request);
|
|
43
|
+
const adminEmail = getAdminEmail?.();
|
|
44
|
+
const isAdmin = adminEmail != null && userEmail === adminEmail;
|
|
45
|
+
|
|
46
|
+
const filtered = config.apiKeys.filter(
|
|
47
|
+
(k) => isAdmin || !k.createdBy || k.createdBy === userEmail,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const active = filtered
|
|
51
|
+
.filter((k) => k.revokedAt == null)
|
|
52
|
+
.map((k) => ({ ...k, hash: "***" }));
|
|
53
|
+
const revoked = filtered
|
|
54
|
+
.filter((k) => k.revokedAt != null)
|
|
55
|
+
.map((k) => ({ ...k, hash: "***" }));
|
|
56
|
+
|
|
57
|
+
return { success: true, data: { keys: active, revoked } };
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// ── POST /api/model-proxy/api-keys ────────────────────────────────
|
|
62
|
+
fastify.post(
|
|
63
|
+
"/api/model-proxy/api-keys",
|
|
64
|
+
{ preHandler: networkGuard },
|
|
65
|
+
async (request, reply) => {
|
|
66
|
+
const body = request.body as any;
|
|
67
|
+
if (!body || typeof body.label !== "string" || !body.label.trim()) {
|
|
68
|
+
return reply.code(400).send({ success: false, error: "label is required" });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const scopes: string[] = Array.isArray(body.scopes) ? body.scopes : ["all"];
|
|
72
|
+
const expiresAt: number | undefined =
|
|
73
|
+
typeof body.expiresAt === "number" ? body.expiresAt : undefined;
|
|
74
|
+
|
|
75
|
+
if (expiresAt != null && expiresAt <= Date.now()) {
|
|
76
|
+
return reply.code(400).send({ success: false, error: "expiresAt must be in the future" });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const cleartext = generateKey();
|
|
80
|
+
const hash = hashKey(cleartext);
|
|
81
|
+
const userEmail = getUserEmail(request);
|
|
82
|
+
|
|
83
|
+
const entry: ProxyApiKey = {
|
|
84
|
+
id: crypto.randomUUID(),
|
|
85
|
+
label: body.label.trim(),
|
|
86
|
+
hash,
|
|
87
|
+
createdAt: Date.now(),
|
|
88
|
+
...(userEmail ? { createdBy: userEmail } : {}),
|
|
89
|
+
scopes,
|
|
90
|
+
...(expiresAt != null ? { expiresAt } : {}),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const config = getModelProxyConfig();
|
|
94
|
+
await writeModelProxyApiKeys([...config.apiKeys, entry]);
|
|
95
|
+
|
|
96
|
+
return reply.code(201).send({
|
|
97
|
+
success: true,
|
|
98
|
+
data: {
|
|
99
|
+
id: entry.id,
|
|
100
|
+
label: entry.label,
|
|
101
|
+
createdBy: entry.createdBy,
|
|
102
|
+
scopes: entry.scopes ?? ["all"],
|
|
103
|
+
createdAt: entry.createdAt,
|
|
104
|
+
...(entry.expiresAt != null ? { expiresAt: entry.expiresAt } : {}),
|
|
105
|
+
key: cleartext, // revealed ONCE
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// ── POST /api/model-proxy/api-keys/:id/revoke ─────────────────────
|
|
112
|
+
fastify.post(
|
|
113
|
+
"/api/model-proxy/api-keys/:id/revoke",
|
|
114
|
+
{ preHandler: networkGuard },
|
|
115
|
+
async (request, reply) => {
|
|
116
|
+
const { id } = request.params as { id: string };
|
|
117
|
+
const config = getModelProxyConfig();
|
|
118
|
+
const entry = config.apiKeys.find((k) => k.id === id);
|
|
119
|
+
|
|
120
|
+
if (!entry) {
|
|
121
|
+
return reply.code(404).send({ success: false, error: "Key not found" });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const userEmail = getUserEmail(request);
|
|
125
|
+
const adminEmail = getAdminEmail?.();
|
|
126
|
+
const isAdmin = adminEmail != null && userEmail === adminEmail;
|
|
127
|
+
|
|
128
|
+
if (!isAdmin && entry.createdBy && entry.createdBy !== userEmail) {
|
|
129
|
+
return reply.code(403).send({ success: false, error: "Not authorized to revoke this key" });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const updated = config.apiKeys.map((k) =>
|
|
133
|
+
k.id === id ? { ...k, revokedAt: Date.now() } : k,
|
|
134
|
+
);
|
|
135
|
+
await writeModelProxyApiKeys(updated);
|
|
136
|
+
|
|
137
|
+
return reply.code(204).send();
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// ── DELETE /api/model-proxy/api-keys/:id ──────────────────────────
|
|
142
|
+
fastify.delete(
|
|
143
|
+
"/api/model-proxy/api-keys/:id",
|
|
144
|
+
{ preHandler: networkGuard },
|
|
145
|
+
async (request, reply) => {
|
|
146
|
+
const { id } = request.params as { id: string };
|
|
147
|
+
const config = getModelProxyConfig();
|
|
148
|
+
const entry = config.apiKeys.find((k) => k.id === id);
|
|
149
|
+
|
|
150
|
+
if (!entry) {
|
|
151
|
+
return reply.code(404).send({ success: false, error: "Key not found" });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const userEmail = getUserEmail(request);
|
|
155
|
+
const adminEmail = getAdminEmail?.();
|
|
156
|
+
const isAdmin = adminEmail != null && userEmail === adminEmail;
|
|
157
|
+
|
|
158
|
+
if (!isAdmin && entry.createdBy && entry.createdBy !== userEmail) {
|
|
159
|
+
return reply.code(403).send({ success: false, error: "Not authorized to delete this key" });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const filtered = config.apiKeys.filter((k) => k.id !== id);
|
|
163
|
+
await writeModelProxyApiKeys(filtered);
|
|
164
|
+
|
|
165
|
+
return reply.code(204).send();
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/model-proxy/refresh — force-refresh the model proxy registry.
|
|
3
|
+
*
|
|
4
|
+
* JWT-gated. Returns 200 on success, 503 if registry cannot be resolved.
|
|
5
|
+
* See change: add-dashboard-model-proxy, task 2.9.
|
|
6
|
+
*/
|
|
7
|
+
import type { FastifyInstance } from "fastify";
|
|
8
|
+
import { refreshModelRegistry, getModelRegistry } from "../model-proxy/registry-singleton.js";
|
|
9
|
+
|
|
10
|
+
export function registerModelProxyRefreshRoutes(fastify: FastifyInstance): void {
|
|
11
|
+
fastify.post("/api/model-proxy/refresh", async (_request, reply) => {
|
|
12
|
+
try {
|
|
13
|
+
// Ensure registry is initialized first, then refresh
|
|
14
|
+
await getModelRegistry();
|
|
15
|
+
await refreshModelRegistry();
|
|
16
|
+
return { ok: true };
|
|
17
|
+
} catch (err: any) {
|
|
18
|
+
return reply.code(503).send({
|
|
19
|
+
code: "MODEL_PROXY_RUNTIME_MISSING",
|
|
20
|
+
message: err.message || "Failed to refresh model proxy registry",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|