AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Convex component for boxd composable computers
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
Run boxd machines from your Convex backend. Each machine is a
full Linux microVM with its own disk, memory and HTTPS URL. boxd cold-boots a
machine in under 10 ms and forks one like a git branch in under 200 ms, and a
machine sleeps and wakes with its memory intact. Through this component,
create and fork return in under a second, because they also wait until the
new machine accepts a command.
The component keeps a reactive row for every machine and every command it runs,
so your UI subscribes to machine state and command output with a plain Convex
query.檢視完整 README ↗Run boxd machines from your Convex backend. Each machine is a
full Linux microVM with its own disk, memory and HTTPS URL. boxd cold-boots a
machine in under 10 ms and forks one like a git branch in under 200 ms, and a
machine sleeps and wakes with its memory intact. Through this component,
create and fork return in under a second, because they also wait until the
new machine accepts a command.
The component keeps a reactive row for every machine and every command it runs, so your UI subscribes to machine state and command output with a plain Convex query.
https://<machine>.boxd.sh.NOT_FOUND, CONFLICT, ...).npm install @boxd-sh/convex
Create an API key with the boxd CLI and set it on your deployment:
boxd auth keys create convex
npx convex env set BOXD_API_KEY bxd_...
A key is fenced to one boxd org. Machines are created in, and billed to, that org.
In a brand-new Convex project, run npx convex dev once first so there is a
deployment to set the key on. Its first push fails until the key is set, since
the component requires it.
Install the component in convex/convex.config.ts, and bind the key into it:
import { defineApp } from "convex/server";
import { v } from "convex/values";
import boxd from "@boxd-sh/convex/convex.config.js";
const app = defineApp({
env: { BOXD_API_KEY: v.string() },
});
app.use(boxd, { env: { BOXD_API_KEY: app.env.BOXD_API_KEY } });
export default app;
The key goes into the component's own environment, so it never travels as a function argument.
// convex/machines.ts
import { Boxd } from "@boxd-sh/convex";
import { v } from "convex/values";
import { components } from "./_generated/api";
import {
action,
query,
type ActionCtx,
type QueryCtx,
} from "./_generated/server";
const boxd = new Boxd(components.boxd);
// The component can't see your users, so derive the owner from your auth.
// Never take it from a client argument.
async function ownerId(ctx: QueryCtx | ActionCtx) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthenticated");
return identity.subject;
}
export const createMachine = action({
args: {},
handler: async (ctx) => {
return await boxd.create(ctx, {
ownerId: await ownerId(ctx),
vcpu: 1,
autoSuspendSeconds: 300,
});
},
});
export const run = action({
args: { machineId: v.string(), command: v.string() },
handler: async (ctx, { machineId, command }) => {
return await boxd.exec(ctx, {
ownerId: await ownerId(ctx),
machineId,
command,
});
},
});
// Reactive: re-runs whenever one of this user's machine rows changes.
export const myMachines = query({
args: {},
handler: async (ctx) => boxd.list(ctx, { ownerId: await ownerId(ctx) }),
});
// Reactive: every command's status and output, newest first.
export const history = query({
args: { machineId: v.string() },
handler: async (ctx, { machineId }) =>
boxd.listExecutions(ctx, { machineId, ownerId: await ownerId(ctx) }),
});
example/convex/example.ts wraps every method this way.
Every method takes the Convex ctx first. Reads run in queries. Everything that
calls boxd runs in actions. Every call that addresses a machine takes the
ownerId it was created with: leave it out and a machine created with an owner
is NOT_FOUND (see Owners).
| Method | What it does |
|---|---|
create(ctx, { ownerId, ... }) | Create a machine. Waits until it accepts a command, unless waitUntilReady: false |
fork(ctx, { machineId, ownerId }) | Copy a machine, disk and memory, into a new one with the same owner |
exec(ctx, { machineId, ownerId, command }) | Run a command, wait for it to exit, record it. command is a string or an argv |
readFile / readFileBytes | Read a UTF-8 file as text, or any file byte for byte |
writeFile(ctx, { machineId, ownerId, path, content }) | Write text or bytes. The parent directory must exist |
listDir(ctx, { machineId, ownerId, path }) | List a directory |
expose(ctx, { machineId, ownerId, port, name? }) | Route public HTTPS to a port. Returns the URL |
pause / resume | Freeze in RAM, and thaw |
hibernate / wake | Save memory to disk and free the host, and restore |
stop / start | Shut down and cold boot. The disk is kept |
refresh(ctx, { machineId, ownerId }) | Re-read the machine from boxd into its row |
destroy(ctx, { machineId, ownerId }) | Destroy the machine. Its row and history stay, with status destroyed |
get / list | Machine rows, reactive |
listExecutions / getExecution | Command history, reactive |
create takes name, image, env, vcpu or memory (size classes 1, 2 or
4 vCPU with 4, 8 or 16 GiB), autoSuspendSeconds, autoDestroySeconds and
isolated. The component never stores env.
Pass ownerId on every call. A machine created with an ownerId is reachable
only with that same ownerId, and one created without one only without one. For
any other owner, reads return null or [], and actions fail with NOT_FOUND,
the same as for a machine that doesn't exist. A fork gets its source's owner.
Every row updates the moment a call through the component changes it, and
queries over them re-run on their own. boxd can also change a machine without a
call, for example auto-suspend after autoSuspendSeconds or auto-destroy after
autoDestroySeconds. Call refresh to pick that up, for instance on a timer
while a machine is on screen.
A paused or hibernated machine wakes by itself when it gets a command or an
HTTPS request, so exec works without a resume first. Every transition is
idempotent: resume on a running machine succeeds.
Errors are ConvexErrors with data: { code, message }. Branch on the code
with isBoxdError:
import { isBoxdError } from "@boxd-sh/convex";
try {
await boxd.refresh(ctx, { machineId, ownerId });
} catch (error) {
if (isBoxdError(error, "NOT_FOUND")) return null;
throw error;
}
| Code | Meaning |
|---|---|
INVALID_ARGUMENT | An argument is wrong. Nothing was sent to boxd |
NOT_FOUND | No such machine for this owner, or no such file |
CONFLICT | The machine's state doesn't allow this, e.g. pause when stopped |
UNAUTHENTICATED | BOXD_API_KEY is missing, invalid, expired or revoked |
PERMISSION_DENIED | The key's org may not do this |
RATE_LIMITED | A quota or rate limit, e.g. the org's machine quota |
TIMEOUT | A command ran past its timeoutMs, or a machine wasn't ready in time |
UNAVAILABLE | boxd could not be reached |
BOXD_ERROR | Anything else |
A failed lifecycle call also stores its message in the machine's lastError.
The next successful call clears it.
When create or fork makes a machine that then fails to become ready, the
error's data.machineId names it. The machine exists, and bills, until you
destroy it, so act on that id instead of creating another machine.
exec
kills its command after timeoutMs: default 9 minutes, at most 9.5. For
longer work, start it in the background (nohup ... &) and check on it with
later commands.exec returns up to 2 million characters per stream. The
execution row keeps the first 64,000.readFile, readFileBytes and writeFile move at most 8 MiB.
Move bigger files with exec, for example with curl.expose with a name returns at once, but the new URL
answers HTTPS only after boxd issues its certificate, typically 15 to 20
seconds later. The default URL answers at once.The component calls boxd through the
@boxd-sh/sdk web entry, which
runs in Convex's default runtime. It exchanges the API key for a session token
once, keeps the token in its own table, and reuses it until it nears expiry.
boxd rate-limits that exchange per source IP, and Convex deployments share their
egress IPs, so each action must not exchange the key again. If boxd revokes the
token early, the component exchanges the key again and retries the call once.
Register the component with convex-test:
import { convexTest } from "convex-test";
import boxdTest from "@boxd-sh/convex/test";
import schema from "./schema";
const modules = import.meta.glob("./**/*.ts");
const t = convexTest(schema, modules);
boxdTest.register(t);
To keep tests off the network, mock @boxd-sh/sdk/web with vi.mock. The
component's own tests do this: see src/component/setup.test.ts.
npm install
npm run build:codegen # codegen and build dist/
npm run check # build, typecheck, lint, format check, unit tests
npm run test:live # the example app against real boxd machines
test:live runs the example app on an anonymous local Convex backend, with no
Convex account and nothing uploaded, against real boxd machines. It needs
BOXD_API_KEY in the environment or in .env.e2e.local, and destroys every
machine it creates.
demo/ is the public demo: a small React page (demo/src) that boots a
machine, runs commands and forks it, all through the component, with its own
Convex backend in demo/convex. That backend adds the parts a public page needs
on top of the component: anonymous sign-in, an optional password
(DEMO_PASSWORD), per-visitor machine caps, rate limits, and a cron that
destroys each machine after ten minutes. Demo machines are isolated, so
visitor commands can't reach boxd or other machines. Run it with
npx convex dev and npm run dev:demo, or build it with npm run build:demo.
example/convex is a reference app that wraps the whole API with no limits. It
backs the unit and live tests and is never deployed alongside the demo, since
its functions are open to any signed-in user.
MIT