@oscharko-dev/keiko 0.1.0-beta.0 → 0.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -570
- package/dist/cli/gen-tests.js +8 -3
- package/dist/cli/index.js +0 -0
- package/dist/cli/init.d.ts +8 -0
- package/dist/cli/init.js +122 -0
- package/dist/cli/investigate.js +6 -2
- package/dist/cli/lifecycle.d.ts +18 -0
- package/dist/cli/lifecycle.js +289 -0
- package/dist/cli/models.js +2 -2
- package/dist/cli/runner.js +21 -28
- package/dist/gateway/capabilities.d.ts +1 -0
- package/dist/gateway/capabilities.data.js +5 -203
- package/dist/gateway/capabilities.js +18 -0
- package/dist/gateway/config.d.ts +2 -1
- package/dist/gateway/config.js +98 -9
- package/dist/gateway/gateway.js +3 -3
- package/dist/gateway/index.d.ts +2 -2
- package/dist/gateway/index.js +2 -2
- package/dist/gateway/model-selection.d.ts +3 -1
- package/dist/gateway/model-selection.js +15 -4
- package/dist/gateway/types.d.ts +1 -0
- package/dist/harness/session.d.ts +1 -1
- package/dist/harness/session.js +1 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.js +1 -1
- package/dist/tools/patch-normalize.js +1 -2
- package/dist/tools/terminal-policy.js +1 -8
- package/dist/ui/chat-handlers.js +26 -12
- package/dist/ui/csp-hashes.json +6 -6
- package/dist/ui/deps.d.ts +14 -0
- package/dist/ui/deps.js +92 -20
- package/dist/ui/gateway-setup.d.ts +3 -0
- package/dist/ui/gateway-setup.js +235 -0
- package/dist/ui/read-handlers.js +14 -7
- package/dist/ui/routes.js +6 -4
- package/dist/ui/run-handlers.js +3 -2
- package/dist/ui/server.d.ts +1 -1
- package/dist/ui/server.js +1 -1
- package/dist/ui/static/404.html +1 -1
- package/dist/ui/static/_next/static/chunks/44-17c259c8e72fb82f.js +1 -0
- package/dist/ui/static/_next/static/chunks/app/_not-found/{page-75825b09bcecad97.js → page-7bd871301b874ae0.js} +1 -1
- package/dist/ui/static/_next/static/chunks/app/launch/{page-9c86a13c29884245.js → page-3bd098d60d6df513.js} +1 -1
- package/dist/ui/static/_next/static/chunks/app/layout-091bb8be985f5c03.js +1 -0
- package/dist/ui/static/_next/static/chunks/app/{page-4168c12c68b7a853.js → page-2006f21df58c2bb9.js} +1 -1
- package/dist/ui/static/_next/static/chunks/{main-app-30679af7240d63e9.js → main-app-e8144a306630b76d.js} +1 -1
- package/dist/ui/static/_next/static/css/{be7cb54d5c5673b6.css → 3d68155c8db012f4.css} +1 -1
- package/dist/ui/static/index.html +1 -1
- package/dist/ui/static/index.txt +3 -3
- package/dist/ui/static/launch.html +1 -1
- package/dist/ui/static/launch.txt +3 -3
- package/dist/ui/store-handlers.js +16 -12
- package/dist/workflows/bug-investigation/model-loop.js +1 -4
- package/dist/workflows/bug-investigation/parse.js +5 -3
- package/dist/workflows/unit-tests/model-loop.js +1 -1
- package/dist/workspace/retrieval.js +1 -1
- package/package.json +4 -3
- package/dist/ui/static/_next/static/chunks/4-be1fef693af8e088.js +0 -1
- package/dist/ui/static/_next/static/chunks/app/layout-bdea63fe87947d50.js +0 -1
- /package/dist/ui/static/_next/static/{ca-A01hy9W98aRvMZKdAw → f456ZUOjzfLnTnTyaLylj}/_buildManifest.js +0 -0
- /package/dist/ui/static/_next/static/{ca-A01hy9W98aRvMZKdAw → f456ZUOjzfLnTnTyaLylj}/_ssgManifest.js +0 -0
package/dist/ui/chat-handlers.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// behind the existing ModelPort/Gateway boundary: the browser sends only chat content and a registry
|
|
3
3
|
// model id, while provider endpoints and keys remain resolved from the local gateway config/.env.
|
|
4
4
|
import { basename } from "node:path";
|
|
5
|
-
import { GatewayError, findCapability } from "../gateway/index.js";
|
|
5
|
+
import { GatewayError, findCapability, findConfiguredCapability, listConfiguredCapabilities, } from "../gateway/index.js";
|
|
6
6
|
import { UiStoreError, isProjectAvailable, } from "./store/index.js";
|
|
7
7
|
import { validateProjectPath } from "./store/validation.js";
|
|
8
|
+
import { currentGatewayConfig } from "./deps.js";
|
|
8
9
|
import { errorBody } from "./routes.js";
|
|
9
|
-
const DEFAULT_CHAT_MODEL = "
|
|
10
|
-
const DEFAULT_CHAT_TITLE = "
|
|
10
|
+
const DEFAULT_CHAT_MODEL = "example-chat-model";
|
|
11
|
+
const DEFAULT_CHAT_TITLE = "New chat";
|
|
11
12
|
const MAX_BODY_BYTES = 128_000;
|
|
12
13
|
const MAX_CHAT_INPUT_CHARS = 16_000;
|
|
13
14
|
const MAX_CONTEXT_MESSAGES = 24;
|
|
@@ -75,13 +76,26 @@ async function readJsonObject(req) {
|
|
|
75
76
|
function isRouteResult(value) {
|
|
76
77
|
return isRecord(value) && typeof value.status === "number" && "body" in value;
|
|
77
78
|
}
|
|
78
|
-
function
|
|
79
|
+
function chatCapability(deps, modelId) {
|
|
80
|
+
const config = currentGatewayConfig(deps);
|
|
81
|
+
return config === undefined ? findCapability(modelId) : findConfiguredCapability(config, modelId);
|
|
82
|
+
}
|
|
83
|
+
function defaultChatModelId(deps) {
|
|
84
|
+
const config = currentGatewayConfig(deps);
|
|
85
|
+
if (config === undefined) {
|
|
86
|
+
return DEFAULT_CHAT_MODEL;
|
|
87
|
+
}
|
|
88
|
+
const configured = listConfiguredCapabilities(config);
|
|
89
|
+
return (configured.find((model) => model.id === DEFAULT_CHAT_MODEL && model.kind === "chat") ??
|
|
90
|
+
configured.find((model) => model.kind === "chat"))?.id ?? DEFAULT_CHAT_MODEL;
|
|
91
|
+
}
|
|
92
|
+
function modelFromBody(body, deps) {
|
|
79
93
|
const modelId = typeof body.modelId === "string" && body.modelId.length > 0
|
|
80
94
|
? body.modelId
|
|
81
|
-
:
|
|
82
|
-
const capability =
|
|
95
|
+
: defaultChatModelId(deps);
|
|
96
|
+
const capability = chatCapability(deps, modelId);
|
|
83
97
|
if (capability?.kind !== "chat") {
|
|
84
|
-
return { status: 400, body: errorBody("BAD_REQUEST", "modelId must be a chat model
|
|
98
|
+
return { status: 400, body: errorBody("BAD_REQUEST", "modelId must be a configured chat model id.") };
|
|
85
99
|
}
|
|
86
100
|
return modelId;
|
|
87
101
|
}
|
|
@@ -176,12 +190,12 @@ function sendRequestFromBody(body) {
|
|
|
176
190
|
modelId: typeof body.modelId === "string" && body.modelId.length > 0 ? body.modelId : undefined,
|
|
177
191
|
};
|
|
178
192
|
}
|
|
179
|
-
function invalidChatModelResult(modelId) {
|
|
180
|
-
const capability =
|
|
193
|
+
function invalidChatModelResult(modelId, deps) {
|
|
194
|
+
const capability = chatCapability(deps, modelId);
|
|
181
195
|
if (capability?.kind === "chat") {
|
|
182
196
|
return undefined;
|
|
183
197
|
}
|
|
184
|
-
return { status: 400, body: errorBody("BAD_REQUEST", "modelId must be a chat model
|
|
198
|
+
return { status: 400, body: errorBody("BAD_REQUEST", "modelId must be a configured chat model id.") };
|
|
185
199
|
}
|
|
186
200
|
function createUserMessage(deps, request) {
|
|
187
201
|
return deps.store.createMessage({
|
|
@@ -242,7 +256,7 @@ export async function handleCreateDesktopChat(ctx, deps) {
|
|
|
242
256
|
const body = await readJsonObject(ctx.req);
|
|
243
257
|
if (isRouteResult(body))
|
|
244
258
|
return body;
|
|
245
|
-
const modelId = modelFromBody(body);
|
|
259
|
+
const modelId = modelFromBody(body, deps);
|
|
246
260
|
if (isRouteResult(modelId))
|
|
247
261
|
return modelId;
|
|
248
262
|
try {
|
|
@@ -274,7 +288,7 @@ export async function handleSendDesktopChat(ctx, deps) {
|
|
|
274
288
|
return { status: 404, body: errorBody("NOT_FOUND", "Chat not found.") };
|
|
275
289
|
}
|
|
276
290
|
const modelId = request.modelId ?? chat.selectedModel;
|
|
277
|
-
const invalidModel = invalidChatModelResult(modelId);
|
|
291
|
+
const invalidModel = invalidChatModelResult(modelId, deps);
|
|
278
292
|
if (invalidModel !== undefined)
|
|
279
293
|
return invalidModel;
|
|
280
294
|
return persistModelChatTurn(deps, request, chat, modelId);
|
package/dist/ui/csp-hashes.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
[
|
|
2
|
+
"'sha256-2Flzo4ItPYV57isCh/jl3+kdIDk82tJZynJPKUh7Pw8='",
|
|
3
|
+
"'sha256-7mSPGegJYFJGVEtaRwBRsbc8ZbNmoSO6U/jFhIwVyUo='",
|
|
2
4
|
"'sha256-FhLHRUQz4c4ntLU9VkfEesX7PnzNLENSe/16Hi523Kk='",
|
|
5
|
+
"'sha256-JvBgydXt9/FYJ748ftCVGdeG0nrCETPaVqZP4+fgvkQ='",
|
|
3
6
|
"'sha256-NMmsYxPlvKu6BMNDUuiUA/0HWXXhODWSkUJ3CrerHAI='",
|
|
4
7
|
"'sha256-OBTN3RiyCV4Bq7dFqZ5a2pAXjnCcCYeTJMO2I/LYKeo='",
|
|
5
|
-
"'sha256-OogwkdfeAY//FSbFGNeTewmi/U11IUHAkAgKNEwJCG0='",
|
|
6
8
|
"'sha256-U9W+ZoRW19rf6ohEfUh2oSN8UmJ8mZjCoxp31AbEGYM='",
|
|
7
|
-
"'sha256-
|
|
9
|
+
"'sha256-Xhv62fM7dwD82udSCiqrICSFGK4rIE9t+2k2y0fsEwg='",
|
|
8
10
|
"'sha256-bg+CWjI8RppcgHYH6RuW4z4OnLAUEUPDXRoYUo9Tyok='",
|
|
9
|
-
"'sha256-
|
|
10
|
-
"'sha256-
|
|
11
|
-
"'sha256-pQjgKfZ7Pcb9s5HrqqpJnRrMazbJ0Nhk6e1aFQNuFsU='",
|
|
12
|
-
"'sha256-q3VO3K+1hbob0r8DheOST7SIt4DQWr76alv4VzyZ44s='",
|
|
11
|
+
"'sha256-dkXQrDpCKNXpywUKLu04aGkL4/5JXS2LWCKQ806R0rU='",
|
|
12
|
+
"'sha256-o5xPG0ZoS77FlzyLEClBD+u6el5Y3HrgzBsy+JPyHx8='",
|
|
13
13
|
"'sha256-qBQ7RdQKJEJuW7Fj1MbGjDbF6lnRdfu+KV0V4A5MTRg='",
|
|
14
14
|
"'sha256-qjuzziE6xLU3Cras89VlShlRYHgYZuOxceXUDmuvClo='",
|
|
15
15
|
"'sha256-xLP5QIbvR88RAxDKoSWqs6CVxNIRu17hhr7S/Q6hlU0='",
|
package/dist/ui/deps.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ import { type TerminalExecutionManager } from "./terminal.js";
|
|
|
7
7
|
import { type BrowserSessionManager } from "../tools/browser/index.js";
|
|
8
8
|
export type Redactor = (value: unknown) => unknown;
|
|
9
9
|
export type ModelPortFactory = (modelId: string) => ModelPort | undefined;
|
|
10
|
+
export interface RuntimeGatewayConfig {
|
|
11
|
+
readonly storagePath: string;
|
|
12
|
+
current(): GatewayConfig | undefined;
|
|
13
|
+
present(): boolean;
|
|
14
|
+
set(config: GatewayConfig | undefined, present: boolean): void;
|
|
15
|
+
}
|
|
10
16
|
export interface UiHandlerDeps {
|
|
11
17
|
readonly config: GatewayConfig | undefined;
|
|
12
18
|
readonly configPresent: boolean;
|
|
@@ -20,6 +26,9 @@ export interface UiHandlerDeps {
|
|
|
20
26
|
readonly uiDbPath?: string | undefined;
|
|
21
27
|
readonly terminal?: TerminalExecutionManager | undefined;
|
|
22
28
|
readonly browser?: BrowserSessionManager | undefined;
|
|
29
|
+
readonly gatewayConfig?: RuntimeGatewayConfig | undefined;
|
|
30
|
+
readonly gatewaySetupTester?: ((config: GatewayConfig, candidateModelIds: readonly string[]) => Promise<readonly string[]>) | undefined;
|
|
31
|
+
readonly gatewayModelDiscovery?: ((baseUrl: string, apiKey: string) => Promise<readonly string[]>) | undefined;
|
|
23
32
|
}
|
|
24
33
|
export interface BuildHandlerDepsOptions {
|
|
25
34
|
readonly configPath: string | undefined;
|
|
@@ -29,6 +38,11 @@ export interface BuildHandlerDepsOptions {
|
|
|
29
38
|
readonly modelPortFactory?: ModelPortFactory | undefined;
|
|
30
39
|
readonly uiDbPath?: string | undefined;
|
|
31
40
|
readonly store?: UiStore | undefined;
|
|
41
|
+
readonly gatewaySetupTester?: ((config: GatewayConfig, candidateModelIds: readonly string[]) => Promise<readonly string[]>) | undefined;
|
|
42
|
+
readonly gatewayModelDiscovery?: ((baseUrl: string, apiKey: string) => Promise<readonly string[]>) | undefined;
|
|
32
43
|
}
|
|
44
|
+
export declare function currentGatewayConfig(deps: UiHandlerDeps): GatewayConfig | undefined;
|
|
45
|
+
export declare function currentGatewayConfigPresent(deps: UiHandlerDeps): boolean;
|
|
33
46
|
export declare function buildRedactor(env: EnvSource, config?: GatewayConfig): Redactor;
|
|
47
|
+
export declare function currentRedactionSecrets(deps: UiHandlerDeps): readonly string[];
|
|
34
48
|
export declare function buildUiHandlerDeps(options: BuildHandlerDepsOptions): UiHandlerDeps;
|
package/dist/ui/deps.js
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
// 3-arg `createUiServer({ staticRoot, csp, port })` form still compiles and the Wave 1 server tests
|
|
6
6
|
// pass unchanged; the handlers degrade gracefully (no config → 400 NO_MODEL on a run, null config on
|
|
7
7
|
// the inspector; no store → an empty evidence list).
|
|
8
|
-
import {
|
|
8
|
+
import { createDefaultChatCapability, loadConfigFromFile, parseGatewayConfig, } from "../gateway/index.js";
|
|
9
9
|
import { GatewayError, Gateway } from "../gateway/index.js";
|
|
10
10
|
import { GatewayModelPort } from "../harness/index.js";
|
|
11
11
|
import { createAuditRedactor } from "../audit/index.js";
|
|
12
12
|
import { deepRedactStrings } from "../audit/redaction.js";
|
|
13
13
|
import { createNodeEvidenceStore, resolveEvidenceDir } from "../audit/store.js";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
14
15
|
import { createRunRegistry } from "./runs.js";
|
|
15
16
|
import { createNodeUiStore, resolveUiDbPath, } from "./store/index.js";
|
|
16
17
|
import { createTerminalExecutionManager, } from "./terminal.js";
|
|
@@ -18,16 +19,38 @@ import { createBrowserSessionManager, } from "../tools/browser/index.js";
|
|
|
18
19
|
function envModelToken(modelId) {
|
|
19
20
|
return modelId.replace(/[^A-Za-z0-9]/g, "_").toUpperCase();
|
|
20
21
|
}
|
|
22
|
+
function envModelIdFromApiKeyName(name) {
|
|
23
|
+
const prefix = "KEIKO_MODEL_";
|
|
24
|
+
const suffix = "_API_KEY";
|
|
25
|
+
if (!name.startsWith(prefix) || !name.endsWith(suffix)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const token = name.slice(prefix.length, -suffix.length);
|
|
29
|
+
return token.length === 0 ? undefined : token.toLowerCase().replace(/_/g, "-");
|
|
30
|
+
}
|
|
21
31
|
function hasEnvProvider(modelId, env) {
|
|
22
32
|
const token = envModelToken(modelId);
|
|
23
33
|
const baseUrl = env[`KEIKO_MODEL_${token}_BASE_URL`];
|
|
24
34
|
const apiKey = env[`KEIKO_MODEL_${token}_API_KEY`];
|
|
25
35
|
return baseUrl !== undefined && baseUrl.length > 0 && apiKey !== undefined && apiKey.length > 0;
|
|
26
36
|
}
|
|
37
|
+
function envModelIds(env) {
|
|
38
|
+
const modelIds = [];
|
|
39
|
+
for (const key of Object.keys(env)) {
|
|
40
|
+
const modelId = envModelIdFromApiKeyName(key);
|
|
41
|
+
if (modelId !== undefined && hasEnvProvider(modelId, env)) {
|
|
42
|
+
modelIds.push(modelId);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return Array.from(new Set(modelIds));
|
|
46
|
+
}
|
|
27
47
|
function resolveEnvOnlyConfig(env) {
|
|
28
|
-
const providers =
|
|
29
|
-
|
|
30
|
-
|
|
48
|
+
const providers = envModelIds(env).map((modelId) => ({
|
|
49
|
+
modelId,
|
|
50
|
+
baseUrl: "",
|
|
51
|
+
apiKey: "",
|
|
52
|
+
capability: createDefaultChatCapability(modelId),
|
|
53
|
+
}));
|
|
31
54
|
if (providers.length === 0) {
|
|
32
55
|
return undefined;
|
|
33
56
|
}
|
|
@@ -41,11 +64,25 @@ function resolveEnvOnlyConfig(env) {
|
|
|
41
64
|
throw error;
|
|
42
65
|
}
|
|
43
66
|
}
|
|
67
|
+
function localGatewayConfigPath(uiDbPath) {
|
|
68
|
+
return join(dirname(uiDbPath), "keiko.config.json");
|
|
69
|
+
}
|
|
44
70
|
// Loads the config without leaking the path or any secret on failure: a missing/invalid config file
|
|
45
71
|
// falls back to KEIKO_MODEL_* env wiring when present, otherwise it is a normal "no config" state.
|
|
46
|
-
function resolveConfig(configPath, env) {
|
|
72
|
+
function resolveConfig(configPath, env, localConfigPath) {
|
|
47
73
|
if (configPath === undefined) {
|
|
48
|
-
|
|
74
|
+
let config;
|
|
75
|
+
try {
|
|
76
|
+
config = loadConfigFromFile(localConfigPath, env);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error instanceof GatewayError) {
|
|
80
|
+
config = resolveEnvOnlyConfig(env);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
49
86
|
return { config, configPresent: config !== undefined };
|
|
50
87
|
}
|
|
51
88
|
try {
|
|
@@ -59,6 +96,25 @@ function resolveConfig(configPath, env) {
|
|
|
59
96
|
throw error;
|
|
60
97
|
}
|
|
61
98
|
}
|
|
99
|
+
function createRuntimeGatewayConfig(initial, initialPresent, storagePath) {
|
|
100
|
+
let config = initial;
|
|
101
|
+
let present = initialPresent;
|
|
102
|
+
return {
|
|
103
|
+
storagePath,
|
|
104
|
+
current: () => config,
|
|
105
|
+
present: () => present,
|
|
106
|
+
set(next, nextPresent) {
|
|
107
|
+
config = next;
|
|
108
|
+
present = nextPresent;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function currentGatewayConfig(deps) {
|
|
113
|
+
return deps.gatewayConfig?.current() ?? deps.config;
|
|
114
|
+
}
|
|
115
|
+
export function currentGatewayConfigPresent(deps) {
|
|
116
|
+
return deps.gatewayConfig?.present() ?? deps.configPresent;
|
|
117
|
+
}
|
|
62
118
|
function isKeikoApiKeyEnvName(name) {
|
|
63
119
|
return (name === "KEIKO_DEFAULT_API_KEY" ||
|
|
64
120
|
(name.startsWith("KEIKO_MODEL_") && name.endsWith("_API_KEY")));
|
|
@@ -85,28 +141,44 @@ export function buildRedactor(env, config) {
|
|
|
85
141
|
const redactString = createAuditRedactor({ additionalSecrets: redactionSecrets(env, config) }, env);
|
|
86
142
|
return (value) => deepRedactStrings(value, redactString);
|
|
87
143
|
}
|
|
144
|
+
export function currentRedactionSecrets(deps) {
|
|
145
|
+
return redactionSecrets(deps.env, currentGatewayConfig(deps));
|
|
146
|
+
}
|
|
88
147
|
// The production ModelPort factory: a GatewayModelPort over a Gateway built from the resolved
|
|
89
148
|
// config (mirrors the CLI's `new GatewayModelPort(new Gateway(config))`). Returns undefined when no
|
|
90
149
|
// config was resolved so the run route answers 400 NO_MODEL rather than constructing a broken port.
|
|
91
|
-
function defaultModelPortFactory(
|
|
150
|
+
function defaultModelPortFactory(runtimeConfig) {
|
|
92
151
|
return () => {
|
|
152
|
+
const config = runtimeConfig.current();
|
|
93
153
|
if (config === undefined) {
|
|
94
154
|
return undefined;
|
|
95
155
|
}
|
|
96
156
|
return new GatewayModelPort(new Gateway(config));
|
|
97
157
|
};
|
|
98
158
|
}
|
|
159
|
+
function buildTerminalManager(options) {
|
|
160
|
+
return createTerminalExecutionManager({
|
|
161
|
+
store: options.store,
|
|
162
|
+
evidenceStore: options.evidenceStore,
|
|
163
|
+
processEnv: options.env,
|
|
164
|
+
redactor: (value) => {
|
|
165
|
+
const redacted = options.liveRedactor(value);
|
|
166
|
+
return typeof redacted === "string" ? redacted : value;
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
99
170
|
// Assembles the handler deps for the real `keiko ui` process, mirroring the CLI config/evidence
|
|
100
171
|
// wiring (loadConfigFromFile / resolveEvidenceDir / createNodeEvidenceStore). The UI store is
|
|
101
172
|
// created at the resolved UI-DB path (explicit → KEIKO_UI_DATA_DIR → ~/.keiko/keiko-ui.db) unless
|
|
102
173
|
// an injected store is supplied (tests).
|
|
103
174
|
export function buildUiHandlerDeps(options) {
|
|
104
|
-
const { config, configPresent } = resolveConfig(options.configPath, options.env);
|
|
105
|
-
const evidenceStore = createNodeEvidenceStore(resolveEvidenceDir(options.evidenceDir, options.env));
|
|
106
|
-
const secrets = redactionSecrets(options.env, config);
|
|
107
|
-
const redactString = createAuditRedactor({ additionalSecrets: secrets }, options.env);
|
|
108
|
-
const liveRedactor = buildRedactor(options.env, config);
|
|
109
175
|
const resolvedUiDbPath = resolveUiDbPath(options.uiDbPath, options.env);
|
|
176
|
+
const runtimeConfigPath = localGatewayConfigPath(resolvedUiDbPath);
|
|
177
|
+
const { config, configPresent } = resolveConfig(options.configPath, options.env, runtimeConfigPath);
|
|
178
|
+
const runtimeConfig = createRuntimeGatewayConfig(config, configPresent, runtimeConfigPath);
|
|
179
|
+
const evidenceStore = createNodeEvidenceStore(resolveEvidenceDir(options.evidenceDir, options.env));
|
|
180
|
+
const redactString = (value) => createAuditRedactor({ additionalSecrets: redactionSecrets(options.env, runtimeConfig.current()) }, options.env)(value);
|
|
181
|
+
const liveRedactor = (value) => deepRedactStrings(value, redactString);
|
|
110
182
|
const uiStore = options.store ?? createNodeUiStore(resolvedUiDbPath, { redactString });
|
|
111
183
|
return {
|
|
112
184
|
config,
|
|
@@ -115,18 +187,18 @@ export function buildUiHandlerDeps(options) {
|
|
|
115
187
|
env: options.env,
|
|
116
188
|
redactor: liveRedactor,
|
|
117
189
|
registry: options.registry ?? createRunRegistry(),
|
|
118
|
-
modelPortFactory: options.modelPortFactory ?? defaultModelPortFactory(
|
|
119
|
-
redactionSecrets:
|
|
190
|
+
modelPortFactory: options.modelPortFactory ?? defaultModelPortFactory(runtimeConfig),
|
|
191
|
+
redactionSecrets: redactionSecrets(options.env, runtimeConfig.current()),
|
|
120
192
|
store: uiStore,
|
|
121
193
|
uiDbPath: resolvedUiDbPath,
|
|
122
|
-
|
|
194
|
+
gatewayConfig: runtimeConfig,
|
|
195
|
+
gatewaySetupTester: options.gatewaySetupTester,
|
|
196
|
+
gatewayModelDiscovery: options.gatewayModelDiscovery,
|
|
197
|
+
terminal: buildTerminalManager({
|
|
123
198
|
store: uiStore,
|
|
124
199
|
evidenceStore,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const redacted = liveRedactor(value);
|
|
128
|
-
return typeof redacted === "string" ? redacted : value;
|
|
129
|
-
},
|
|
200
|
+
env: options.env,
|
|
201
|
+
liveRedactor,
|
|
130
202
|
}),
|
|
131
203
|
browser: createBrowserSessionManager({
|
|
132
204
|
evidenceDir: resolveEvidenceDir(options.evidenceDir, options.env),
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// First-run gateway setup for non-technical UI users. The browser provides only a base URL and API
|
|
2
|
+
// token; the loopback BFF builds the local provider config, performs a real chat-completions smoke
|
|
3
|
+
// call, stores the resulting config on disk with private permissions, and updates the in-memory
|
|
4
|
+
// runtime config without exposing credentials back to the browser.
|
|
5
|
+
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
import { Gateway, createDefaultChatCapability, listConfiguredCapabilities, parseGatewayConfig, toSafeObject, } from "../gateway/index.js";
|
|
8
|
+
import { redact } from "../gateway/redaction.js";
|
|
9
|
+
import { errorBody } from "./routes.js";
|
|
10
|
+
const MAX_BODY_BYTES = 64_000;
|
|
11
|
+
const MAX_DISCOVERED_MODELS = 25;
|
|
12
|
+
class BodyTooLargeError extends Error {
|
|
13
|
+
constructor() {
|
|
14
|
+
super("request body too large");
|
|
15
|
+
this.name = "BodyTooLargeError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function isRecord(value) {
|
|
19
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
function readBody(req) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const chunks = [];
|
|
24
|
+
let total = 0;
|
|
25
|
+
let capped = false;
|
|
26
|
+
req.on("data", (chunk) => {
|
|
27
|
+
total += chunk.length;
|
|
28
|
+
if (total > MAX_BODY_BYTES) {
|
|
29
|
+
if (!capped) {
|
|
30
|
+
capped = true;
|
|
31
|
+
chunks.length = 0;
|
|
32
|
+
reject(new BodyTooLargeError());
|
|
33
|
+
req.resume();
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
chunks.push(chunk);
|
|
38
|
+
});
|
|
39
|
+
req.on("end", () => {
|
|
40
|
+
if (!capped) {
|
|
41
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
req.on("error", reject);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function normalizeBaseUrl(raw) {
|
|
48
|
+
let value = raw.trim().replace(/\/+$/u, "");
|
|
49
|
+
if (value.endsWith("/chat/completions")) {
|
|
50
|
+
value = value.slice(0, -"/chat/completions".length).replace(/\/+$/u, "");
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
function candidateBaseUrls(baseUrl) {
|
|
55
|
+
const primary = normalizeBaseUrl(baseUrl);
|
|
56
|
+
const candidates = [primary];
|
|
57
|
+
if (!primary.endsWith("/v1")) {
|
|
58
|
+
candidates.push(`${primary}/v1`);
|
|
59
|
+
}
|
|
60
|
+
return Array.from(new Set(candidates));
|
|
61
|
+
}
|
|
62
|
+
function providerRaw(modelId, baseUrl, apiKey) {
|
|
63
|
+
return {
|
|
64
|
+
modelId,
|
|
65
|
+
baseUrl,
|
|
66
|
+
apiKey,
|
|
67
|
+
capability: createDefaultChatCapability(modelId),
|
|
68
|
+
timeoutMs: 30_000,
|
|
69
|
+
maxRetries: 2,
|
|
70
|
+
retryBaseDelayMs: 500,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function buildRawConfig(baseUrl, apiKey, modelIds) {
|
|
74
|
+
return {
|
|
75
|
+
providers: modelIds.map((modelId) => providerRaw(modelId, baseUrl, apiKey)),
|
|
76
|
+
circuitBreaker: { failureThreshold: 5, cooldownMs: 30_000, halfOpenProbes: 2 },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function modelsEndpoint(baseUrl) {
|
|
80
|
+
return `${baseUrl}/models`;
|
|
81
|
+
}
|
|
82
|
+
function parseModelList(payload) {
|
|
83
|
+
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
84
|
+
throw new Error("model discovery response must contain a data array");
|
|
85
|
+
}
|
|
86
|
+
const ids = [];
|
|
87
|
+
for (const item of payload.data) {
|
|
88
|
+
if (!isRecord(item) || typeof item.id !== "string" || item.id.trim().length === 0) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
ids.push(item.id.trim());
|
|
92
|
+
}
|
|
93
|
+
const unique = Array.from(new Set(ids));
|
|
94
|
+
if (unique.length === 0) {
|
|
95
|
+
throw new Error("model discovery returned no model ids");
|
|
96
|
+
}
|
|
97
|
+
return unique.slice(0, MAX_DISCOVERED_MODELS);
|
|
98
|
+
}
|
|
99
|
+
async function defaultGatewayModelDiscovery(baseUrl, apiKey) {
|
|
100
|
+
const response = await fetch(modelsEndpoint(baseUrl), {
|
|
101
|
+
method: "GET",
|
|
102
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
103
|
+
signal: AbortSignal.timeout(30_000),
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`model discovery returned HTTP ${String(response.status)}`);
|
|
107
|
+
}
|
|
108
|
+
let payload;
|
|
109
|
+
try {
|
|
110
|
+
payload = await response.json();
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
throw new Error("model discovery response was not readable JSON");
|
|
114
|
+
}
|
|
115
|
+
return parseModelList(payload);
|
|
116
|
+
}
|
|
117
|
+
async function defaultGatewaySetupTester(config, candidateModelIds) {
|
|
118
|
+
const gateway = new Gateway(config);
|
|
119
|
+
const tested = [];
|
|
120
|
+
for (const modelId of candidateModelIds) {
|
|
121
|
+
try {
|
|
122
|
+
await gateway.chat({
|
|
123
|
+
modelId,
|
|
124
|
+
messages: [{ role: "user", content: "Reply with exactly: OK" }],
|
|
125
|
+
});
|
|
126
|
+
tested.push(modelId);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Non-chat models can appear in OpenAI-compatible model discovery responses. They are
|
|
130
|
+
// intentionally ignored so only chat-callable models become selectable in the UI.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (tested.length === 0) {
|
|
134
|
+
throw new Error("no discovered model accepted the chat-completions smoke test");
|
|
135
|
+
}
|
|
136
|
+
return tested;
|
|
137
|
+
}
|
|
138
|
+
function savePrivateJson(path, raw) {
|
|
139
|
+
const dir = dirname(path);
|
|
140
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
141
|
+
if (process.platform !== "win32") {
|
|
142
|
+
chmodSync(dir, 0o700);
|
|
143
|
+
}
|
|
144
|
+
writeFileSync(path, `${JSON.stringify(raw, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
145
|
+
if (process.platform !== "win32") {
|
|
146
|
+
chmodSync(path, 0o600);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function readSetupRequest(raw) {
|
|
150
|
+
if (!isRecord(raw)) {
|
|
151
|
+
return { status: 400, body: errorBody("BAD_REQUEST", "Request body must be a JSON object.") };
|
|
152
|
+
}
|
|
153
|
+
const baseUrl = typeof raw.baseUrl === "string" ? raw.baseUrl.trim() : "";
|
|
154
|
+
const apiKey = typeof raw.apiKey === "string" ? raw.apiKey.trim() : "";
|
|
155
|
+
if (baseUrl.length === 0 || apiKey.length === 0) {
|
|
156
|
+
return { status: 400, body: errorBody("BAD_REQUEST", "baseUrl and apiKey are required.") };
|
|
157
|
+
}
|
|
158
|
+
return { baseUrl, apiKey };
|
|
159
|
+
}
|
|
160
|
+
function safeError(error, secrets) {
|
|
161
|
+
if (error instanceof Error) {
|
|
162
|
+
return redact(error.message, secrets);
|
|
163
|
+
}
|
|
164
|
+
return "Gateway setup failed.";
|
|
165
|
+
}
|
|
166
|
+
async function verifySetupCandidate(baseUrl, apiKey, tester, discovery) {
|
|
167
|
+
const candidateModelIds = await discovery(baseUrl, apiKey);
|
|
168
|
+
const candidateRawConfig = buildRawConfig(baseUrl, apiKey, candidateModelIds);
|
|
169
|
+
const candidateConfig = parseGatewayConfig(candidateRawConfig);
|
|
170
|
+
const testedModelIds = await tester(candidateConfig, candidateModelIds);
|
|
171
|
+
const rawConfig = buildRawConfig(baseUrl, apiKey, testedModelIds);
|
|
172
|
+
const config = parseGatewayConfig(rawConfig);
|
|
173
|
+
return { rawConfig, config, testedModelIds };
|
|
174
|
+
}
|
|
175
|
+
function setupSuccessResult(config, testedModelIds) {
|
|
176
|
+
const testedModelId = testedModelIds[0] ?? "unknown";
|
|
177
|
+
return {
|
|
178
|
+
status: 200,
|
|
179
|
+
body: {
|
|
180
|
+
ok: true,
|
|
181
|
+
testedModelId,
|
|
182
|
+
testedModelIds,
|
|
183
|
+
providerCount: config.providers.length,
|
|
184
|
+
models: listConfiguredCapabilities(config),
|
|
185
|
+
config: toSafeObject(config),
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function setupFailureResult(errors) {
|
|
190
|
+
return {
|
|
191
|
+
status: 502,
|
|
192
|
+
body: errorBody("GATEWAY_SETUP_FAILED", `Credentials could not be verified. ${errors.join(" ")}`),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export async function handleGatewaySetup(ctx, deps) {
|
|
196
|
+
if (deps.gatewayConfig === undefined) {
|
|
197
|
+
return { status: 500, body: errorBody("GATEWAY_SETUP_UNAVAILABLE", "Gateway setup is unavailable.") };
|
|
198
|
+
}
|
|
199
|
+
let bodyText;
|
|
200
|
+
try {
|
|
201
|
+
bodyText = await readBody(ctx.req);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (error instanceof BodyTooLargeError) {
|
|
205
|
+
return { status: 413, body: errorBody("PAYLOAD_TOO_LARGE", "Request body exceeds the size limit.") };
|
|
206
|
+
}
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
let parsed;
|
|
210
|
+
try {
|
|
211
|
+
parsed = JSON.parse(bodyText);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return { status: 400, body: errorBody("BAD_REQUEST", "Request body is not valid JSON.") };
|
|
215
|
+
}
|
|
216
|
+
const request = readSetupRequest(parsed);
|
|
217
|
+
if ("status" in request) {
|
|
218
|
+
return request;
|
|
219
|
+
}
|
|
220
|
+
const tester = deps.gatewaySetupTester ?? defaultGatewaySetupTester;
|
|
221
|
+
const discovery = deps.gatewayModelDiscovery ?? defaultGatewayModelDiscovery;
|
|
222
|
+
const errors = [];
|
|
223
|
+
for (const baseUrl of candidateBaseUrls(request.baseUrl)) {
|
|
224
|
+
try {
|
|
225
|
+
const verified = await verifySetupCandidate(baseUrl, request.apiKey, tester, discovery);
|
|
226
|
+
savePrivateJson(deps.gatewayConfig.storagePath, verified.rawConfig);
|
|
227
|
+
deps.gatewayConfig.set(verified.config, true);
|
|
228
|
+
return setupSuccessResult(verified.config, verified.testedModelIds);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
errors.push(`candidate ${String(errors.length + 1)}: ${safeError(error, [request.apiKey, baseUrl])}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return setupFailureResult(errors);
|
|
235
|
+
}
|
package/dist/ui/read-handlers.js
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
// The six read-only BFF endpoints (ADR-0011 D5 routes 2,3,4,10,11,12). Each returns a redacted JSON
|
|
2
|
-
// projection of already-safe data: config via `toSafeObject` (strips apiKey),
|
|
3
|
-
//
|
|
2
|
+
// projection of already-safe data: config via `toSafeObject` (strips apiKey), configured model
|
|
3
|
+
// capabilities, the workflow launch-form descriptors, the workspace summary built from the workspace
|
|
4
4
|
// layer, and evidence list/detail served straight from the store (manifests are redacted-by-
|
|
5
5
|
// construction on disk, served as-is per D9). No secret reaches any response; the config route
|
|
6
6
|
// never leaks the config path even on a load failure (handled upstream in deps.ts, which yields
|
|
7
7
|
// `config: undefined` rather than throwing).
|
|
8
|
-
import { toSafeObject,
|
|
8
|
+
import { toSafeObject, listConfiguredCapabilities } from "../gateway/index.js";
|
|
9
9
|
import { UNIT_TEST_WORKFLOW_DESCRIPTOR, BUG_INVESTIGATION_WORKFLOW_DESCRIPTOR, } from "../workflows/index.js";
|
|
10
10
|
import { DEFAULT_LIMITS } from "../harness/index.js";
|
|
11
11
|
import { listEvidence, loadEvidence, assertValidRunId, EvidenceReadError, EvidenceSchemaError, } from "../audit/index.js";
|
|
12
12
|
import { buildContextPackFromFiles, buildWorkspaceSummary, DEFAULT_CONTEXT_REQUEST, detectWorkspace, discoverWithStats, WORKSPACE_CODES, WorkspaceError, } from "../workspace/index.js";
|
|
13
13
|
import { errorBody } from "./routes.js";
|
|
14
|
+
import { currentGatewayConfig, currentGatewayConfigPresent } from "./deps.js";
|
|
14
15
|
import { validateProjectPath } from "./store/validation.js";
|
|
15
16
|
// Route 2 — resolved config (SafeGatewayConfig, never apiKey/baseUrl) or null when no config was resolved.
|
|
16
17
|
export function handleConfig(_ctx, deps) {
|
|
17
|
-
const config =
|
|
18
|
-
return {
|
|
18
|
+
const config = currentGatewayConfig(deps);
|
|
19
|
+
return {
|
|
20
|
+
status: 200,
|
|
21
|
+
body: {
|
|
22
|
+
config: config === undefined ? null : toSafeObject(config),
|
|
23
|
+
configPresent: currentGatewayConfigPresent(deps),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
19
26
|
}
|
|
20
27
|
// Route 3 — models published by the resolved UI gateway config. If no config is resolved, no
|
|
21
28
|
// model-backed run can start, so the endpoint returns an empty list.
|
|
22
29
|
export function handleModels(_ctx, deps) {
|
|
23
|
-
const
|
|
24
|
-
const models =
|
|
30
|
+
const config = currentGatewayConfig(deps);
|
|
31
|
+
const models = config === undefined ? [] : listConfiguredCapabilities(config);
|
|
25
32
|
return { status: 200, body: { models } };
|
|
26
33
|
}
|
|
27
34
|
// Route 4 — launch-form metadata: the workflow descriptors plus the synthesized explain-plan and
|
package/dist/ui/routes.js
CHANGED
|
@@ -9,6 +9,7 @@ import { handleConfig, handleModels, handleWorkflows, handleWorkspace, handleEvi
|
|
|
9
9
|
import { handleCreateRun, handleCreateChatRun, handleRunEvents, handleCancelRun, handleGetRun, handleApplyRun, } from "./run-handlers.js";
|
|
10
10
|
import { handleListProjects, handleCreateProject, handleUpdateProject, handleDeleteProject, handleListChats, handleCreateChat, handleUpdateChat, handleDeleteChat, handleListMessages, handleCreateMessage, handleCreateRunSummaryPair, handleUpdateMessage, } from "./store-handlers.js";
|
|
11
11
|
import { handleCreateDesktopChat, handleSendDesktopChat } from "./chat-handlers.js";
|
|
12
|
+
import { handleGatewaySetup } from "./gateway-setup.js";
|
|
12
13
|
import { handleCreateTerminalExecution, handleDeleteTerminalExecution, handleTerminalDirectories, handleTerminalEvents, handleTerminalPolicy, } from "./terminal-routes.js";
|
|
13
14
|
import { handleFilesDirectories, handleFilesPreview, handleFilesTree, } from "./files.js";
|
|
14
15
|
import { handleBrowserApplyScreenshot, handleBrowserContent, handleBrowserEvents, handleBrowserNavigate, handleBrowserScreenshot, handleBrowserStatus, handleCreateBrowserSession, handleDeleteBrowserSession, } from "./browser.js";
|
|
@@ -16,14 +17,15 @@ export const STREAMING = Symbol("streaming");
|
|
|
16
17
|
function health() {
|
|
17
18
|
return { status: 200, body: { status: "ok", version: SDK_VERSION } };
|
|
18
19
|
}
|
|
19
|
-
// The full route contract: the twelve original (ADR-0011 D5), the
|
|
20
|
-
// routes (ADR-0013 D7), three Issue #66 run-summary routes,
|
|
21
|
-
// desktop terminal JSON routes, and read-only Files widget routes.
|
|
22
|
-
// uses a token-scoped WebSocket upgrade path.
|
|
20
|
+
// The full route contract: the twelve original (ADR-0011 D5), the first-run gateway setup
|
|
21
|
+
// endpoint, the 10 additive UI-store routes (ADR-0013 D7), three Issue #66 run-summary routes,
|
|
22
|
+
// two desktop chat routes, desktop terminal JSON routes, and read-only Files widget routes.
|
|
23
|
+
// Terminal byte I/O uses a token-scoped WebSocket upgrade path.
|
|
23
24
|
export const API_ROUTES = [
|
|
24
25
|
{ method: "GET", pattern: "/api/health", handler: health },
|
|
25
26
|
{ method: "GET", pattern: "/api/config", handler: handleConfig },
|
|
26
27
|
{ method: "GET", pattern: "/api/models", handler: handleModels },
|
|
28
|
+
{ method: "POST", pattern: "/api/gateway/setup", handler: handleGatewaySetup },
|
|
27
29
|
{ method: "GET", pattern: "/api/workflows", handler: handleWorkflows },
|
|
28
30
|
{ method: "POST", pattern: "/api/runs", handler: handleCreateRun },
|
|
29
31
|
{ method: "GET", pattern: "/api/runs/:runId/events", handler: handleRunEvents },
|
package/dist/ui/run-handlers.js
CHANGED
|
@@ -11,6 +11,7 @@ import { startRun, applyRun } from "./run-engine.js";
|
|
|
11
11
|
import { ActiveRunLimitError } from "./runs.js";
|
|
12
12
|
import { SSE_HEADERS, writeEvent, readyMessage } from "./sse.js";
|
|
13
13
|
import { errorBody, STREAMING } from "./routes.js";
|
|
14
|
+
import { currentRedactionSecrets } from "./deps.js";
|
|
14
15
|
import { UiStoreError } from "./store/index.js";
|
|
15
16
|
const MAX_BODY_BYTES = 1_000_000;
|
|
16
17
|
const VERIFY_NOOP_MODEL = {
|
|
@@ -221,7 +222,7 @@ function engineContextFor(deps, request, model) {
|
|
|
221
222
|
evidence: {
|
|
222
223
|
store: deps.evidenceStore,
|
|
223
224
|
env: deps.env,
|
|
224
|
-
additionalSecrets: deps
|
|
225
|
+
additionalSecrets: currentRedactionSecrets(deps),
|
|
225
226
|
},
|
|
226
227
|
};
|
|
227
228
|
}
|
|
@@ -288,7 +289,7 @@ export async function handleCreateRun(ctx, deps) {
|
|
|
288
289
|
evidence: {
|
|
289
290
|
store: deps.evidenceStore,
|
|
290
291
|
env: deps.env,
|
|
291
|
-
additionalSecrets: deps
|
|
292
|
+
additionalSecrets: currentRedactionSecrets(deps),
|
|
292
293
|
},
|
|
293
294
|
};
|
|
294
295
|
try {
|