Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ $S entry2 agent # 控制面已起,只补电脑代理,shell 里的电
它做的事:

1. `docker start baize-postgres`,等 `pg_isready`。
2. 控制面:把 `.env` 按 dev.mjs 的规则导出(只认 `KEY=value`,shell 里已有的优先),跑 `pnpm --filter @baize/control-plane start`。端口取 `.env` 的 `PORT`,没写就是 3001。控制面启动要 `CREDENTIALS_KEY`,shell 和 `.env` 都没有时脚本生成一把存进 `.audit/verify-run/credentials.key`,之后每次都用它。挂 GitHub、X 账号用 `stack.sh entry2 account-add --owner … --platform github --handle … --token …`,走的是同一把钥匙;实机发布把 `GITHUB_API_BASE_URL`、`X_API_BASE_URL` 指到假平台,不用真令牌。
2. 控制面:把 `.env` 按 dev.mjs 的规则导出(只认 `KEY=value`,shell 里已有的优先),跑 `pnpm --filter @baize/control-plane start`。端口取 `.env` 的 `PORT`,没写就是 3001。控制面启动要 `CREDENTIALS_KEY` 和 `TOOL_SECRET_KEY`,shell 和 `.env` 都没有时脚本各生成一把存进 `.audit/verify-run/credentials.key`、`.audit/verify-run/tool-secret.key`,之后每次都用它们。挂 GitHub、X 账号用 `stack.sh entry2 account-add --owner … --platform github --handle … --token …`,走的是同一把钥匙;实机发布把 `GITHUB_API_BASE_URL`、`X_API_BASE_URL` 指到假平台,不用真令牌。
3. Web:`API_PORT=<.env 的 API_PORT,没写取 PORT> WEB_PORT=5173 pnpm --filter @baize/web dev`。
4. 加 `--agent` 或跑 `entry2 agent` 时起电脑代理:`pnpm --filter @baize/computer-agent exec tsx src/main.ts`。`COMPUTER_ID`、`AGENT_TOKEN`、`CONTROL_PLANE_AGENT_URL`、`BAIZE_DATA_DIR` 每个先取 shell 里的值,没有再取 `.env`,拼成一行 JSON `{ url, computerId, token, dataDir }`,经 `.audit/verify-run/entry2/computer-agent.stdin` 这个 fifo 写进代理的 stdin。代理环境里去掉 `.env` 的所有键和这四个键,和 dev.mjs 一样,token 和上游密钥都不进 Bot 的 bash。环境里只要有 `AGENT_TOKEN`(空串也算),代理就拒绝启动。stdin 关闭时代理自己停机。脚本等到代理日志里出现 `{"type":"online"}` 这一整行就算连上。这行是代理在 stdout 写的状态行(shared 的 `AgentStatusLine`),控制面接纳后就写,桌面端也认它;随后几毫秒内日志里会出现「收到第 N 代 config」。日志措辞会变,别拿它判就绪。

Expand Down
12 changes: 12 additions & 0 deletions .claude/skills/verify/scripts/stack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ ensure_credentials_key() {
export CREDENTIALS_KEY
}

ensure_tool_secret_key() {
[[ -n "${TOOL_SECRET_KEY-}" ]] && return
local file="$RUN/tool-secret.key"
if [[ ! -s "$file" ]]; then
mkdir -p "$RUN"
(umask 077 && openssl rand -hex 32 >"$file")
fi
TOOL_SECRET_KEY="$(cat "$file")"
export TOOL_SECRET_KEY
}

