diff --git a/.claude/skills/ui-test/SKILL.md b/.claude/skills/ui-test/SKILL.md deleted file mode 100644 index 59bb2c5910..0000000000 --- a/.claude/skills/ui-test/SKILL.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -name: ui-test -description: 'Grade a live Workbench UI surface through the owner progression bugs before beauty then drop-dead gorgeous. Drives agent-browser against a running app, checks three pass/fail tiers (entity sanity and dead-button bugs, DESIGN.md beauty, Vercel/Stripe/Apple-bar gorgeousness), and produces a surface-tier-blockers-fix report. Use when asked to QA, sweep, test, or grade a UI screen, dialog, or flow before it ships or demos.' ---- - -# ui-test — grade a surface, don't vibe it - -Adapted from [browserbase/skills ui-test](https://github.com/browserbase/skills/tree/main/skills/ui-test): -kept the adversarial stance, the before/after evidence discipline, and the -structured pass/fail markers. Replaced `browse` with **agent-browser** (our -driver), replaced generic UX heuristics with our own three gates (DESIGN.md, -consumer-language, CL-6650's tier ladder), and replaced "find any bug" with -"stop at the first tier you fail" — a surface that fails Tier 1 does not get -a beauty opinion, because a data bug undermines any beauty judgment made -around it. - -Your job is to **find the reason this surface would embarrass someone in a -demo**, not confirm it renders. Every verdict below Tier 3 must cite -snapshot/screenshot evidence; a "looks fine" without a ref or a quoted -snapshot line is not a passing check, it's a skipped one. - -## The three tiers (CL-6650) - -Walk them in order. **Stop at the first tier the surface fails** and report -that as the tier reached — don't grade beauty on a surface that still has -data bugs, and don't grade gorgeousness on a surface that's merely tidy. - -### Tier 1 — bugs (blocks everything) - -- **Entity sanity**: every list contains only its own kind. A routine - showing up in an agent list, a workbench in a skills list — a data bug, - not a display nuance. -- **Names in name slots**: the title/name position never renders a - description, an ID, or a template string. Check the actual text content - of the name element, not just that *some* text is present. -- **No placeholder content**: no "Lorem ipsum", no `TODO`, no seeded fixture - names a real user would never see (see repo's `no-customer-data-in-code` - rule — the reverse also holds: no leaked internal fixture names in a - screen meant to look real). -- **No dead buttons**: every visible actionable control does something - when activated. A button wired to nothing is a Tier 1 bug even if it - looks correct. -- **Feedback ≤100ms**: every action (click, submit, toggle) produces a - visible state change — a spinner, a disabled state, an optimistic - update — within 100ms. Silence after a click reads as broken even if - the real response eventually arrives. -- **Failures name their cause**: an error state shows a human-readable - reason and, where the app's error-sink is wired up, a `refId` a person - could quote to support. A bare "Something went wrong" with no ref is a - Tier 1 finding. - -### Tier 2 — beauty (blocks demo-path surfaces) - -Only graded once Tier 1 is clean. - -- **Identity on every row**: avatar/mark + name + one-line consumer - description — never a bare name floating with no visual identity, never - an ID standing in for a description. -- **Spacing per DESIGN.md**: tight, not loose. Loose spacing is a defect - per standing owner rule, not a style preference — see - `always-tighten-spacing`. -- **Consistent loading treatment**: no blank panes while data resolves, no - overlapping chrome (a skeleton and real content both mounted at once), no - layout jump when content lands. -- **Consumer language only**: copy speaks the user's world - ("Running now," a cron rendered as "every weekday at 9am"), never the - system's internals ("in flight," a raw cron expression, an enum value - leaking into a label). - -### Tier 3 — drop-dead gorgeous (the differentiator) - -Only graded once Tier 2 is clean. This tier is a judgment call, not a -checklist — but the judgment must be argued, not asserted. - -- **The side-by-side test**: screenshot the surface, then ask — if a - viewer put this next to OpenBot or another agent-desktop competitor, - would they say "that's better"? Name the specific thing that would make - them say it, or the specific thing that would make them say the - opposite. -- **Motion earns its place**: transitions encode a real state change - (something entering, transforming, focus moving) — durations in the - 150–300ms DESIGN.md range, easing from the two named curves. Motion that - could be deleted without losing information is decoration, not gorgeous. -- **States feel alive**: presence dots, typing indicators, live status — - anything idle-looking that should read as active is a Tier 3 miss. -- **Zero "is it working?" moments**: no point in the flow where a person - would pause and wonder whether their action registered. - -Report Tier 3 as **reached with notes**, not pass/fail — it's the -differentiator tier, so name what's already gorgeous and what's one step -short, rather than a binary verdict. - -## Driving the surface: agent-browser - -Use a named session for the whole run — never the shared default session, -which other agents and the human may be using concurrently: - -```bash -export AGENT_BROWSER_SESSION="$(agent-browser session id --scope worktree --prefix uitest)" -``` - -(Per CL-6650's acceptance run, `--session uitest` is also acceptable when a -literal fixed name is asked for — either way, name it, don't use the -default.) - -Core loop — identical discipline to the upstream skill, our driver: - -```bash -agent-browser open -agent-browser snapshot -i # interactive elements only -agent-browser click @e3 # act on a ref from the snapshot just taken -agent-browser snapshot -i # re-snapshot — refs go stale the instant the page changes -``` - -Log in once per run if the surface requires auth, then re-use the session -for every subsequent check rather than re-authing per tier. - -### The stale-match trap (read this before grepping a snapshot) - -**Never `grep`/search a whole-page snapshot for the text you're checking.** -A full snapshot includes every region on screen at once — sidebar bench -previews, a notifications panel, a command palette residue — and Workbench -intentionally echoes short strings (a bench's first message, an agent's -name) into more than one place. A grep over the entire tree will match a -sidebar preview and report a false pass ("found the name") when the actual -timeline or dialog you're grading still shows the bug. - -Always scope the assertion to the specific region under test: - -```bash -# Wrong: matches anywhere on the page, including the sidebar's bench preview -agent-browser snapshot -i | grep "Jimmy" - -# Right: scope to the region that actually renders the thing under test -agent-browser snapshot -i -s "[role=dialog]" # a modal/dialog -agent-browser snapshot -i -s "[data-testid=timeline]" # the conversation timeline -agent-browser snapshot -i -s "#main" # the page body, excluding the sidebar -``` - -If the surface has no stable selector for its region, screenshot it and -verify the specific row/element visually rather than trusting a -whole-tree text match. When in doubt, take a narrower snapshot, not a -wider grep. - -### Evidence per finding - -Every claim in the report — pass or fail — cites one of, in order of -rigor: - -1. A scoped snapshot line with its `@eN` ref and exact text. -2. A before/after pair of scoped snapshots showing what changed. -3. A screenshot (`agent-browser screenshot `), for visual-only - properties a snapshot can't capture (spacing, color, motion). - -Save screenshots to `.context/ui-test-screenshots/-.png`. - -## Running the sweep - -1. **Confirm the target renders.** `agent-browser open `, then a - scoped snapshot of the main region — not an error overlay, not a blank - body. -2. **Walk Tier 1 first**, end to end, on the whole surface. Any failure: - stop, that's the tier reached. -3. **If Tier 1 is clean, walk Tier 2.** Any failure: stop, that's the tier - reached — note it still blocks if this is a demo-path surface. -4. **If Tier 2 is clean, walk Tier 3** and write it up as judgment with - evidence, not pass/fail. -5. **Write the report** (format below). -6. `agent-browser close` when done with the session. - -## Report format - -``` -## UI Test — - -**Tier reached:** <1 (bugs) | 2 (beauty) | 3 (gorgeous)> -**URL:** -**Session:** - -### Blockers -1. - - Evidence: - - Smallest fix: - -2. ... - -### Tier 3 notes (only if Tiers 1–2 are clean) -- What already clears the Vercel/Stripe/Apple bar: ... -- What's one step short, and what would close the gap: ... - -### Passed checks -- — — -``` - -List blockers before passed checks — a reviewer needs the bad news first. -A surface with zero Tier 1 or Tier 2 blockers still gets its passed -checks listed, so the report proves the tiers were actually walked rather -than skipped. - -## References - -- [references/agent-browser-recipes.md](references/agent-browser-recipes.md) — - copy-paste command patterns: sessions, scoped snapshots, deterministic - checks (console errors, broken images), screenshot capture. -- [references/regression-seeding.md](references/regression-seeding.md) — - how to seed a known regression locally to verify the skill actually - catches it, per CL-6650's acceptance criteria. - -## Troubleshooting - -- **Stale refs**: re-`snapshot` — refs are assigned fresh every time and - go stale the moment the page changes. -- **Blank snapshot**: `agent-browser wait --load networkidle` before - snapshotting; Workbench routes can return 200 with a still-mounting - shell. -- **Session collision**: make sure every command in the run uses the same - `AGENT_BROWSER_SESSION` (or `--session `) — a command without it - falls back to the shared default session. -- **Can't tell if a match is the real region or a stale sidebar echo**: - narrow the snapshot's `-s` selector further, or fall back to a - screenshot and look at it. diff --git a/.claude/skills/ui-test/references/agent-browser-recipes.md b/.claude/skills/ui-test/references/agent-browser-recipes.md deleted file mode 100644 index 94247297eb..0000000000 --- a/.claude/skills/ui-test/references/agent-browser-recipes.md +++ /dev/null @@ -1,85 +0,0 @@ -# agent-browser recipes for ui-test - -Copy-paste patterns for the checks SKILL.md calls out. Load `agent-browser -skills get core --full` for the full command reference if a pattern here -doesn't cover what you need — this file only holds the ui-test-specific -subset. - -## Session setup - -```bash -export AGENT_BROWSER_SESSION="$(agent-browser session id --scope worktree --prefix uitest)" -agent-browser open http://localhost:3000 -``` - -Or a fixed literal name when a run needs to be resumable across commands: - -```bash -agent-browser --session uitest open http://localhost:3000 -``` - -Always `agent-browser close` (or `close --all` if you opened more than one -tab/context) at the end of a run. - -## Scoped snapshots (the stale-match trap, operationalized) - -```bash -# Whole page — use only to orient yourself at the start of a run -agent-browser snapshot -i - -# A dialog/modal under test -agent-browser snapshot -i -s "[role=dialog]" - -# The conversation timeline, not the sidebar's preview of the same bench -agent-browser snapshot -i -s "[data-testid=timeline]" - -# The page body, explicitly excluding the sidebar -agent-browser snapshot -i -s "#main" -``` - -If the surface has no `data-testid`, scope by the nearest stable -landmark role (`snapshot -i -s "[role=main]"`, `-s "form"`) before -falling back to a screenshot. - -## Before/after comparison - -```bash -agent-browser snapshot -i -s "[role=dialog]" # BEFORE -agent-browser click @e4 # ACT -agent-browser snapshot -i -s "[role=dialog]" # AFTER — compare, don't assume -``` - -## Feedback-latency check (Tier 1: actions acknowledged ≤100ms) - -There is no built-in stopwatch — approximate it by snapshotting -immediately after the action and checking whether *any* visible change -(disabled state, spinner, optimistic row) is already present, versus a -`wait --text` call to see how long the real change takes to land: - -```bash -agent-browser click @e4 -agent-browser snapshot -i -s "[role=dialog]" # should already differ from BEFORE -``` - -If the immediate snapshot is byte-identical to BEFORE, that's a Tier 1 -finding regardless of whether the eventual result is correct. - -## Deterministic checks - -```bash -# Console errors since the page loaded -agent-browser eval "window.__uitestErrors ?? []" # if the app exposes a hook; otherwise: -agent-browser eval "Array.from(document.querySelectorAll('img')).filter(i => i.naturalWidth === 0).map(i => i.src)" -``` - -## Screenshot evidence - -```bash -mkdir -p .context/ui-test-screenshots -agent-browser screenshot .context/ui-test-screenshots/-.png -``` - -Take the screenshot at the moment of failure, not after any recovery -click. For Tier 3 visual judgment (spacing, motion, color), screenshot -the surface even when nothing is "wrong" — the report cites it as -evidence for the gorgeous-tier argument either way. diff --git a/.claude/skills/ui-test/references/regression-seeding.md b/.claude/skills/ui-test/references/regression-seeding.md deleted file mode 100644 index 88ebeb29a0..0000000000 --- a/.claude/skills/ui-test/references/regression-seeding.md +++ /dev/null @@ -1,40 +0,0 @@ -# Seeding a regression to verify the skill actually catches it - -CL-6650's acceptance criteria requires proving the skill flags a known -Tier 1 bug, not just that it produces a report. Use this before trusting -a change to the skill itself. - -## Pattern - -1. Pick a surface and a specific Tier 1 rule (e.g. "names in name slots"). -2. Temporarily edit the source so the surface violates that rule — e.g. - rename an agent row's displayed name to its description string. -3. Run the ui-test sweep against the running dev server with the edit in - place. Confirm the report's Tier 1 blockers section names the exact - row and cites a scoped snapshot line showing the wrong text. -4. Revert the edit (`git checkout -- ` or undo the in-memory - change) before finishing — a seeded regression is a local-only probe, - never something that ships. - -## Worked example (Jimmy's row) - -```bash -# Seed: swap the name-slot text for the description text in the -# component that renders the roster row (path depends on the surface -# under test — locate the row-rendering component first). -# e.g. temporarily render {agent.description} where {agent.name} belongs. - -agent-browser open http://localhost:3000/ -agent-browser snapshot -i -s "[data-testid=roster]" -# Expect the report to flag: "name slot renders 'Handles onboarding -# flows for new hires' instead of 'Jimmy'" with the scoped snapshot ref -# as evidence. - -# Revert the seed once confirmed. -git checkout -- -``` - -A sweep that reports Tier 2/3 findings but misses the seeded Tier 1 swap -means the Tier 1 walk isn't actually checking name-slot content — fix the -skill's Tier 1 instructions before trusting its verdicts on anything -else. diff --git a/.env.example b/.env.example index abfec096cc..70db5b5d76 100644 --- a/.env.example +++ b/.env.example @@ -1,278 +1,71 @@ -# Copy this file to .env before starting the stack: +# cp .env.example .env # -# cp .env.example .env -# -# DATABASE_URL, HUB_DATA_DIR, CREDENTIAL_ENCRYPTION_KEY, and -# PRINCIPAL_KEY_ENCRYPTION_KEY are required; everything else is optional -# (see its own comment). `bun run dev` reports the first missing or -# malformed value it hits at boot — nothing is silently defaulted. -# -# The hub itself (apps/hub/src/server.ts) is upstream Interchange's own -# hub shape: it reads DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME -# individually rather than a single connection string, and mints its -# own session auth with no SESSION_SECRET or social-provider config. -# `bun run dev` derives those DB_* values from DATABASE_URL below so -# this file only needs to state one URL. +# Required: DATABASE_URL, HUB_DATA_DIR, CREDENTIAL_ENCRYPTION_KEY, +# PRINCIPAL_KEY_ENCRYPTION_KEY. Everything else is optional and off when +# unset. The hub reads DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME; +# `bun run dev` derives them from DATABASE_URL. -# Postgres connection URL for your local Postgres (see the README's -# "Running locally" section for a one-time setup). Prefer an explicit -# username (postgres://@localhost:5432/workbench) — environments -# without $USER cannot infer one. DATABASE_URL=postgres://localhost:5432/workbench -# The origin the Corbits connections/OAuth mount uses to build callback -# URLs (github/gmail/huggingface connect, MCP OAuth). Unset falls back to -# http://localhost:, which is wrong once anything sits in front of -# the hub — set this explicitly behind a reverse proxy (e.g. tailscale -# serve), where BASE_URL is the public origin and PORT is the local port -# the proxy forwards to. +# Public origin used to build OAuth callback URLs. Set it behind a proxy. BASE_URL=http://localhost:3000 # PORT=3000 -# Directory where the hub keeps durable repo and asset state. Created -# on boot if absent; relative paths resolve against the hub's working -# directory. The default below lives inside this git work tree, so local -# `bun run dev` also needs HUB_ALLOW_GIT_INSIDE_WORK_TREE. Tests must -# not use this path — they install test/disposable-hub-data-dir.ts. +# Durable hub state (git repos and assets). The default sits inside this +# work tree, which needs the opt-in below. Tests never use this path. HUB_DATA_DIR=.data/hub - -# Opt-in to initialize hub git-on-disk state inside an existing git work -# tree. Required for the default HUB_DATA_DIR above. Never set this in -# tests; a missed nested init walks up to this repo's .git and a genesis -# commit can land on the working branch and delete the checkout. HUB_ALLOW_GIT_INSIDE_WORK_TREE=1 -# The web app now runs its own dev server (`bun run dev` starts it -# alongside the hub) rather than being served as a static build from the -# hub's own origin — see apps/web/vite.config.ts's /api proxy. - -# The administrator identity `bun run dev` seeds for local development: -# the dev script signs this account up on the fresh hub, so a fresh -# checkout can sign in immediately. Unset values fall back to the defaults -# shown — fine for local development, set both for real deployments. +# Local admin that `bun run dev` signs up on a fresh hub. # HUB_ADMIN_EMAIL=alice@example.com # HUB_ADMIN_PASSWORD=password123 -# Everything below is optional. Leave a variable unset to leave the -# feature it configures off; the hub never treats a partially-set group -# as configured — it fails loudly at boot instead. - -# Self-serve signup is ungated: stock Interchange composition (createAuth) -# carries no signup mode, domain allowlist, rate limit, or -# email-verification gate, so anyone can register. Gate access in the -# composition that embeds the hub if a deployment needs it. +# 32 bytes hex each (openssl rand -hex 32). Generate your own outside a laptop. +CREDENTIAL_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 +PRINCIPAL_KEY_ENCRYPTION_KEY=1111111111111111111111111111111111111111111111111111111111111111 -# Provider API keys are no longer read from the environment: hub boot -# plants nothing, and setting ANTHROPIC_API_KEY or any other provider's -# env var here has no effect. Connect a provider in the UI (the -# onboarding wizard's connect cards) instead. +# Max tool-package tarball the registry accepts. Default 10 MiB. +# HUB_MAX_TARBALL_BYTES=10485760 -# Local (or tailscale-tunneled) Ollama origin. Optional, and still read -# by `bun run dev`'s memory setup: it mounts the `@corbits/memory` plane -# against this origin when EMBED_BASE_URL is unset (`nomic-embed-text`, -# ollama style). Connect the Ollama provider in the UI to give the hub -# itself an inference credential — no key required, just the base URL. +# Inference providers are connected from the UI, never from env. A local +# Ollama origin also lets `bun run dev` mount the memory plane against it. # OLLAMA_BASE_URL=http://localhost:11434 -# Set to 1 to make `workbench seed` also deploy the zero-cost -# catalog-test workflows (heartbeat, channel-digest), which exist only -# to exercise the platform's scheduling and channel-mail paths -# continuously. Leave unset for a real bench — these are dev/CI -# tooling, never part of a real user's workflow set. -# WORKBENCH_SEED_CATALOG_TEST_WORKFLOWS= - -# The `github` connector stores a personal access token today (Plugins -# page and the in-room connect-github card are PAT-first — CL-6345). Create -# a token at https://github.com/settings/tokens with the `repo` scope and -# paste it in the connect UI; no OAuth App is required for that path. -# -# Optional — a hosted one-click GitHub connect (CL-6343, not the shipped -# welcome mat). Create an OAuth App at -# https://github.com/settings/developers with an authorization callback -# URL of /api/tenants/ — GitHub matches any redirect under that -# prefix; the flow's actual callback is tenant-scoped -# /api/tenants//connections/oauth/github/callback -# (CL-6394). Leave both unset for normal PAT-first deploys. +# OAuth connectors. Callback: /api/tenants//connections/oauth//callback # GITHUB_APP_CLIENT_ID= # GITHUB_APP_CLIENT_SECRET= - -# Gmail connector (read, draft, send — sending always waits for approval): -# create an OAuth client at https://console.cloud.google.com/apis/credentials -# with the Gmail API enabled. Google requires EXACT-match redirect URIs, so register the -# tenant-scoped callback for each tenant that will connect: -# /api/tenants//connections/oauth/gmail/callback -# The connect asks for the gmail.modify scope with offline access (the -# refresh token). Google's app verification for gmail scopes takes weeks — -# start it early; unverified apps are capped at 100 test users. Leave both -# unset and the Gmail connect card renders as not configured. # GMAIL_CLIENT_ID= # GMAIL_CLIENT_SECRET= - -# Onboarding's Hugging Face connect card (a public OAuth app — no -# secret): create one at https://huggingface.co/settings/applications/new -# with a redirect URI of /api/onboarding/oauth/huggingface/callback -# (first-login onboarding) — the settings/plugins Connect buttons ride -# the tenant-scoped /api/tenants//connections/oauth/huggingface/callback -# instead (CL-6394), so register that shape too if your HF app supports -# it — and scope "openid inference-api". See -# docs/onboarding-huggingface-connect.md for the full setup. Leave unset -# and Hugging Face stays available only as a paste-a-token provider card. # HUGGINGFACE_OAUTH_CLIENT_ID= -# Optional firm-memory plane (@corbits/memory). Uses DATABASE_URL — the -# same URL as everything else — in its own `memory` schema. Recommended: -# run `bun run setup:memory` for a machine-specific recommendation (native -# Ollama, Docker, or a remote endpoint, whichever this checkout can -# actually use) instead of hand-picking the block below. That command -# writes missing EMBED_* keys into .env when a local embed path exists. -# `bun run dev` also mounts the plane from OLLAMA_BASE_URL, or from native -# Ollama on PATH, so a local embed path does not leave memory dark. -# -# Leave EMBED_BASE_URL and OLLAMA_BASE_URL unset (and no native Ollama) to -# boot without memory: memory_search/ memory_add/memory_list then answer -# with a plain "memory isn't set up on this server yet" note instead of an -# error, and the tool isn't even offered to Myra. This is the one -# honest-degradation case worth being explicit about: setting -# EMBED_BASE_URL later does NOT retroactively embed anything written while -# it was unset — migrations create the tables either way, but there is no -# automatic backfill, so rows added before embedding was configured stay -# invisible to memory_search forever unless something re-adds them. -# -# Managed OpenAI embeddings: -# EMBED_BASE_URL=https://api.openai.com/v1 -# EMBED_MODEL=text-embedding-3-small -# EMBED_API_KEY= -# -# Local dev with no external embedding account: point at a local Ollama -# instance instead (`ollama pull nomic-embed-text`, then `ollama serve`, -# the default at http://localhost:11434). No EMBED_API_KEY needed. +# Memory plane (@corbits/memory). Run `bun run setup:memory` to fill these. # EMBED_BASE_URL=http://localhost:11434 # EMBED_MODEL=nomic-embed-text -# EMBED_API_STYLE=ollama -# -# Memory plane tuning, all optional — leave unset for the defaults below. -# Max Postgres connections the memory plane's own pool opens. Default 8. +# EMBED_API_STYLE=ollama # openai | tei | ollama +# EMBED_API_KEY= +# EMBED_TIMEOUT_MS= # DB_POOL_MAX=8 -# Postgres full-text-search config name (e.g. "english"). Package default -# applies when unset. # FTS_LANGUAGE= -# Embedding endpoint's request shape: "openai" (default), "tei" (a Text -# Embeddings Inference server), or "ollama" (a local/self-hosted Ollama -# instance's /api/embed). -# EMBED_API_STYLE=openai -# Timeout in milliseconds for a single embedding request. Package default -# applies when unset. -# EMBED_TIMEOUT_MS= -# -# Optional reranking step on memory search results — leave both -# RERANK_BASE_URL and RERANK_MODEL unset to search without reranking (the -# hub refuses to boot if only one of the pair is set, since a half-wired -# reranker can never work). Once both are set, a reranker outage degrades -# search quietly rather than breaking it — that's by design, not a bug. -# There's no local install for a reranker; the supported shape is a Text -# Embeddings Inference server, most easily run in Docker: -# docker run -d -p 8081:80 \ -# ghcr.io/huggingface/text-embeddings-inference:cpu-latest \ -# --model-id BAAI/bge-reranker-base -# Reranker endpoint base URL. +# Optional reranker (a Text Embeddings Inference server); set both or neither. # RERANK_BASE_URL=http://localhost:8081 -# Reranker model name. # RERANK_MODEL=BAAI/bge-reranker-base -# Reranker API key, if the endpoint requires one. # RERANK_API_KEY= -# Max characters of a candidate document sent to the reranker per call. # RERANK_MAX_DOC_CHARS= -# Timeout in milliseconds for a single rerank request. # RERANK_TIMEOUT_MS= -# Optional Markdown-artifact-to-PDF rendering (@corbits/gotenberg-render) -# through an operator-configured Gotenberg server. Leave unset to keep the -# PDF-render capability off entirely — nothing errors at startup either -# way. Gotenberg has no good native story, so this is Docker (or a remote -# endpoint) the same as the reranker: -# docker run --rm -p 3000:3000 gotenberg/gotenberg:8 +# Markdown-to-PDF rendering through a Gotenberg server. # GOTENBERG_URL=http://localhost:3000 -# Encrypts secrets at rest through Interchange's CredentialCipher seam — -# webhook-trigger signing secrets, and the onboarding OAuth connect state -# (PKCE verifier) sealed between /start and /callback so it survives a hub -# restart in between. 64 hex characters (32 bytes, AES-256): openssl rand -# -hex 32. Required unconditionally — the hub refuses to boot without it, -# with no dev-only unencrypted fallback. The value below is fine for a -# laptop that only you can reach; generate your own for anything else. -CREDENTIAL_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 - -# Seals every principal's per-principal signing key at rest, under its own -# key so it rotates independently of CREDENTIAL_ENCRYPTION_KEY. 64 hex -# characters (32 bytes, AES-256): openssl rand -hex 32. Required -# unconditionally, same as CREDENTIAL_ENCRYPTION_KEY above. -PRINCIPAL_KEY_ENCRYPTION_KEY=1111111111111111111111111111111111111111111111111111111111111111 - -# Maximum size, in bytes, of a tool-package tarball the package-registry -# PUT endpoint accepts. Defaults to 10 MiB when unset. -# HUB_MAX_TARBALL_BYTES=10485760 - -# Artifacts plane (@corbits/artifacts) uses DATABASE_URL too — different -# schema only (`artifacts.*` FKs into public.tenant/principal). Nothing to -# configure here; `bun run dev` mounts Library automatically. - -# Exclusive per-workbench sidecar placement (CL-6096, CL-6283, CL-6534). -# Leave unset (the default) and the hub registers the "process" backend -# alone: each exclusive sidecar runs as one apps/sidecar child process on -# the hub's own host, so a single server hosts many chats and workflows -# with no configuration, no container runtime, and no sandbox account. -# -# SIDECAR_PROVISIONERS is a comma-separated list of backend ids to -# register. Setting it replaces the default entirely — list "process" too -# if you want it alongside another backend. Supported ids: -# -# process same host, one child process per allocation (the default). -# No required settings. Process isolation only: sidecars share -# the hub's kernel, filesystem, and user account. -# docker one container per allocation via the local docker CLI. -# Requires DOCKER_PROVISIONER_IMAGE. -# e2b one remote E2B sandbox per allocation. Requires E2B_API_KEY -# and E2B_TEMPLATE (see packages/e2b-sandbox-sidecar/README.md). -# -# Listing more than one id registers all of them; the first listed is the -# default an exclusive placement provisions on. -# -# Adding another backend is: implement SidecarProvisioner in its own -# package (copy packages/process-provisioner — the allocation lifecycle -# itself comes from @corbits/sandbox-sidecar), then add a case for its id -# to buildSidecarProvisioner in apps/hub/src/server.ts. +# Where sidecars run: comma-separated ids, first is the default. +# process (default, no config) | docker | e2b. See docs/sidecar-provisioners.md. # SIDECAR_PROVISIONERS=process # DOCKER_PROVISIONER_IMAGE= - -# Overrides for the "process" backend, both optional. Unset, it spawns -# this repository's own apps/sidecar/src/index.ts with the bun binary -# running the hub — which is why the default install needs no settings. -# Set these to run a sidecar build from somewhere else on the host. -# PROCESS_PROVISIONER_SIDECAR_ENTRY=/opt/workbench/apps/sidecar/src/index.ts -# PROCESS_PROVISIONER_RUNTIME=/usr/local/bin/bun - -# HUB_SIDECAR_WEBSOCKET_URL overrides the ws(s):// address a provisioned -# sidecar dials back to reach this hub. Leave unset to derive it from -# PORT (ws://127.0.0.1:/api/sidecars/ws) — fine when the hub and its -# sidecars can reach each other at that address, which is always true for -# the "process" backend (a child process on the hub's own host). Set this -# explicitly whenever -# SIDECAR_PROVISIONERS includes "docker" or "e2b": a container's own -# "localhost" is itself, not the hub host, and an E2B sandbox is a remote -# machine that needs a publicly reachable wss:// URL or tunnel. +# E2B_API_KEY= +# E2B_TEMPLATE= +# PROCESS_PROVISIONER_SIDECAR_ENTRY= +# PROCESS_PROVISIONER_RUNTIME= +# ws(s):// address sidecars dial back to; required for docker and e2b. # HUB_SIDECAR_WEBSOCKET_URL= - -# SIDECAR_ADAPTER_MANIFEST configures custom Interchange inference adapters -# for a sidecar process, replacing the default manifest wholesale (not -# merging with it). Leave unset (the default) and the sidecar already -# registers @corbits/ollama-adapter for the "ollama" provider key, so a -# seeded Ollama model's per-model num_ctx reaches Ollama with no operator -# configuration. Set this only to point a provider key at a different -# adapter package. The value is a JSON array of -# {"provider","specifier","export"} entries; each specifier must resolve -# from the sidecar's own module-resolution root (an installed package, not -# a bare file path), and every workflow-process child it spawns resolves -# the same manifest. Example replacing the default with a custom adapter -# for the "anthropic" provider key: -# SIDECAR_ADAPTER_MANIFEST=[{"provider":"anthropic","specifier":"@acme/custom-anthropic-adapter","export":"createCustomAdapter"}] - +# JSON array of {"provider","specifier","export"} replacing the sidecar's +# default inference adapter manifest. +# SIDECAR_ADAPTER_MANIFEST= diff --git a/AGENTS.md b/AGENTS.md index 4be4aed2af..eb9a9045df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,9 +6,9 @@ libraries, each mounted once (`@corbits/mailbox`, `@corbits/memory`, `@corbits/artifacts`, `@corbits/webhooks`, an OAuth library on `@corbits/oauth-core`); any other hub mount is cutover debt with a Linear issue, never a pattern to extend. A workbench is a plain -Interchange tenant, nothing more. The web client drives setup itself, as a -converge loop over stock routes (auth → tenant → definitions/credentials/ -grants); the hub never seeds data on a client's behalf. +Interchange tenant, nothing more. The web client sets itself up over stock +routes on start (auth → tenant → definitions/credentials/grants); the hub +never seeds data on a client's behalf. ## Ground rules diff --git a/README.md b/README.md index 5e2e8b5160..6f53afc8a9 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,15 @@ # Corbits Workbench -The multiplayer workspace for humans and AI agents. Workbench is a web -client of [Interchange](https://github.com/faremeter/interchange): it -signs you into a plain Interchange tenant, and chat is mail threads over -`@corbits/mailbox`. The one shipped agent, Myra, creates whatever -workflows, tools, or skills a job needs on the fly. +A multiplayer workspace where people and AI agents share the same threads. +Workbench is a web client for [Interchange](https://github.com/faremeter/interchange): +you sign in to a plain Interchange tenant, chat is mail, and Myra, the one +shipped agent, builds whatever workflows, tools, or skills a job needs. -## Quickstart +## Run it -Requires [Bun](https://bun.sh) >= 1.2 (pinned in `.bun-version`) and a -local [Postgres](https://www.postgresql.org) 17 with the pgvector -extension — `brew install postgresql@17 pgvector` on macOS, or bring up -the same Postgres CI uses: `docker compose -f compose.test.yml up -d`. +Needs [Bun](https://bun.sh) (version in `.bun-version`) and Postgres 17 +with pgvector. `docker compose -f compose.test.yml up -d` gives you the +same Postgres CI uses. ```sh bun install @@ -19,56 +17,35 @@ cp .env.example .env bun run dev ``` -`bun run dev` validates `.env`, applies pending migrations, builds the web -UI if needed, and starts the hub. It signs up a local administrator -(`HUB_ADMIN_EMAIL` / `HUB_ADMIN_PASSWORD`, default -`alice@example.com` / `password123`) through the same auth API the UI -uses — hub boot itself creates no users, tenants, or product data. +`bun run dev` starts the hub and the web dev server, applies migrations, +and signs up a local admin (`alice@example.com` / `password123` unless +`HUB_ADMIN_EMAIL` / `HUB_ADMIN_PASSWORD` are set). Connect an inference +provider from the UI; keys are never read from the environment. -Required env vars (see `.env.example` for the full, commented list): -`DATABASE_URL`, `BASE_URL`, `SESSION_SECRET`, `HUB_DATA_DIR`, -`HUB_ALLOW_GIT_INSIDE_WORK_TREE`, `HUB_STATIC_DIR`. Everything else — -OAuth sign-in, the memory plane's embedding provider, sidecar -provisioners — is optional and documented inline in that file. To run -fully offline, connect the Ollama provider from the UI instead of a -cloud key; see [docs/local-dev.md](docs/local-dev.md). +`bun run reset` wipes local state. It refuses anything but a local database. -To wipe local state (drops the schema and the hub's on-disk asset -directory, refuses against anything but a local database): +## Layout -```sh -bun run reset -``` - -## Repo layout +| Path | Contents | +| ------------- | ---------------------------------------------------------------- | +| `apps/` | `hub` (API), `web` (React client), `sidecar` (execution host) | +| `packages/` | Corbits libraries | +| `tools/` | `@corbits/*` agent tool packages | +| `agents/` | `@corbits/assistant` (Myra) | +| `skills/` | `@corbits/*` skill packages | +| `vendor/intx` | Hand-copied Interchange packages, see [VENDORED.md](VENDORED.md) | -| Path | What lives here | -| ------------- | --------------------------------------------------------------- | -| `apps/` | `hub` (API), `web` (React client), `sidecar` (execution host) | -| `packages/` | Domain packages and Workbench-specific composition | -| `tools/` | Publishable `@corbits/*` agent-tool packages | -| `agents/` | Publishable `@corbits/*` agent packages (`assistant` is Myra) | -| `skills/` | Publishable `@corbits/*` skill packages | -| `workflows/` | Workflow definition packages, deployed as assets | -| `vendor/intx` | Hand-copied Interchange fallbacks; see [VENDORED.md](VENDORED.md) | - -## Development +## Develop ```sh -bun run check # typecheck, lint, fmt:check, test — the full gate -bun run test:e2e # end-to-end smoke tests against a scratch database -bun run fmt # oxfmt write +bun run check # typecheck, lint, fmt:check, test +bun run test:e2e # end-to-end suites, need DATABASE_URL ``` -`test` and `test:e2e` need a reachable `DATABASE_URL`; DB-gated suites -skip locally with a banner and fail loudly in CI. - -More: [AGENTS.md](AGENTS.md), [ARCHITECTURE.md](ARCHITECTURE.md), -[IMPLEMENTATION.md](IMPLEMENTATION.md), [PRODUCT.md](PRODUCT.md). +Conventions live in [AGENTS.md](AGENTS.md). ## License -The application (`apps/` and the rest of this repo) is GPLv2 with the -[AI Exception](GPLv2-AI-Exception.md) — see [LICENSE.md](LICENSE.md). -Libraries under `packages/`, `tools/`, `agents/`, `skills/`, and -`workflows/` are each licensed LGPL-2.1-or-later. +The application is GPLv2 with the [AI Exception](GPLv2-AI-Exception.md); +see [LICENSE.md](LICENSE.md). Libraries under `packages/`, `tools/`, +`agents/`, and `skills/` are LGPL-2.1. diff --git a/vendor/intx/hub-agent/package.json b/vendor/intx/hub-agent/package.json index ee9dca0860..dc148483c6 100644 --- a/vendor/intx/hub-agent/package.json +++ b/vendor/intx/hub-agent/package.json @@ -15,8 +15,7 @@ } }, "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" + "typecheck": "tsc --noEmit" }, "dependencies": { "@intx/harness": "workspace:*",