All notable changes to AI Workbench are documented in this file.
The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning
starting at 0.1.0. Pre-1.0, breaking changes can land in a minor
release — they will be called out under Changed below.
0.5.6 — 2026-07-02
Release metadata + dependency refresh. 0.5.6 moves the project-owned package manifests, lockfiles, CLI/web runtime constants, README badge, docs site metadata, and release-note pointers from 0.5.4 to 0.5.6 so the shipped artifacts agree with the release tag. This release also carries the dependency and GitHub Actions refreshes that landed after v0.5.5. There is no HTTP wire-contract change, no data migration, and no Docker runtime behavior change.
- Refreshed dependency and workflow pins across the monorepo. The root
toolchain, web app, TypeScript runtime, docs site, and GitHub Actions
workflows have been updated to the current Dependabot-reviewed versions on
main. - Aligned release-facing version metadata at 0.5.6. The previous v0.5.5 tag shipped without moving the manifests off 0.5.4; this release corrects the version fields and visible app/CLI version strings before cutting the new tag.
0.5.4 — 2026-06-10
Beta-feedback release. Every change in 0.5.4 responds to first-wave beta feedback (thanks, David Jones-Gilardi): bulk document deletion and parallel ingest in the Knowledge Base Explorer, and three fixes that make the Docker quickstart work on the first run. One additive wire-contract change (the new bulk-delete endpoint); no breaking change and no data migration.
-
Bulk select + delete documents in the Knowledge Base Explorer. Deleting documents was strictly one at a time — one trash click, one dialog, one round trip each. The document table now has an opt-in checkbox column with a select-all header scoped to the visible/filtered rows (a filter narrows the blast radius, never widens it), a "Delete selected (N)" action bar, and a single confirmation for the whole batch. Server-side, the new
POST /api/v1/workspaces/{ws}/knowledge-bases/{kb}/documents/bulk-deleteendpoint accepts{ documentIds: string[] }(1–100 ids per call; larger selections are paged client-side). Each id runs the same RLAC mutation gate, the same chunk cascade, and writes the same audit record as the single DELETE — bulk is not a side door around row-level policy. Per-id failures (not_found/policy_denied/delete_failed) are reported in afailedarray without aborting the rest of the batch, and partial failures surface in the web app as a warning toast. -
Bounded parallel drain in the ingest queue. The ingest queue dialog drained one file at a time even though the runtime already bounds concurrent ingest jobs server-side (
runtime.maxConcurrentIngestJobs, default 4), so large batches paid one-job-at-a-time wall-clock. The queue now keeps up to N jobs in flight (a "Parallel ingests" picker offers 1/2/4/8, defaulting to 4 to mirror the server semaphore;1restores the old sequential behavior for rate-limited embedding providers), and every running row streams its own live progress. Uploads and duplicate/name-conflict prompts stay serialized, and a failing file still fails independently without tanking the batch.
-
Docker first run: workspace creation no longer fails with
EACCES. The image runs as the non-rootnodeuser but never created the/var/lib/workbenchmount point, so Docker initialized theworkbench-datanamed volume root-owned and every control-plane write — including the setup wizard's managed.env— failed on day one. The Dockerfile now pre-creates the directory owned bynode, so the volume inherits writable ownership. -
Ollama is reachable from the Docker quickstart. The Ollama base URL was hardcoded to
http://localhost:11434/v1— inside the container,localhostis the container itself, never the host where Ollama runs. The runtime now resolves an env-aware default (OLLAMA_BASE_URL; bare origins get/v1appended),docker-compose.ymlmapshost.docker.internalto the host gateway (covering Linux Engine, not just Docker Desktop) and defaultsOLLAMA_BASE_URLto it, and the LLM service form gains an optional Endpoint base URL field so the target is visible and editable per service. Transport errors now name the endpoint they tried, with a container-vs-host hint when it'slocalhost. See the new "Ollama on the host" section indocs/docker.md. -
Mock workspaces ingest out of the box. New workspaces — including
kind: "mock"— were seeded with only the NVIDIA embedding service with no credential attached, so a mock workspace's first ingest failed with400 embedding_unavailable. Mock workspaces now seed the credential-free mock embedder, which the mock vector driver accepts, so the zero-credential demo flow works end to end.
0.5.3 — 2026-06-04
Security tooling + dependency maintenance. No HTTP wire-contract change and
no data migration — the runtime, API, and web app behave exactly as they did in
0.5.2. This release adds a trust gate for MCP tool definitions and keeps PDF
ingestion working across a major pdfjs-dist upgrade.
- MCP tool-surface trust gate (
toolprint). AI Workbench both hosts an MCP server (/api/v1/workspaces/{id}/mcp) and lets agents call external MCP servers as tools, and an agent reads each tool's description + input schema to decide what to do — so a server that silently rewrites a tool definition (a "rug-pull") can redirect an agent. The MCP tool surface of our own server and of the external servers we trust (.toolprint/mcp.json) is now hashed and pinned into a committedtoolprint.lock; every change is diffed against that pin, and drift — a changed description or schema, an injected instruction, a leaked secret — fails CI (.github/workflows/toolprint.yml). Run it locally withnpm run security:mcp(-- --pinto re-pin after an intended change). The scanner only lists tools; it never executes one. Seedocs/mcp-trust.mdand the new "MCP tool-surface trust" section inSECURITY.md.
- PDF ingestion under
pdfjs-dist6. Thepdfjs-dist5 → 6 upgrade removedPDFDocumentProxy.destroy(); the native PDF extractor called it during teardown, which threw after a successful parse and turned everyPOST /ingest/filePDF upload into a500. The extractor now tears down via the loading task (loadingTask.destroy()), which works on both 5 and 6; text extraction itself was unaffected.
pdfjs-distupgraded 5.7.284 → 6.0.227, alongside grouped Dependabot updates to the TypeScript-runtime, web, and GitHub-Actions dependency sets. ThetoolprintCLI pinned by the trust gate tracks 0.1.1, which classifies a rug-pull ashigh(so the default--fail-on highgates on drift) and adds--header/--bearerfor authenticated remote targets.
0.5.2 — 2026-06-03
Maintenance release. A housekeeping pass on the 0.5 Enterprise Access Control line with no wire-contract change and no data migration. It drops a metric that never carried a signal and reconciles the contributor codemaps with the shipped 0.5.x code; the runtime, HTTP API, and web app are otherwise unchanged.
- The
workbench_chat_stream_tokens_totalmetric. This counter was registered on the/metricsendpoint but never incremented anywhere, so it always exported0— a flat-zero "Stream tokens / sec" Grafana panel and a doc claim with nothing behind it. Populating it faithfully would need a prompt-vs-completion token split the chat abstraction does not surface (ChatCompletion/ChatStreamEventcarry only a totaltokenCount, and the OpenAI-compatible provider narrows the APIusageobject tototal_tokens), so the dead counter is removed rather than left misleading. It is gone from the runtime metrics registry, the bundled Grafana dashboard (docs/observability/grafana-workbench.json),docs/production.md, anddocs/api-spec.md. If you scrape it, drop the panel or alert — no replacement metric is emitted.
- Contributor codemaps reconciled with the shipped code.
docs/CODEMAPS/*had drifted from the 0.5.x source: a route undercount is fixed, misleading "Generated:" headers are relabeled, several stale routes and table names are corrected, thepackages/aiw-clipackage and the Auth service boundary are added, the scoped-auth (0.5.0) decision is recorded, and the RLAC "(prototype)" labels are dropped now that it ships enforced. These are contributor-facing docs only — no user-facing behavior changes.
0.5.1 — 2026-06-01
RLAC "view as" in the web app. A fix-and-polish release on the 0.5.0 Enterprise Access Control line. There is no wire-contract change and no data migration — it closes a UX dead-end where enabling RLAC made knowledge bases unreadable from the web app under the default auth-disabled posture, and adds a discreet control for previewing a knowledge base as any principal.
- Enabling RLAC no longer dead-ends knowledge-base reads in the web app.
With
auth.mode: disabled(the default / quickstart posture), the SPA carried no token and no principal, so every document read against an RLAC-enabled knowledge base returned401 policy_principal_requiredthe instant the KB was opened — even though flip-on bootstrap had already created the defaultadminprincipal. The runtime's disabled-mode principal resolver and the flip-on design both assumed the web app would send anx-view-as-principalheader, but the picker that sets it was never ported intoapps/web. The web API client now sends that header on workspace-scoped requests, defaulting to theadminprincipal (universal read) when no auth token is present, so an RLAC-enabled workspace is immediately usable again. An explicit "view as" selection always wins; when a bearer token is present the header is omitted and the runtime derives the principal from the token exactly as before.
- A discreet "view as principal" control on the knowledge-base explorer.
When RLAC is enabled and the app runs without an auth token, a small icon in
the explorer's action row lets you browse the knowledge base as any principal
to preview exactly what they can see. It defaults to
admin(sees all) and turns into an accent chip naming the principal while you're impersonating someone else; switching refetches the document list under that identity. The control is hidden in token-authenticated deployments, where the principal is derived from the token and the header is ignored.
docs/rlac.mdnow documents the shipped web-app view-as behavior (the defaultadminheader plus the explorer control) instead of the prototype picker it previously described.
0.5.0 — 2026-06-01
"Enterprise Access Control." 0.5.0 turns AI Workbench's access-control story from prototype + coarse roles into enforced, fine-grained, and audited — so it is safe to run multi-team retrieval workloads where "who can see what" holds at the data plane. Three must-have features land together: row-level access control (RLAC) enforced on every read path, fine-grained API-key scopes, and access-controlled agent MCP tool-calling.
There is no breaking wire-contract change and no required data migration — fine-grained scopes are additive (coarse keys keep working), and RLAC chunk visibility backfills automatically. See Migration below before enabling RLAC on an existing deployment.
- Row-level access control is now enforced on every read path — including
agent chat retrieval. RLAC policies were previously enforced on the REST
document routes only; an agent's RAG retrieval bypassed them entirely. Now
search_kb,list_chunks,get_document, document listing, and the Astradata_apitool all compose the compiled policy filter, so a principal's agent can only retrieve what that principal can see. Chunks are stamped with their document'svisible_toat ingest, so the data plane matches the control plane — the central bug that made RLAC-on search return nothing. - RLAC admin UI. A new Access Control card (RLAC on/off), Principals panel
(CRUD), and Policy Audit panel in workspace settings — the admin surface
docs/rlac.mddescribed but that did not previously exist. - Fine-grained API-key scopes. Keys can be minted with narrow scopes
(
read:content,read:chat,read:audit,write:ingest,write:kb,write:services,write:agents,manage:keys,manage:access,manage:workspace,tools:invoke) alongside the coarseread/write/managetiers. A "Custom (advanced)" scope picker in the create-key dialog, tier-colored scope chips, and a newaiw key create|list|revokeCLI command (with--rolepresets and repeatable--scope) expose them. - Agent external MCP tool-calling, access-controlled. Agents can call tools
on registered external MCP servers under a
tools:invokegrant, enforced per-call — a call without the scope is denied and audited, never executed. Save-timetoolIdvalidation rejects an agent that references an unresolvablemcp:/native:/astra:tool (422 agent_tool_unresolved). The agent form groups tools by server, shows required arguments, and warns about saved tools that no longer resolve. Per-server tool discovery is memoized with a short TTL to keep agent turns and form loads fast. - Tool-invocation and scope-denial audit detail.
tool.invokeaudit rows carrysourceandmcpServerId; a scope-denied API request records therequiredScope. - New cross-runtime conformance scenarios pinning the RLAC principal/policy contract, fine-scope mint/normalization, the external MCP-server registry lifecycle (incl. SecretRef enforcement), and the available-tools catalog shape.
- Scope checks use hierarchical containment instead of exact-string match.
A held scope
Xgrants a required scopeYwhenY === XorYis nested underX(so a coarsewritekey grantswrite:ingest). This is what lets the coarse tiers stay supersets of the new fine scopes with no data migration — every existing key keeps exactly the access it had. The MCP JSON-RPC façade adopts the same containment check, replacing its own exact-match copy of the gate. - A single shared in-memory Data API filter interpreter now backs the mock
driver, the document-list path, and the mock-Astra conformance server, so
$or/$andvisibility filters evaluate identically across them — closing a silent mock-vs-production semantic drift. - Docs rewritten to match shipped reality:
docs/rlac.md,docs/auth.md(scope taxonomy + containment table + migration notes),docs/audit.md(new fields), and the stale client-side-MCP line indocs/roadmap.md.
- DNS-resolution SSRF parity for external MCP server URLs. Beyond the
literal-host check, an MCP server's hostname is resolved and every resolved
address re-validated against the same egress policy, so a benign-looking name
that resolves to
169.254.169.254— or, when private egress is locked down, an internal IP — is refused before any connection is opened. On-prem deployments that allow private egress can still register internal MCP servers. - Untrusted MCP servers can no longer bloat the model prompt. An advertised tool description is length-capped and an oversized advertised input schema is dropped to a permissive object, bounding the metadata an external server injects into the tool manifest each turn. Tool descriptions render as inert text, never HTML.
- Enabling RLAC on an existing workspace: new ingests are tagged with their
visibility automatically, and existing chunks are re-tagged from each
document's
visibleTowhen you flip RLAC on (and via the backfill script). Until a workspace is backfilled, an RLAC-on search reflects only re-tagged chunks — flip on, let the backfill run, then verify. - Fine-grained scopes are additive — no action required. Existing coarse
read/write/managekeys keep working unchanged; the default for a new key is still["read", "write"]. Mint narrower keys only where you want them. - Two deliberate behavior changes to note: reading the policy-audit log
now requires
manage:access(a coarsemanagekey still grants it), and an agent calling an external MCP tool now requirestools:invoke(a coarsewritekey grants it, so existing write-capable keys are unaffected). Chat message sends remain ungated for read-shaped keys.
A hardening-and-correctness release on the 0.4.x line. There is no HTTP wire-contract change and no data migration — this release completes the control-plane delete cascade, adds a self-healing cross-partition cascade with an opt-in orphan reconciler, hardens the rescue/setup surface, and keeps secrets out of structured logs.
- Self-healing control-plane cascade + opt-in orphan reconciler. On the
Astra backend, deleting a workspace now removes its child rows
children-first / parent-last. A partial failure leaves the workspace row in
place and returns
500 cascade_incomplete(a new error code) so the idempotent cascade finishes on retry instead of stranding orphans. A newreconcileOrphans()pass — opt-in viacontrolPlane.reconcileOrphansOnStart— sweeps pre-existing orphaned rows at startup.
- Unified row-level access control (RLAC) defaulting behind a single
resolveRlacDefaultspath: a document's owner defaults are applied independently ofvisibleTo. Authorization behaviour is unchanged (security-reviewed) — this removes a divergent code path, not a rule. - Internal modularization sweep (no behaviour change). Continued splitting
large modules — the multipart ingest parser moved to
routes/api-v1/ingest-file-form.ts, and the Playground code generation moved into its own tested module. - The OpenAPI document now describes the job-progress SSE endpoint.
GET .../jobs/{jobId}/eventswas served but missing from/api/v1/openapi.json; it is now registered (path params, theLast-Event-IDresume header, and thetext/event-streamresponse), so the generated web API types and any client built from the spec cover the async-progress contract. No behaviour change.
- Workspace delete now cascades
mcpServers,principals, andpolicyAudit. These three child collections were previously left behind when a workspace was deleted; they are now removed on every backend (memory, file, SQLite, Astra). Policy-audit rows are purged rather than retained — they become unreadable once their workspace is gone, so keeping them would only strand inaccessible rows. - Reliability hardening.
/readyzis now bounded by a deadline so a slow dependency can't hang the readiness probe; chat requests carry a request timeout; and the web app guards against a malformed JSON response instead of throwing. - Bounded prompt-history read in agent dispatch. Assembling a prompt no longer reads unbounded conversation history, so long, tool-heavy conversations stop re-scanning the full transcript on every turn.
- Gated the rescue/setup mutation routes. The setup/rescue surface's mutating routes now sit behind the setup auth-gate, and the bootstrap-token comparison is constant-time (timing-safe), removing a token-guessing side channel.
- Secret redaction in structured logs. The logger now redacts secret- and token-shaped values so credentials don't leak into structured log output.
- Hardened the release workflow's supply chain. Every GitHub Action in
release.yml— the workflow that publishes the runtime image to GHCR and cuts the GitHub Release — is now pinned to a full commit SHA, and each job declares least-privilegepermissions:so the build/test jobs no longer inheritpackages: write/id-token: write.
Hardening continues on the 0.4.x line, with one new capability:
store-level keyset pagination for the chat surface. The wire shape is
unchanged ({ items, nextCursor }) and there is no data migration —
this release changes how a conversation's history is read, tightens a
fetch-tool SSRF boundary, and makes shutdown cleaner.
- Keyset pagination for chat history. An agent's conversations
(
GET .../conversations) and a conversation's messages (GET .../conversations/{c}/messages) now page with an opaque keyset cursor instead of an offset, pushed down into all four control-plane backends (memory, file, SQLite, Astra) so the runtime stops materialising the whole conversation on every list call — SQLite uses a realpk-indexed partition scan. The model's prompt assembly and the MCP façade continue to read full history; paging never truncates what the model sees. The user-visible message listing still filters internal tool-call scaffolding, so a page may be shorter thanlimit(or empty) with a non-null cursor — drain on the cursor, not on an empty page. Seedocs/api-spec.md.
- Chat list cursors are now keyset, not offset. They remain opaque and
are not stable across deploys: a client mid-pagination across an
upgrade gets
400 invalid_cursorand restarts from the first page. Unlike an offset, a row inserted or deleted above the cursor no longer shifts the caller's position. The bounded control-plane list surfaces (workspaces, services, knowledge bases, API keys, agents, …) keep their existing offset cursors. - Conservative dependency refresh across all workspaces (patch/minor
only);
npm auditreports 0 known vulnerabilities.
- Cleaner graceful shutdown with active job streams. A long-lived
.../jobs/{id}/eventsSSE stream no longer holds its connection open through the shutdown drain window: onSIGTERMthe stream ends so the client'sEventSourcereconnects (to a surviving replica or after restart) and resumes viaLast-Event-ID, andserver.close()finishes promptly instead of waiting out the timeout.
- Closed a DNS-based SSRF hole in the
native:fetchagent tool. The tool's URL is model-supplied (so reachable via prompt injection) and previously range-checked only literal-IP hosts — a DNS name that resolved to169.254.169.254, a10.xaddress, loopback, etc. slipped through. It now resolves the host and validates every resolved address against the same blocked ranges, failing closed (a host that won't resolve, resolves to nothing, or resolves to any blocked address is refused).safeFetch'sredirect: "error"still bounds the residual sub-second rebind window.
Hardening + docs/UX polish on top of 0.4.0. No API contract changes and no data migration — this release simplifies surfaces and fixes a bug, it doesn't change the wire model. The headlines are a unified agent editor and a deliberately simpler access-control UI.
- Unified agent editor. All three agent create/edit surfaces — the
workspace overview, the dedicated Agents page, and the chat
zero-state — now share one form and dialog (
AgentFormDialog, which owns the tool-catalog fetch so no surface can omit it). The tool picker and every other field are available everywhere. When a workspace has no external tools the picker shows an empty-state callout linking to MCP settings, and each agent card/row carries an "all tools / N tools" scope badge. - Published documentation site at
https://datastax.github.io/ai-workbench/ (VitePress → GitHub Pages),
linked from the README, plus a public
docs/whats-new-0.4.0.mdnarrative.
- Simplified access control (UI only). Role-based API keys
(Viewer / Editor / Admin) are now the single access-control surface in
the web app. The advanced row-level access-control prototype — raw
principals, per-row policies, and the "view as" picker — is no longer
surfaced in the UI. It remains fully available through the HTTP API
and the
aiwCLI for advanced operators; no backend or schema change. - CLI polish.
aiw principalandaiw policyare hidden fromaiw --help(still fully functional for scripting); the--workspaceflag description and the "--workspace is required" error are consistent across commands;aiw login --oidc --output jsonnow emits a JSON result envelope for scripting parity. - Leaner README focused on getting an end user running, with the
dev/contributor detail consolidated into
CONTRIBUTING.md. - Consistent error messaging in the web UI — workspace, knowledge-
base, document, and service panels now route load errors through the
shared
formatApiErrorhelper. - Unified
vitestversions across workspaces and added aformat:checkgate tonpm run check.
- Agent tool selector missing on the workspace-overview dialogs. Creating or editing an agent from the workspace overview silently hid the Tools section because that dialog never fetched the tool catalog; the shared dialog now owns the fetch, so tools appear on every surface.
- The
aiwCLI version constant was stale at0.3.0(missed in the 0.4.0 cut); it now reports the correct version.
- The web UI's principals panel, policy-audit panel, "view as" picker, and per-document visibility editor (the RLAC prototype surfaces). The underlying API and CLI commands are unchanged.
Headline: two flagship capabilities — agent tool-calling and
role-based access control (RBAC) — on a security + reliability
hardening pass. Agents can now call tools mid-conversation in a
bounded multi-step loop; every /api/v1/* surface is gated by coarse
roles (viewer / editor / admin) with a new manage tier for admin-only
operations. This is a breaking release for write-scoped API keys
that performed admin actions — see Migration.
- Agent tool-calling. Agents resolve a per-agent allow-list
(
agent.toolIds) and call tools in a multi-step loop (cap 6). Tool sources: the built-in workspace tools; the workspace's external MCP servers (a new per-workspace registry + REST CRUD at/api/v1/workspaces/{w}/mcp-servers+ an in-runtime MCP client); native toolsnative:fetch(SSRF-guarded, with a timeout, response-size cap, and content-type allow-list) andnative:web_search(pluggable, off until configured); and a read-onlyastra:data_apitool. EmptytoolIdsgrandfathers in all built-in tools; external / native / Astra tools are opt-in. Every tool call is bounded by a timeout + output cap and recorded as atool.invokeaudit event (arguments omitted). Code execution is a documented non-goal this release. - Tool-calling UI. Inline expandable tool-call / result cards in the
chat transcript, a source-grouped tool picker in the agent form, a
GET /available-toolscatalog endpoint, and an MCP-servers settings panel. - RBAC. Coarse roles
viewer | editor | adminmap to the privilege scopesread | write | manage. A newmanagescope gates admin-only surfaces (API keys, RLAC principals + policy, workspace delete). Roles unify with RLAC principals (a principal carries arole); OIDC subjects map to a role via the opt-inauth.oidc.roleMapping(group/claim → role, with aviewerfloor). API keys are issued with explicit scopes / a role, enforced across HTTP routes, MCP tools, theaiwCLI, and the web UI (useRolegating). A self-maintaining route-inventory guard proves every mutating route is gated. - SQLite control-plane driver. A
driver: "sqlite"control-plane + job-store backend for durable single-node deployments — row-level WAL writes instead of thefilebackend's whole-file rewrite. - Job durability for all kinds. The async-resume path (previously
ingest-only) is generalized: a kind-tagged
inputSnapshot+ aJobKind → resumeregistry let the orphan sweeper replay any registered job kind idempotently. - Streaming robustness. A shared SSE helper guarantees exactly one
terminal event; the job-events stream supports
Last-Event-IDresume; a client disconnect aborts the in-flight LLM call; a dropped stream still persists a terminal assistant row. - MCP write tools —
create_knowledge_base,delete_knowledge_base, andrun_agentjoin the MCP façade (KB writes requirewrite). - OIDC device-flow login —
aiw login --oidc(RFC 8628) with runtime proxy endpoints; the IdP client secret stays server-side. - In-app "What's new" modal, auto-opening once per
APP_VERSION, plus discoverability tooltips on commonly-missed operator actions. - Chat-surface conformance scenarios (CRUD + a deterministic SSE tool-call happy path), closing the last gap in the cross-runtime contract.
- Secret rotation guide (
docs/auth.md), an expanded secret scanner, and a wire-leak test asserting no resolved secret crosses the API boundary.
managescope split out ofwrite(breaking). Admin-only operations — API-key issuance / revocation, RLAC principal + policy management, and workspace deletion — now require themanagescope (anadminrole) instead ofwrite. See Migration.- RLAC audit-log contract is stable.
PolicyAuditRecord,PolicyAction, andPolicyDecisionare committed as a stable public contract (PolicyAuditRecordV1) for SIEM ingestion; additive changes stay non-breaking. aiwCLI parses the real wire shapes ({ items, nextCursor }, resource-specific ids, bare-array/search), gains--top-k/--hybrid/--rerank, and translates auth mismatches + 401s into actionable guidance.- Job records carry a generalized
inputSnapshot(back-compat reads of the legacy ingest snapshot). - Conservative dependency refresh across all workspaces; 0 known vulnerabilities.
- The dead Stage-2 MCP-tool scaffold (
McpToolRow/MCP_TOOLS_*/ the unmountedtoWireMcpToolserde), superseded by the MCP-server registry above.
- Write-scoped API keys lose admin access. A key minted before
0.4.0 carries
["read", "write"](aneditor). It can no longer issue / revoke API keys, manage RLAC principals or policy, or delete a workspace — those now return403 forbidden(missing scopemanage). Re-mint an admin key (["read", "write", "manage"]) for those operations. OIDC and bootstrap subjects are unaffected (unscoped); setauth.oidc.roleMappingto assign OIDC users a role. - Agent tools beyond the built-ins are opt-in. Existing agents
(empty
toolIds) keep every built-in workspace tool. To grant an external-MCP / native / Astra tool, add its id to the agent'stoolIds. - No data migration required. Legacy job snapshots, principal rows
(defaulting to
viewer), and API-key scopes all back-compat on read.
Tests: 1,686 runtime + 462 web + 198 CLI passing on a green typecheck, lint, and cross-runtime conformance across all packages.
0.3.0 — 2026-05-28
Headline: HuggingFace is retired; chat and embeddings are unified on
the OpenAI-compatible wire protocol. One adapter now serves three
providers — OpenRouter (hosted default, one key → 300+ models with
standardized function calling), OpenAI (direct/BYOK), and
Ollama (local, no credential, for air-gapped installs). This is a
breaking release for anyone with a stored provider: "huggingface"
LLM service or a HUGGINGFACE_API_KEY-based config — see Migration
below.
- Provider strategy: OpenRouter + Ollama, not HuggingFace. The
runtime's chat path was wired specifically to HF (an HF-only adapter,
HF-only routability probe, HF-only model picker). 0.2.1's release
notes were largely HF firefighting — symptoms of HF Inference
Providers being an awkward fit (models silently become unroutable,
uneven function-calling support). All three wired providers are now
OpenAI-compatible and dispatched through a single
OpenAIChatService+ provider registry: a provider is just a base URL + an optional credential + a label. - Default chat model + credential.
chat.modeldefaults toopenai/gpt-4o-mini(an OpenRouter slug, wasopenai/gpt-oss-20b);chat.tokenRefdefaults toenv:OPENROUTER_API_KEY(wasenv:HUGGINGFACE_API_KEY); a newchat.provider(openrouter|openai|ollama, defaultopenrouter) and nullablechat.baseUrlare added (config/schema.ts). - Seed + managed-env allow-list. The auto-seeded default LLM
service now targets OpenRouter
(
control-plane/default-services.ts); the/setup/envallow-list swapsHUGGINGFACE_API_KEYforOPENROUTER_API_KEY+OPENAI_API_KEY(setup/managed-env.ts). - ZDR-only by default. OpenRouter requests carry
provider.data_collection: "deny"so prompts route only to zero-data-retention upstreams; operators opt out with a single global flag. Prompt logging is never enabled by default.
- Live model catalog. New
GET /api/v1/llm-models(route + catalog) proxies OpenRouter/models(filtered to tool-calling-capable models, a curated "recommended" subset surfaced first) or a local Ollama server's/models, with a curated static fallback so the picker is never empty offline. TheLlmServiceFormpicker is now driven live from this endpoint. The endpoint is not workspace-auth-scoped, so itsbaseUrlquery param is validated through the same SSRF guard (EndpointBaseUrlSchema) as service endpoints. - Ollama + OpenRouter embeddings. When Astra
$vectorizeisn't configured, embeddings run through the same OpenAI-compatible client foropenrouter/ollama/openai/cohere(embeddings/langchain.ts); Ollama needs no credential. Each embedding service declares its model's native vector dimension (the Ollama seed pinsnomic-embed-text→ 768, which can't be truncated); when a returned vector doesn't match the declaredembeddingDimension, the embed call now fails with an actionable error naming the exact size to set (and to create the KB collection at), instead of a generic mismatch. - Settings: per-field "Configured" indicators.
/settingsnow shows a green ✓ Configured marker next to each credential that already resolves in the runtime environment, backed by a newmanagedEnv.configuredKeysfield onGET /setup-status(the value itself never crosses the wire); configured secret fields hint that a blank input keeps the current value (SettingsPage.tsx). - Agent cards show their bound LLM model. The workspace overview's
agent cards render a
model …chip — mirroring the KB cards' embedding chip — resolving the agent'sllmServiceIdto its model id, ordefaultwhen it inherits the workspace chat default (WorkspaceDetailPage.tsx). - Browser tab title now reads
AI Workbench | IBM.
- HuggingFace chat adapter and SDK. Deleted
runtimes/typescript/src/chat/huggingface.tsand removed the@huggingface/inferencedependency. The HF-specific routability/chat-model probe is replaced by an OpenRouter-aware check (a model is valid when it appears in/modelswithtoolsinsupported_parameters); Ollama models are accepted without probing.
- Stored
provider: "huggingface"LLM services now fail closed. An agent bound to such a service raises422 llm_provider_unsupportedat send time (not a 500) with an actionable message; recreate the service againstopenrouter,openai, orollama. Regression-locked intests/chat/agent-resolution.test.ts. - Swap the env var. Replace
HUGGINGFACE_API_KEYwithOPENROUTER_API_KEY(or setOPENAI_API_KEYfor direct BYOK, or pointchat.provider: ollamaat a local server for offline use). Paste the new key at/settingsand the runtime restarts and reconnects.
Tests: 1,403 runtime + 434 web passing on a green typecheck and lint across both packages. Docs (configuration, api-spec, agents, docker, telemetry, roadmap, codemaps) updated to describe the new provider model.
0.2.1 — 2026-05-27
Headline: RLAC on Documents graduates from Preview to GA, with
new aiw principal and aiw policy CLI surfaces and a wider
test-coverage sweep behind it. No public API or schema changes;
safe upgrade from 0.2.0.
-
Default seeded LLM service is HuggingFace. Every freshly-created workspace used to be auto-seeded with an
openai-gpt-4o-miniLLM service (control-plane/default-services.ts), which pushed operators toward an OpenAI key just to try the out-of-the-box experience even though the runtime's defaultchat.tokenRefalready pointed atHUGGINGFACE_API_KEY. The seed is nowhuggingface-gpt-oss-20bpointing atopenai/gpt-oss-20b(matches the runtime's default chat model and the wizard's managed-env allow-list), so pasting a HuggingFace token at/settingslights up agent chat with zero LLM-service edits. HF doesn't expose native function calling, so the agent dispatcher falls back to the retrieve-and-answer flow described inchat/agent-dispatch.ts— tools still execute, just not via a function-call protocol. Existing workspaces are unaffected (only seeded on POST). -
LLM-service form: popular-model picker + Other (custom).
LlmServiceFormused to be a free-form<Input>where operators had to remember the exact HF model slug (e.g.openai/gpt-oss-20b). It's now a<Select>with four curated HuggingFace defaults — GPT-OSS 20B (default), GPT-OSS 120B, Qwen3 32B, Llama 3.3 70B Instruct, all currently served by the HF Inference Providers router — plus an Other (custom)… row that reveals the free-form input for any other model name. Picking a popular row also pre-fills the provider and a sensiblemaxOutputTokens. Edit mode renders the free-form input automatically when the service points at a non-popular model (existing services keep working unchanged). 3 new tests (LlmServiceForm.test.tsx) cover the picker + Other branch + edit-mode pre-fill, plus a Radix-hasPointerCapturejsdom polyfill in the shared test setup (src/test/setup.ts) so other Select-driven tests work without per-file workarounds. -
Chat is default-on. Previously, omitting
chat:fromworkbench.yamlleft chat disabled and every agent send route returned503 chat_disabled. The default flips: a fresh install boots withchat.enabled: true,tokenRef: env:HUGGINGFACE_API_KEY, and the canonical HF defaults (src/config/schema.ts). When the env var isn't set, the existing degraded path applies unchanged — preflight is advisory,buildChatServicereturns null with awarnlog, and the agent send routes still return503 chat_disableduntil the operator pastes a token via/settings(which writes the managed dotenv file and triggers a restart). To opt out of chat entirely, add the new single-field block:chat: enabled: false
Explicit
chat:blocks in existing configs continue to work — the newenabledfield defaults totrue, so omitting it preserves prior behavior. 4 new tests (tests/config.test.tstests/chat/factory.test.ts) lock the four branches: default-on, explicit opt-out, unresolved token (bootstrap path), and the healthy resolve.
- HuggingFace agents can actually call tools. The HF chat adapter
(
src/chat/huggingface.ts) used to ignore the agent's advertisedtools[]and never parsetool_calls, so a tool-using agent (e.g. Bobby) on an HF-backed model — whose persona prompt nameslist_kbs,search_kb,count_documents, … — emitted its intended calls as a plain-text code block that the dispatcher couldn't execute, then returned that text as the answer. The adapter now forwards the OpenAI-compatibletools[]+tool_choiceand parses the model's structuredtool_calls(bothcompleteand streaming), threading assistant tool-call turns androle: "tool"results back through the prompt — so the dispatcher's list-KBs → search → answer loop works the same way it does on the OpenAI adapter. The defaultopenai/gpt-oss-20bis served for tools; the auto-seeded service now advertisessupportsTools: true. New unit coverage (tests/chat/huggingface.test.ts). - Default chat model is actually routable. A HuggingFace model
can be unusable for chat in two distinct ways, and both used to
surface only at send time. (1) Not a chat model — HF's router
stopped serving
mistralai/Mistral-7B-Instruct-v0.3for theconversationaltask (… is not a chat model). (2) Not routable — the Inference Providers router only serves models a provider has onboarded, soQwen/Qwen2.5-7B-Instruct(onboarded by no provider) failed withnot supported by any provider you have enabled. The runtime default chat model, the auto-seeded LLM service, the wizard managed-env allow-list, and the form's popular-model menu all now useopenai/gpt-oss-20b— the widest-served ungated small chat model on the router (live across groq, novita, together, fireworks, and more) — so a fresh token with default provider settings routes out of the box. - Config-time chat-model guard. Creating or updating a
HuggingFace LLM service now runs a fail-open probe
(
src/chat/model-probe.ts) before persisting: a singlemax_tokens: 1chat completion that, on a definitive signal, rejects the save so a bad custom model (picked via Other (custom)…) is caught at configuration time instead of when an agent first replies —422 llm_model_not_chatwhen the model is served but not for chat,422 llm_model_unavailablewhen no provider serves it. The probe is strictly fail-open — it only runs when a credential resolves, and any transient failure (network, rate limit, auth, cold-start) lets the save through rather than blocking it. New unit coverage for the classifiers (tests/chat/model-probe.test.ts) and route-level reject / allow / skip / PATCH-re-probe coverage (tests/llm-services.test.ts).
-
Rescue-mode boot. Control-plane init throwing at startup (typo'd Astra endpoint resolving to
ENOTFOUND, revoked token triggering 401, region hibernating past the resume window) used to take down the whole process: the operator would seestartup failedin logs and be left with no in-app remediation for credentials they'd just entered through/settings. The runtime now wraps control-plane init inmain()atroot.tsand, on failure, pivots to a minimal HTTP server (src/rescue/app.ts) that:- serves the SPA so
/settingsactually renders, - reports the classified failure via
GET /setup-status's new optionalbootError: {code, message}field (control_plane_dns_unresolvable,control_plane_unauthorized,control_plane_unreachable,control_plane_forbidden, or the catch-allcontrol_plane_unavailable), - accepts
POST /setup/envwith no auth gate (rescue mode is open by definition — no privilege boundary exists when the control plane is down), - triggers
POST /setup/restartso the container restart policy brings the runtime back with the corrected credentials, - returns
503 control_plane_unavailableon every/api/v1/*call so callers see a clean failure instead of a 404, and - returns 503 on
/healthzand/readyzso external probes know the runtime is degraded.
The SPA cooperates: the new
SettingsPagerenders a red rescue-mode banner with a tailored remediation hint per error code, andAppShellredirects users from data routes to/settingswheneverbootErroris present so the dead-end is broken on first paint. 12 new runtime tests (tests/rescue/app.test.ts)- 1 new SPA test lock the contract.
- serves the SPA so
-
Header nav: icons-only + reorder. The header
<nav>was half text-half icons (API docswas the lone text link). Reorder toTheme · API docs · Settings · What's new · UserMenu, all rendered as icon buttons (BookOpen for API docs, Cog for settings) witharia-labels and tooltips for accessibility. No behavior change. -
Runtime settings page (
/settings). The first-run onboarding wizard captures Astra and HuggingFace credentials into a managed dotenv file and disappears once setup completes. There was no in-app surface to update those credentials afterwards — a missingHUGGINGFACE_API_KEYleft chat at503 chat_disabledwith no remediation short of shelling into the container and editing the env file by hand. This release adds:- A new top-level
SettingsPageat/settings, reachable from a gear-icon link in the header. Hosts a Runtime credentials card with paste-and-update fields forASTRA_DB_API_ENDPOINT,ASTRA_DB_APPLICATION_TOKEN, andHUGGINGFACE_API_KEY(the sameMANAGED_ENV_KEYSallow-list the wizard uses). Saving POSTs to/setup/env, triggers/setup/restart, polls/readyz, and reconnects automatically. Surfaces a banner whensetup-status.hasChatProvideris false so the chat-disabled state is discoverable from the SPA. - Backend:
setupAuthGatewas relaxed so/setup/envand/setup/restartaccept post-setup updates whenauth.mode: disabled(the single-user dev posture — no privilege boundary exists). Auth-enabled deployments still require the bootstrap token, unchanged. - Dev-mode self-respawn.
/setup/restartpreviously SIGTERMed the process and relied on a container restart policy to bring the runtime back. Innpm run dev/node dist/root.js/tsx watchthere's no orchestrator, so the SPA's/readyzpoll spun forever. A newrespawnhelper now detects "no orchestrator" mode (PID != 1 AND noWORKBENCH_DISABLE_SELF_RESPAWN=1override) and spawns a detached child process with the same argv + execArgv + env before draining the parent. Container mode (PID 1) stays unchanged — the orchestrator already does this and a stray detached child wouldn't survive container teardown. - Managed env file is loaded at boot.
loadDotEnvused to only walk for a project.env, so a HuggingFace token pasted via/settings(which writes to.workbench-data/.env) never reached the respawned child'sprocess.env— the SPA banner kept saying "Chat is unconfigured" even after a successful save + restart. The loader now additionally callsloadEnvFileonmanagedEnvLocation()after the primary source, so the managed file fills any gaps without overriding higher-priority sources (process env > explicitWORKBENCH_ENV_FILE> managedwalked
.env). New optionalmanagedEnvPathfield onEnvFileResultfor the boot-time log. - 6 new SPA render tests
(
SettingsPage.test.tsx)- 1 new runtime gate test
(
setup-routes.test.ts) - 8 respawn-helper tests
(
tests/lib/respawn.test.ts) - 3 env-loader tests
(
tests/env-file.test.ts) lock the contract.
- 1 new runtime gate test
(
- A new top-level
-
DSL: admin bypass for the default policy. The default policy DSL applied when a KB has
policyEnabled: trueand no custom predicate is now:$principal.admin = 'true' OR current_principal_id() = ANY(visible_to) OR '*' = ANY(visible_to)Principals carrying
admin: 'true'see every row regardless ofvisible_to— the workspace operator no longer has to add themselves to every doc to read their own data. The compiler evaluates the bypass clause at compile time and collapses the surroundingORto an empty Data API filter (MATCH_ALL) for admin-attributed callers, so admin reads have no per-row overhead. Non-admin principals see exactly the prior behavior: the bypass clause drops out of the compiled filter, leaving the same$orofvisible_togrants as before. New compiler short-circuit logic +applyVisibleToFiltersentinel handling + evaluator/compiler/validator tests (src/policy/compiler.ts,src/policy/validator.ts,tests/policy/policy.test.ts) lock the contract. -
Header: icon-only theme toggle + Settings repositioned. The theme picker dropped its dropdown and is now a single icon button that cycles light → dark → system → light; matches the other icon-only header affordances. Settings (Cog) moved to the right of What's New so the final order reads: Theme · API docs · What's new · Settings · UserMenu.
-
RLAC flip-on bootstrap. Flipping a workspace's
rlacEnabledfromfalsetotrueused to drop operators into a UX dead-end on the in-memory /auth.mode: disabledquickstart: the KB list was still visible, but every document call returnedpolicy_principal_requiredbecause (a) no principal record existed, so the View-as picker didn't even render, and (b) every pre-RLAC document had a nullvisibleTo, which the canonical DSL treats as invisible. The PATCH handler atroutes/api-v1/workspaces.tsnow detects the false→true transition and runsbootstrapRlacFlipOn:- Default principal with admin bypass. If the workspace has
zero principals, creates
adminwithattributes: { admin: "true" }. The default DSL grants universal read access to admin-attributed principals (see "DSL: admin bypass" below), so the operator sees every document immediately without having to add themselves to each doc'svisible_to. The View-as picker auto-selects the first principal alphabetically, so the next render sendsx-view-as-principal: adminon every API call. - Visibility backfill. Every document with
visibleTo: nullis upgraded tovisibleTo: ["*"]. Documents with an explicitvisibleTo(including the empty array — a deliberate "no audience" choice) are left alone. - Best-effort + idempotent. A bootstrap failure logs
warnbut doesn't fail the workspace toggle. Re-flipping is a no-op.
The web UI side cooperates by broadening
useUpdateWorkspace's cache invalidation (apps/web/src/hooks/useWorkspaces.ts) to also invalidate the workspace-scoped query subtree, so the principals list, View-as picker, and document table all refetch through the post-bootstrap state. 8 new tests (tests/policy/flip-on-bootstrap.test.ts) lock the bootstrap contract. - Default principal with admin bypass. If the workspace has
zero principals, creates
-
RLAC on Documents — GA. The audit-log shape was locked in 0.2.0; the in-process policy evaluator hit 100% statement coverage in this release (see the next entry). With both the shape and the primary code path now stable, RLAC on Documents drops the
Previewchip and graduates to GA:- The amber
Previewbadge on the Access control card in workspace settings is removed; the "Learn about the Preview status →" link becomes a neutral "Learn more →" (apps/web/src/pages/WorkspaceSettingsPage.tsx). docs/rlac-preview.mdis renamed todocs/rlac.mdand reworded: the "Preview feature" callout is gone, "Known limitations" is reframed as "Scope of this release", and the doc gains a CLI-quickstart block. Out-of-scope items (RLAC on conversations/agents, rich predicates like group hierarchies or time-bounded visibility) are flagged as additive 0.3.x follow-ups that won't break the GA surface.- The visibility-list semantics and the principal CRUD surface are now part of the stable contract alongside the audit shape.
- The amber
-
aiw principalandaiw policyCLI commands. The runtime routes have existed since RLAC shipped; 0.2.1 wires the long-promised CLI on top. All commands respect the active profile--workspace/ profiledefaultWorkspace.
aiw principal {list,get,create,update,delete}wraps/api/v1/workspaces/{w}/principals[/{id}].createandupdateaccept repeatable--attribute key=valueflags so a principal'sattributesmap (the same one the policy DSL queries via$principal.<attr>) can be set from the shell.aiw policy preview --dsl "..." [--principal id]POSTs to/policy/compile-previewand surfaces the compiled Data API filter alongside any validation issues — useful for iterating on a DSL before flippingrlacEnabledon a workspace.aiw policy auditlists recent RLAC decisions with--principal/--kb/--day/--limitfilters.- 16 new pure-helper tests
(
tests/principal-command.test.ts,tests/policy-command.test.ts) lock the human renderer layouts.
-
Branch-complete RLAC policy evaluator tests. The in-process evaluator at
runtimes/typescript/src/policy/evaluator.tsdrives every write-path authz check, but coverage was sitting at 33.9% statements / 27.5% branches — well below the rest of the policy module despite the RLAC audit shape being a stability commitment as of 0.2.0. A newevaluator.test.tsadds 41 targeted tests covering scalar resolution (literals,$principal.<attr>,current_principal_id(), row column refs), every comparison operator on both numbers and strings, NULL semantics, type-mismatch defenses,IN/ANY()/@>membership against arrays andSets, and boolean composition (includingAND[]andOR[]identity edge cases). Evaluator now lands at 100% statements / 98% branches — the single remaining branch is the documentedNULL = NULL → truequirk incompare(). -
Agent-resolution branch coverage.
agent-resolution.tswas at 63% statements because the per-agent llm-service binding path (HuggingFace / OpenAI construction, the 4xx surface for missing credential refs and unsupported providers, the 503chat_disabledfailure mode) was only exercised indirectly through dispatcher integration tests. 14 new tests now cover every failure mode plus the system-prompt / KB-scope precedence rules, taking the file to 100% statements. -
Prompt-assembly tool-call paths.
prompt.tswent from 65% → 100% statements. New tests pin: persistedtoolCallPayloaddecode (well-formed, malformed, missing, partial), tool-result row validation (toolResponse.content/toolCallId/toolIdtriple required), orphan-tool stripping when the matchingassistant(toolCalls)is history-trimmed away, and skippingrole:"system"history entries. -
Jobs SSE route coverage.
routes/api-v1/jobs.tsjumped from 24% → 88% statements. The previous suite only exercised the polling GET; the new tests open a real SSE stream, push twoupdate()calls into theJobStore, and assert that every update appears as adata:frame and a terminaldoneevent closes the stream. Also covers the 404 envelope on a missing job. -
Web hook coverage uplift. Five hooks gained dedicated test files or expansions:
useRlac.ts: 11.5% → 96%. CoversuseRlacEnabledprecedence, principal CRUD invalidation,usePolicyCompilePreviewgating (workspaceId, non-blank DSL, null-vs-undefined principal id), andusePolicyAuditfilter pass-through.useServices.ts: 13.6% → 100%. Covers list hooks for all three service kinds (chunking / embedding / reranking), theenabled: falsepath when workspaceId is undefined, and create / update / delete invalidation for every kind.useSession.ts: 14.8% → 93%. CoversuseAuthConfig, theenabledgating onuseSessionagainstauth/config.modes.login, and all fouruseSilentRefreshno-op branches (missing refreshPath, opaque session,canRefresh: false) plus the 80%-of-lifetime timeout schedule.useWorkspaces.ts: 16.7% → 100%. CoversuseWorkspacesingle-fetch, the enabled-on-id gate, all CRUD mutations with cache invalidation- detail-cache seeding, and
useTestConnection.
- detail-cache seeding, and
-
CLI pure-logic extraction. Several CLI command files were at 0% coverage because their tests run the compiled binary in a subprocess (v8 can't instrument across process boundaries). To make pure logic measurable:
exit-codes.tsgained a 55-casetests/exit-codes.test.tsthat locks the server-error-code → exit-code table and the HTTP-status fallback heuristic. Public contract surface — scripts depend on this. 0% → 100%.commands/status.tswas refactored to exportbuildStatusReport,renderHuman, andprobe(with an injectablefetchImpl). 15 unit tests now cover the report-shape assembly, the human renderer (every fallback branch — '?' placeholders, mcp on/off, ✗/✓ indicator), and probe success / non-JSON / schema-rejection / fetch-rejection paths.commands/job.tsextractedrenderJob; new tests lock the human layout of theaiw job statusoutput.types.tsgained a smoke suite for every wire schema — including thepassthrough()tolerance for runtime upgrades and the bare-array shape ofSearchResponseSchema. 0% → 100%.
- Boot no longer crashes when Astra rotates wire shapes mid-resume.
isAstraResumingErrorused to match only the legacy 503 envelope ("resuming your database"). Astra has been observed switching to a newer 400/"resuming from hibernation"envelope across LB hand-offs while a single resume is still in progress: the first failure classifies and retries, the second comes back as the new shape, the classifier rejects it, andmain()exits with aDataAPIHttpErrorinstead of waiting out the rest of the 60-second resume window. The classifier now accepts both503and400, and matches either body phrasing, so a hibernated DB resumes cleanly under either shape (or any mix of them). A newwaitForAstraResumeintegration test reproduces the rotating-shape scenario directly so this can't regress.
| Surface | Before | After (statements) |
|---|---|---|
| TS runtime | 79.5% / 70.4% branch | 80.8% / 71.9% branch |
| Web app | 57.1% / 50.0% funcs | 59.7% / 55.5% funcs |
| CLI | 18.3% | 24.2% (src/ excluding command files now at 82%) |
Total tests: 1,932 passing (+248 new vs 0.2.0 — 1,316 runtime / 422 web / 194 CLI; the +16 CLI tests on top of the sweep are the new principal + policy renderer tests, and the +4 runtime tests cover the Astra wire-shape rotation fix above).
0.2.0 — 2026-05-22
- Conformance coverage for the chat surface. The chat / agent
message routes were previously excluded from the cross-runtime
conformance harness because chat completion depended on an upstream
LLM API. The runtime now ships a tiny
FixtureChatService(runtimes/typescript/src/chat/fixture.ts) that replays a scripted reply (sync) and a scripted token stream (async); scenarios opt in via a new optionalchatScriptfield inconformance/scenarios.jsonthat the regen + drift harness wires throughcreateApp({chatService: new FixtureChatService(scenario.chatScript)}). The runner now detectstext/event-streambodies and parses them via a sharedparseSseBodyhelper (conformance/runner.mjs) into a deterministic array of{event, data}records, so SSE fixtures normalize cleanly through the existing UUID / timestamp rules. Two new committed fixtures,chat-message-syncandchat-message-stream, pin the user + assistant wire shape + metadata for both delivery modes. Drift guard picks them up automatically. - Opt-in anonymous telemetry (wired-but-dark by default). The
runtime and CLI gain a tiny telemetry surface, off unless explicitly
enabled. Posture: off by default;
WORKBENCH_TELEMETRY=1/AIW_TELEMETRY=1flips it on;WORKBENCH_TELEMETRY_URL/AIW_TELEMETRY_URLpoints at a sink. When enabled without a URL, the emitter constructs events and logstelemetry: dark mode (no sink configured)but never sends anything — operators can verify the wiring before standing up a sink. Three event types:runtime_start(controlPlane, authMode, environment, hasChat, chatProvider),errorfromapp.onError(code + status),command_runfrom the CLI's top-level wrapper (subcommand name only, never argument values), plus a CLI-sideerror(code + exit). Every event carries an anonymous install id persisted at$WORKBENCH_DATA_DIR/.install-id(runtime) or$AIW_CONFIG_HOME/.install-id(CLI). Wire format is a fire-and-forgetPOSTwith a 2 s timeout — network failures never block the runtime or the CLI. Newdocs/telemetry.mdis the canonical event catalog and no-PII guarantee. New runtime config blockruntime.telemetry: { enabled, url }(env vars win over YAML). (runtimes/typescript/src/lib/telemetry.ts,packages/aiw-cli/src/telemetry.ts,docs/telemetry.md) - Observability surfaces:
/health/details,/health/recent-errors, curated metrics, Grafana starter dashboard, web/statuspage. Two new unauthenticated read-only endpoints surface deep backend health:GET /health/detailsreturns{controlPlane, chat, ingest, recentErrors}with per-probe{status: ok|degraded|down, detail, durationMs};GET /health/recent-errorsexposes an in-memory ring buffer (cap 100, newest first) of the last error envelopes —code,status,method, matched route pattern, request id, timestamp, no PII. Five new Prometheus families land at/metrics:workbench_chat_requests_total{provider,outcome},workbench_chat_stream_tokens_total{direction},workbench_ingest_documents_total{outcome},workbench_search_requests_total{mode,outcome},workbench_search_duration_seconds{mode}.ChatServicenow declaresproviderId("huggingface","openai", …) and an optionalping()(HFwhoami-v2, OpenAI/models) that powers the chat probe. A starter Grafana dashboard with rows for HTTP, chat, ingest, and search is committed atdocs/observability/grafana-workbench.json— drop-in via Dashboards → Import. The web UI gains a/statusroute (lazy-loadedapps/web/src/pages/StatusPage.tsx) rendering traffic-light cards for each probe + the recent-errors table, polled every 10 seconds. (runtimes/typescript/src/lib/health-probes.ts,runtimes/typescript/src/lib/recent-errors.ts,runtimes/typescript/src/lib/runtime-metrics.ts,runtimes/typescript/src/routes/operational.ts,docs/production.md) - First-run setup wizard + managed credentials file. New
unauthenticated
GET /setup-statusreports whether the runtime needs first-run configuration (setupComplete,workspacesCount,controlPlane,hasAstraCreds,hasChatProvider,managedEnv). NewPOST /setup/envatomically writes a wizard-managed dotenv file (allow-list:ASTRA_DB_API_ENDPOINT,ASTRA_DB_APPLICATION_TOKEN,HUGGINGFACE_API_KEY) to$WORKBENCH_DATA_DIR/.envwith mode0600;POST /setup/restarttriggers graceful shutdown so the bundled composerestart: unless-stoppedbrings the runtime back with the new values loaded. Both mutation routes accept the bootstrap token, or run unauthenticated only whileauth.mode === "disabled"AND no workspaces exist (the fresh-install window). The web onboarding page gains a new "Credentials" step 0 driven byapps/web/src/components/onboarding/CredentialsStep.tsxthat posts to the new routes, polls/readyz, and advances to the existing backend/details/agents flow. The compose file setsWORKBENCH_DATA_DIRandWORKBENCH_ENV_FILEso the wizard's output is auto-loaded on the next boot.WORKBENCH_ENV_FILEis no longer fatal-on-absent — fresh containers boot with no managed file and the wizard writes it. (runtimes/typescript/src/routes/setup.ts,runtimes/typescript/src/setup/managed-env.ts,apps/web/src/pages/OnboardingPage.tsx,docker-compose.yml) - CLI:
aiw doctor,aiw status,aiw profile,aiw completion. Pre-flight diagnostics (aiw doctor) run a fixed checklist — profile resolution, runtime reachability,/readyz,/auth/me, MCP feature flag, Astra-CLI auto-discovery — and exit 0 / 1 / 2 on pass / fail / warn-only.aiw doctor --explain <code>prints the runtime's error-registry entry for a given code (fetched live from/error-codes).aiw statusis the one-line counterpart for scripted health probes.aiw profile {ls,use,rm}manages the CLI's stored credential profiles without re-runninglogin.aiw completion {bash,zsh,fish}emits a hand-rolled shell-completion script (citty has no generator); covers top-level verbs and one level of subcommands. (packages/aiw-cli/src/commands/doctor.ts,packages/aiw-cli/src/commands/status.ts,packages/aiw-cli/src/commands/profile.ts,packages/aiw-cli/src/commands/completion.ts) - CLI: documented exit codes, retries, timeouts, container-aware
config path. Scripts wrapping
aiwcan now branch on stable exit codes (OK,RUNTIME_ERROR,USAGE_ERROR,AUTH_ERROR,NOT_FOUND,CONFLICT,UNAVAILABLE) derived from the server's error code first, then HTTP status.request()inpackages/aiw-cli/src/http.tsnow enforces a 10-second timeout (override viaAIW_REQUEST_TIMEOUT_MS) and retries network failures once (AIW_REQUEST_RETRIES); 4xx/5xx are never retried.HttpErrorcarries the envelope'shint,docs, andrequestId, andpackages/aiw-cli/src/output.tsrenders them as indented follow-up lines under the✗bullet. Profiles now live at$WORKBENCH_DATA_DIR/cli/config.jsonwhen the CLI runs inside the bundled compose container (override withAIW_CONFIG_HOME) so they survivedocker compose down/upin the same volume that holds control-plane state. (packages/aiw-cli/src/exit-codes.ts,packages/aiw-cli/src/http.ts,packages/aiw-cli/src/config.ts,packages/aiw-cli/README.md) - Error code registry + remediation hints in every API envelope.
Every error response now carries optional
hint(one-line remediation) anddocs(relative path under the docs root, e.g.docs/errors.md#workspace-not-found) alongside the existingcode/message/requestId. Hints come from a single registry atruntimes/typescript/src/lib/error-codes.ts; the runtime auto-fills them whenever a thrownApiError(or a control-plane error mapped to a registered code) matches an entry, so route handlers don't restate the hint at every throw site. New unauthenticated read-onlyGET /error-codesendpoint returns the registry as JSON for tooling (CLI--explain, web/statuspage, external dashboards). The newdocs/errors.mdis generated from the registry vianpm run docs:errors; a vitest drift guard fails CI if it goes stale or if a thrown code is unregistered. (runtimes/typescript/src/lib/error-codes.ts,runtimes/typescript/src/lib/errors.ts,runtimes/typescript/src/routes/operational.ts,docs/errors.md) - OIDC device-flow login (RFC 8628).
aiw login --oidcopens a device-flow grant against the runtime's new/auth/device/authorize+/auth/device/tokenproxy. The runtime fronts the configured IdP's device endpoints (auto-discovered from the OIDC discovery doc), so the CLI never needs the IdP issuer URL and the IdP client secret stays server-side. The resulting JWT is what the existing OIDC verifier already validates — no new verifier path on either side. Profiles persist the access token, optional refresh token, and expiry under a newoidcblock; the HTTP client prefers the OIDC bearer over the API key when both are present. Runtime responds501 device_flow_not_supportedwhen the IdP doesn't advertise a device endpoint, and/auth/configexposesmodes.deviceso the CLI knows up front.auth.device.authorize+auth.device.tokenjoin the audit- action union (documented indocs/audit.md). (packages/aiw-cli/src/commands/login-oidc.ts,runtimes/typescript/src/routes/auth.ts) - E2E coverage for settings + RLAC + CLI live API. Three new
Playwright specs (
settings.spec.ts,rlac.spec.ts) cover the workspace-settings RLAC toggle (revealing/hiding the principals + audit panels) and the principals-panel CRUD dialog. A new vitest subprocess spec (packages/aiw-cli/tests/cli-live-api.test.ts) drives the compiledaiwbinary against a Nodehttpstub mimicking the runtime's/auth/config,/auth/me, and/api/v1/workspacesendpoints so the bearer-wiring, config-file precedence, and JSON-output envelope are exercised end-to-end without spawning the full runtime. A newe2e/_fixtures.tshelper stamps the "What's new" modal as dismissed viaaddInitScriptso existing specs (golden-path, agent-templates, ingest) no longer have their first click intercepted by the modal's Radix overlay. - "What's new" modal + discoverability tooltips — the header
carries a sparkles trigger that opens a per-release release-notes
dialog. Auto-opens once per
APP_VERSION(dismissal persists underaiw:wn:${APP_VERSION}inlocalStorage) and stays available on demand via the trigger. Content lives inapps/web/src/lib/whats-new-content.tsas a typed array so the doc isn't parsed at runtime. Plus three hover tooltips on commonly-missed operator actions: the KB explorer Ingest button, the Agents From template button, and the API-keys New key button. The workspace-settings Access-control Preview chip already carried its own tooltip and is unchanged. (apps/web/src/components/onboarding/WhatsNewModal.tsx) - MCP write expansion — three new write tools land on the MCP
façade:
create_knowledge_base,delete_knowledge_base, andrun_agent. The first two wrap the sameKnowledgeBaseServicethe REST/knowledge-basesroute uses, so the collection-provision and rollback dance runs identically across MCP and REST.run_agentis a one-call form ofchat_send— it resolves (or creates) a conversation bound to the agent's KB set and drives the same orchestration helper, returning a structured envelope with the conversation id so callers can follow up without juggling chat lifecycle. KB writes require thewritescope;run_agentfollows the sameread-passes convention aschat_sendsince its mutations are scoped to one conversation. (runtimes/typescript/src/mcp/server.ts,runtimes/typescript/src/mcp/run-agent.ts)
- MCP façade is on by default.
mcp.enablednow defaults totrueso the Connect tab recipes (LangGraph, Google ADK, CrewAI, Microsoft Agent Framework, watsonx Path A) work against a fresh install without an extra config edit. The route still sits behind the standard/api/v1/*auth middleware and the workspace-scoped authz wrapper, so the security boundary is unchanged — disabling the route never broadened or narrowed what the auth gate allows. Operators who want a narrower surface than the REST API can setmcp.enabled: falseexplicitly.mcp.exposeChatstill defaults tofalseso MCP clients don't accidentally rack up inference cost. (runtimes/typescript/src/config/schema.ts,docs/mcp.md,docs/configuration.md) - RLAC audit-log shape is now stable. The
PolicyAuditRecordfield set, JSON types, and thePolicyAction/PolicyDecisionenum membership are committed across minor releases starting with 0.2.0. Additive changes are non-breaking; renames/removals require a minor-version deprecation window announced under Changed. A newPolicyAuditRecordV1type alias re-exports the current shape so future breaking evolutions can land asV2alongside V1 without breaking integrators. Locked byaudit-shape-lock.test.ts. (docs/rlac.md,runtimes/typescript/src/control-plane/types.ts) - RLAC scope clarification. The Preview label now covers only the policy DSL (visibility-list semantics only). The audit log is no longer marked unstable, and the doc redirects integrators to the new Audit-log shape table for the canonical wire shape.
0.1.0 — 2026-05-17
First named release. Establishes semver tracking, an automated release
workflow, a published CHANGELOG, and the aiw command-line interface.
Everything in this release is considered internal Beta — interfaces
may still change between minor versions until 1.0.
@ai-workbench/cli(aiwbinary). Newpackages/aiw-cli/workspace. Talks to a running runtime over the existing HTTP API. Commands:aiw login,aiw logout,aiw whoami,aiw workspace {list,create,delete},aiw kb {list,create},aiw doc upload,aiw search,aiw agent list,aiw chat,aiw job status. Profiles live in~/.aiw/config.json(mode0600);--profile+--urlflag overrides supported. API-key auth only (paste a key minted in the web UI). Single-binary builds attached to GitHub Releases; npm publishes under@ai-workbench/cli.- MCP read tools —
list_agentsandget_agent. External MCP clients can now discover and inspect workbench-defined agents without leaving the protocol. (runtimes/typescript/src/mcp/server.ts) - Web UI Beta · v0.1.0 chip in the header so internal users know
what release they're looking at.
(
apps/web/src/components/layout/AppShell.tsx) - Skeleton loaders —
SkeletonCard+SkeletonRowfor list/table pages; replaces the centered spinner so layouts don't jump when data arrives. Live regions +aria-busyon all shared state components. (apps/web/src/components/common/states.tsx) docs/whats-new-0.1.0.md— narrative tour of this release.docs/rlac.md— dedicated guide for the Preview-labeled RLAC feature (renamed fromdocs/rlac-preview.mdwhen RLAC went GA in 0.2.1).- Changesets workflow — every PR with user-visible impact adds a
.changeset/*.mdfile. SeeCONTRIBUTING.md. .github/workflows/release.yml— tag-triggered release that builds + publishes the CLI to npm, pushes the runtime Docker image to GHCR, builds cross-platform single-binary CLI artifacts, and creates the GitHub Release with the matching CHANGELOG section.
- Polyglot runtime framing. Python + Java runtimes are explicitly labeled Experimental contrib in their READMEs and in the root README runtime matrix. TypeScript is the supported runtime for 0.1.0; the polyglot runtimes remain valuable as conformance-harness targets but carry no stability guarantee.
- RLAC on Documents stays labeled Preview — the access-control
card in workspace settings shows a
Previewchip linking todocs/rlac.md(renamed in 0.2.1). API and audit-log shapes may change before 0.2; see the doc for the deferred items. - Root
package.jsonnow orchestrates the newpackages/aiw-cliworkspace viainstall:cli,test:cli,build:cli, and includes it innpm run check.
These were scoped for 0.1.0 but deferred to keep the release focused:
- OIDC device-flow login for the CLI.
- MCP write expansion (
create_knowledge_base,delete_knowledge_base,run_agent) — requires threading the KB and chat services into the MCP deps. - RLAC GA and stability commitment for the audit log shape.
- Full E2E coverage for chat + settings + RLAC + the
aiw-clismoke flow. - Discoverability tooltips + "What's new in 0.1.0" modal in the web UI.