listener_pid() {
lsof -nP -t -iTCP:"$1" -sTCP:LISTEN 2>/dev/null | head -1 || true
}
Expand Down Expand Up @@ -330,6 +341,7 @@ entry2() {
exec_control_plane() {
export_env_file
ensure_credentials_key
ensure_tool_secret_key
exec pnpm --filter @baize/control-plane start
}

Expand Down
5 changes: 2 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ COOKIE_SECURE=
# 控制面前面有几层自己的反向代理。登录失败和注册按来源 IP 限频,留空或 0 只认 socket 地址,不读 X-Forwarded-For,
# 因为客户端能随便填它。前面挂了 Caddy 或 nginx 就填 1,取 X-Forwarded-For 最右一项;没有代理却填了,任何人都能伪造来源。
TRUSTED_PROXY_HOPS=
# 工具凭据的加密钥匙,64 位十六进制,用 openssl rand -hex 32 生成,换了之前存的工具凭据就解不开。
# 不设时用代码里的开发密钥并告警;设了空值会启动失败,所以不用时整行保持注释。
# TOOL_SECRET_KEY=
# 工具凭据的加密钥匙,64 位十六进制,没有默认值,缺了控制面起不来。用 openssl rand -hex 32 生成,换了之前存的工具凭据就解不开。
TOOL_SECRET_KEY=
# 账号令牌的加密钥匙,32 字节 base64,没有默认值,缺了控制面起不来。用 openssl rand -base64 32 生成,换了之前存的令牌就解不开。
CREDENTIALS_KEY=
# 发布走的平台接口根地址,默认官方地址。验证时指到假平台。
Expand Down
4 changes: 2 additions & 2 deletions deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ compose 读 `deploy/remote/.env`(`remote/README.md:41`)。控制面读哪些
| `UPSTREAM_BASE_URL` | 是 | 报错不起 | OpenAI 兼容的上游地址(`env.ts:94`) |
| `UPSTREAM_API_KEY` | 是 | 报错不起 | 只能是可打印 ASCII,否则控制面启动时抛错(`env.ts:170-176`) |
| `GATEWAY_MODELS` | 否 | `plan=baize-default,fast=baize-default` | 格式 `plan=模型,fast=模型`,两个用途都要有,写错启动时抛错(`env.ts:146-167`) |
| `TOOL_SECRET_KEY` | 是 | 报错不起 | 64 位十六进制(`packages/control-plane/src/crypto.ts:31`),`openssl rand -hex 32`。控制面本身没设时退回一把写在代码里的开发密钥(`env.ts:16-24`),compose 用 `:?` 挡住了这种情况 |
| `CREDENTIALS_KEY` | 是 | 报错不起 | 32 字节 base64,`openssl rand -base64 32`。没设或格式不对启动失败(`env.ts:27-33`、`packages/control-plane/src/credentials.ts:24`) |
| `TOOL_SECRET_KEY` | 是 | 报错不起 | 64 位十六进制,`openssl rand -hex 32`。没设或格式不对启动失败(`env.ts:15-19`、`packages/control-plane/src/crypto.ts:31`) |
| `CREDENTIALS_KEY` | 是 | 报错不起 | 32 字节 base64,`openssl rand -base64 32`。没设或格式不对启动失败(`env.ts:21-27`、`packages/control-plane/src/credentials.ts:24`) |
| `WEB_BASE_URL` | 是 | 报错不起 | 浏览器打开 Web 的地址,如 `https://baize.example.com`,OAuth 回调拼在它上面(`env.ts:100`) |
| `PRICE_INPUT_PER_M`、`PRICE_OUTPUT_PER_M` | 否 | `0` | 每百万 token 价格,折算 run 的花费(`env.ts:96-97`)。不设用量金额恒为 0 |
| `GITHUB_API_BASE_URL`、`X_API_BASE_URL`、`GITHUB_OAUTH_BASE_URL`、`X_OAUTH_BASE_URL` | 否 | 各家官方地址,和 `env.ts:84-87` 一致 | 只在验证时指到假平台(`env.ts:98-99`、`:122-123`),生产不用设 |
Expand Down
4 changes: 3 additions & 1 deletion docs/开发.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Bot 走到要点头的工具时群里出一张审批卡,点按钮或在群里
| `UPSTREAM_API_KEY` | 空 | 转发给上游的 key,只能是可打印 ASCII,否则启动时失败 |
| `PRICE_INPUT_PER_M` | `0` | 每百万输入 token 的价,用来折算 run 的花费 |
| `PRICE_OUTPUT_PER_M` | `0` | 每百万输出 token 的价 |
| `TOOL_SECRET_KEY` | 开发密钥,启动时告警 | 加密工具连接凭据的钥匙,64 位十六进制,用 `openssl rand -hex 32` 生成。设了但格式不对(包括空值)启动失败;换了钥匙之前存的工具凭据就解不开。部署必须设 |
| `TOOL_SECRET_KEY` | 无,必填 | 加密工具连接凭据的钥匙,64 位十六进制,用 `openssl rand -hex 32` 生成。缺了或格式不对启动失败;换了钥匙之前存的工具凭据就解不开 |
| `CREDENTIALS_KEY` | 无,必填 | 加密账号令牌的钥匙,32 字节 base64,用 `openssl rand -base64 32` 生成。缺了或长度不对启动失败;换了钥匙之前存的令牌就解不开 |
| `GITHUB_API_BASE_URL` | `https://api.github.com` | 发 GitHub 草稿打的接口根地址,验证时指到假 GitHub |
| `X_API_BASE_URL` | `https://api.x.com` | 发 X 草稿打的接口根地址,X 的令牌端点也在它下面 |
Expand All @@ -77,6 +77,8 @@ GitHub、X 账号在账号页走 OAuth 连。开发时手里有令牌想跳过

## 跑起来

先 `cp .env.example .env`,填上两把钥匙:`CREDENTIALS_KEY` 用 `openssl rand -base64 32`,`TOOL_SECRET_KEY` 用 `openssl rand -hex 32`。缺哪一把控制面都起不来,开发也没有默认值。

```
pnpm install
docker compose up -d
Expand Down
2 changes: 1 addition & 1 deletion packages/control-plane/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ hub 认连接用 `#onHello` 入表时登记的 WeakMap,再校验它还是 `#co

工具连接是用户级对象。`tool_connections` 存 MCP 地址、发现的操作及其 `needsApproval` 标记,并把认证信息以 AES-256-GCM 密文存入 `authorization_enc`;`tool_connection_bots` 记录它分配给哪些 Bot,删 Bot 时对应分配由外键级联清除。API 列表和写入响应都不返回认证信息。默认 `localOnly: false`,凭据只留在服务器,`config.tools` 仅下发操作表和 Bot 分配,不含 `authorization`。运行器经 `/mcp-proxy/:connectionId/call` 代执行,本机和远程电脑都能使用。设成「只在本机用」后,本机电脑收到 `localOnly: true` 和解密后的凭据并直连 MCP;远程电脑不收到这条连接。

生产环境设置 `TOOL_SECRET_KEY` 为 64 位十六进制密钥。未设置时启动会警告一次,并使用固定开发密钥;已有密文依赖原密钥解密,换密钥前需迁移密文。MCP 发现时把认证信息作为 `Authorization` 请求头发送。操作默认按 MCP 的 `readOnlyHint` 定审批:明确为 `true` 的不需要,其余都需要;用户可通过 `PATCH /api/tools/:id` 覆盖标记。连接创建、修改、删除后,控制面都会向该用户在线电脑重发整份 `config`。
`TOOL_SECRET_KEY` 是 64 位十六进制密钥,开发和部署都必须设,没设控制面拒绝启动。不给开发默认值,因为写在仓库里的钥匙人人知道,漏配的部署会带着它照常起来,工具凭据等于明文。已有密文依赖原密钥解密,换密钥前需迁移密文。MCP 发现时把认证信息作为 `Authorization` 请求头发送。操作默认按 MCP 的 `readOnlyHint` 定审批:明确为 `true` 的不需要,其余都需要;用户可通过 `PATCH /api/tools/:id` 覆盖标记。连接创建、修改、删除后,控制面都会向该用户在线电脑重发整份 `config`。

从分享链接导入的插件固定 `localOnly: false`,没有授权,操作表只有包里带的名字和 `needsApproval`,参数表是占位的空表:导入时控制面不去连包里的地址(D11)。所以导入的连接记成 `awaiting_authorization`,随 `config.tools[].status` 下发,运行器照样注册它的操作,但调用当场拒绝、不去连包里的地址。不能拿 `authorization` 为 null 来判:自己建的匿名 MCP 也是 null,那是 `ready`。导入后要先在 `PATCH /api/tools/:id` 带上 `authorization` 补授权,插件才能用;对端本来就不要认证的,传 `authorization: null`,控制面不带头重新探测,连上了清掉凭据、记成 `ready`。这一步对一种对端会误删凭据:`tools/list` 允许匿名、只有 `tools/call` 才要凭据。不带头的探测照样连得上,凭据一次就被清掉,之后每次调用都是 401,要所有者重新带凭据 PATCH。这不算越权,只有所有者能改自己的连接。Web 现在不发 null,以后接入这个入口时只对 `awaiting_authorization` 的连接开放,或者先二次确认。补授权时控制面当场重新 initialize 加 `tools/list`,连上了就按真实列表重建操作表并记成 `ready`,同名操作沿用原来的 `needsApproval`;连不上回 400,旧凭据和操作表都不动。探测要等对端,期间别的 PATCH 可能已经改了审批,所以写回在事务里先 `SELECT ... FOR UPDATE` 锁住这一行,按落库的最新操作表合并,普通的 operations 修改也走同一把锁。导入撞名时工具名和 Bot 名一样截掉尾巴给序号让位,不超过 40,再导出不会被分享包的上限挡住。对端能把收到的 Authorization 按任意编码写进报错,逐种编码去认认不完,所以探测失败只回固定的中文分类(`describeMcpFailure`,HTTP 状态码、MCP 错误码这一级),对端写的文字一个字都不回;运行器连不上、调用抛错也是同一套分类。探测成功时对端也可能把凭据写进操作的名字、说明或参数表,这些会落库、进回包、进分享包、进模型的工具定义,`discoverTools` 发现其中有凭据(原文、百分号编码、JSON 转义三种写法)就整份不收、回 400,不去改写操作名,改了名字就调不到对端的工具。工具调用的正文是对端的业务输出。默认由控制面在返回电脑之前脱敏,本机直连时由运行器脱敏;两条路径都换掉凭据的那三种写法,别的编码不保证。base64、小写的百分号编码、凭据的片段、Unicode 转义都认不出,成功和 `isError` 的正文一样原样进模型,模型可能转述进群消息,Pi 的 session 文件也会落盘。操作元数据的检查用的是同样三种写法,操作名里放凭据的 base64 能过检查,会落库,也会进再分享包的操作名。这个范围是有意的:对端本来就持有这把凭据,想外传不必借我们的通道,脱敏防的是它无意中原样回显。

Expand Down
10 changes: 2 additions & 8 deletions packages/control-plane/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import type { GatewayModels } from "@baize/shared";

const DEFAULT_DATABASE_URL = "postgres://baize:baize@localhost:5434/baize";
const DEFAULT_PORT = 3001;
const DEV_TOOL_SECRET_KEY = "8f8d7b86df250e27ea4f1bb66c7f4dc57b75ad623154f343890a258d68d03c90";
let warnedToolSecret = false;

export function databaseUrl(): string {
return nonEmpty("DATABASE_URL") ?? DEFAULT_DATABASE_URL;
Expand All @@ -24,12 +22,8 @@ export function trustedProxyHops(): number {

export function toolSecretKey(): string {
const key = nonEmpty("TOOL_SECRET_KEY");
if (key !== null) return key;
if (!warnedToolSecret) {
console.warn("TOOL_SECRET_KEY 没设,工具认证信息用开发密钥加密");
warnedToolSecret = true;
}
return DEV_TOOL_SECRET_KEY;
if (key === null) throw new Error("TOOL_SECRET_KEY 没设,用 openssl rand -hex 32 生成一个");
return key;
}

/** 账号凭据的钥匙没有开发默认值:令牌是真平台的,用一把人人知道的钥匙加密等于没加密。 */
Expand Down
13 changes: 12 additions & 1 deletion packages/control-plane/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { connect } from "./db/client.js";
import { databaseUrl, port, runtimeConfig, secureCookie, trustedProxyHops } from "./env.js";
import {
credentialsKey,
databaseUrl,
port,
runtimeConfig,
secureCookie,
toolSecretKey,
trustedProxyHops,
} from "./env.js";
import { acquireInstanceLock } from "./instance-lock.js";
import { startServer, type RunningServer } from "./server.js";

Expand Down Expand Up @@ -47,6 +55,8 @@ process.on("uncaughtException", (error) => {
});

try {
// 钥匙先验:缺了直接拒绝启动,不去连库,也不占单实例锁。
const keys = { toolSecretKey: toolSecretKey(), credentialsKey: credentialsKey() };
await acquireInstanceLock(databaseUrl(), () => {
console.error("持有单实例锁的库连接断了,别的实例可能已经拿走了锁,按停机流程退出");
stop(1);
Expand All @@ -58,6 +68,7 @@ try {
secureCookie: secureCookie(),
trustedProxyHops: trustedProxyHops(),
config: runtimeConfig(),
...keys,
});
} catch (error) {
console.error("控制面没起来", error);
Expand Down
1 change: 1 addition & 0 deletions packages/control-plane/test/disconnect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ beforeAll(async () => {
port: 0,
config: DEFAULT_RUNTIME_CONFIG,
credentialsKey: TEST_CREDENTIALS_KEY,
toolSecretKey: "a".repeat(64),
disconnectGraceMs: graceMs,
});
});
Expand Down
1 change: 1 addition & 0 deletions packages/control-plane/test/dispatchable-push.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ beforeAll(async () => {
port: 0,
config: DEFAULT_RUNTIME_CONFIG,
credentialsKey: TEST_CREDENTIALS_KEY,
toolSecretKey: "a".repeat(64),
scheduler: { now: () => new Date(), intervalMs: DAY_MS },
publisher: { intervalMs: DAY_MS },
disconnectGraceMs: 200,
Expand Down
6 changes: 1 addition & 5 deletions packages/control-plane/test/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
DEFAULT_X_OAUTH_BASE_URL,
credentialsKey,
runtimeConfig,
toolSecretKey,
trustedProxyHops,
} from "../src/env.js";

Expand Down Expand Up @@ -166,19 +165,17 @@ it("没配 OAuth 变量时两个平台都是 null,WEB_BASE_URL 留空就是 nu
expect([config.oauth.github, config.oauth.x]).toEqual([null, null]);
});

it(".env 里留空的地址和工具密钥按没设处理,不把空串下发成网关地址", () => {
it(".env 里留空的地址按没设处理,不把空串下发成网关地址", () => {
for (const name of [
"PUBLIC_BASE_URL",
"UPSTREAM_BASE_URL",
"GITHUB_API_BASE_URL",
"X_API_BASE_URL",
"GITHUB_OAUTH_BASE_URL",
"X_OAUTH_BASE_URL",
"TOOL_SECRET_KEY",
]) {
vi.stubEnv(name, "");
}
vi.spyOn(console, "warn").mockImplementation(() => {});

const config = runtimeConfig();
expect(config.publicBaseUrl).toBeNull();
Expand All @@ -187,7 +184,6 @@ it(".env 里留空的地址和工具密钥按没设处理,不把空串下发
expect(config.xApiBaseUrl).toBe(DEFAULT_X_API_BASE_URL);
expect(config.oauth.githubOauthBaseUrl).toBe(DEFAULT_GITHUB_OAUTH_BASE_URL);
expect(config.oauth.xOauthBaseUrl).toBe(DEFAULT_X_OAUTH_BASE_URL);
expect(toolSecretKey()).not.toBe("");
});

it("TRUSTED_PROXY_HOPS 留空是 0,只收非负整数", () => {
Expand Down
1 change: 1 addition & 0 deletions packages/control-plane/test/liveness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ beforeAll(async () => {
port: 0,
config: DEFAULT_RUNTIME_CONFIG,
credentialsKey: TEST_CREDENTIALS_KEY,
toolSecretKey: "a".repeat(64),
scheduler: { now: () => clock, intervalMs: HOUR_MS },
publisher: { intervalMs: 24 * HOUR_MS },
silenceTimeoutMs: SILENCE_MS,
Expand Down
1 change: 1 addition & 0 deletions packages/control-plane/test/reconnect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ beforeAll(async () => {
port: 0,
config: DEFAULT_RUNTIME_CONFIG,
credentialsKey: TEST_CREDENTIALS_KEY,
toolSecretKey: "a".repeat(64),
});
});

Expand Down
8 changes: 7 additions & 1 deletion packages/control-plane/test/run-done-replay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ let hasty: RunningServer;

beforeAll(async () => {
database = await createTestDb();
const base = { db: database.db, port: 0, config, credentialsKey: TEST_CREDENTIALS_KEY } as const;
const base = {
db: database.db,
port: 0,
config,
credentialsKey: TEST_CREDENTIALS_KEY,
toolSecretKey: "a".repeat(64),
} as const;
// 宽限设长,断开到重连之间只有 hello 对账会收尾。
patient = await startServer({ ...base, disconnectGraceMs: 60_000 });
hasty = await startServer({ ...base, disconnectGraceMs: 50 });
Expand Down
40 changes: 40 additions & 0 deletions packages/control-plane/test/start-keys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { execFile } from "node:child_process";
import { randomBytes } from "node:crypto";
import { fileURLToPath } from "node:url";
import { expect, it } from "vitest";

const MAIN = fileURLToPath(new URL("../src/main.ts", import.meta.url));
const TSX = fileURLToPath(new URL("../node_modules/.bin/tsx", import.meta.url));
const UNREACHABLE_DATABASE_URL = "postgres://baize:baize@127.0.0.1:1/none";

function start(drop: "TOOL_SECRET_KEY" | "CREDENTIALS_KEY" | null, toolSecretKey: string) {
const env: NodeJS.ProcessEnv = {
...process.env,
DATABASE_URL: UNREACHABLE_DATABASE_URL,
PORT: "0",
TOOL_SECRET_KEY: toolSecretKey,
CREDENTIALS_KEY: randomBytes(32).toString("base64"),
};
if (drop !== null) delete env[drop];
return new Promise<{ readonly code: number | null; readonly stderr: string }>((resolve) => {
execFile(TSX, [MAIN], { env, timeout: 30_000 }, (error, _stdout, stderr) => {
resolve({ code: error === null ? 0 : typeof error.code === "number" ? error.code : null, stderr });
});
});
}

it("TOOL_SECRET_KEY 没设或留空时控制面拒绝启动,报错点名这个变量", async () => {
const missing = await start("TOOL_SECRET_KEY", "");
expect(missing.code).toBe(1);
expect(missing.stderr).toContain("TOOL_SECRET_KEY 没设,用 openssl rand -hex 32 生成一个");

const blank = await start(null, " ");
expect(blank.code).toBe(1);
expect(blank.stderr).toContain("TOOL_SECRET_KEY 没设,用 openssl rand -hex 32 生成一个");
});

it("CREDENTIALS_KEY 没设时控制面同样拒绝启动", async () => {
const result = await start("CREDENTIALS_KEY", randomBytes(32).toString("hex"));
expect(result.code).toBe(1);
expect(result.stderr).toContain("CREDENTIALS_KEY 没设,用 openssl rand -base64 32 生成一个");
});
1 change: 1 addition & 0 deletions packages/desktop/scripts/prove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ async function main(): Promise<void> {
PORT: String(controlPort),
COOKIE_SECURE: "false",
CREDENTIALS_KEY: randomBytes(32).toString("base64"),
TOOL_SECRET_KEY: randomBytes(32).toString("hex"),
};
run("pnpm", ["--filter", "@baize/control-plane", "migrate"], baseEnv);
control = spawn("pnpm", ["--filter", "@baize/control-plane", "start"], {
Expand Down
Loading
Loading