diff --git a/.changeset/canonical-harness-foundation.md b/.changeset/canonical-harness-foundation.md index 9f71b1d..e1a99cb 100644 --- a/.changeset/canonical-harness-foundation.md +++ b/.changeset/canonical-harness-foundation.md @@ -2,4 +2,6 @@ 'bazilion': patch --- -Add the canonical Team-template and one-policy-per-Group persistence foundation, atomically migrate legacy Profile Groups, and preserve their CLI/API behavior through deprecated compatibility adapters. +Complete the breaking alpha cleanup to canonical Teams, Team Templates, and Team Policy. Remove +Group, Profile Group, Harness prototype, compatibility API/URL, and incremental migration +surfaces; consolidate fresh installs into the final `0001_init.sql` schema. diff --git a/.changeset/fresh-pi-models.md b/.changeset/fresh-pi-models.md new file mode 100644 index 0000000..aabae73 --- /dev/null +++ b/.changeset/fresh-pi-models.md @@ -0,0 +1,6 @@ +--- +"bazilion": minor +--- + +Update the bundled Pi agent packages to 0.80.6 and refresh provider examples for the GPT-5.6 +Luna, Terra, and Sol model family. diff --git a/.codex/skills/bazilion-release/SKILL.md b/.codex/skills/bazilion-release/SKILL.md index a06b98d..2d979f2 100644 --- a/.codex/skills/bazilion-release/SKILL.md +++ b/.codex/skills/bazilion-release/SKILL.md @@ -7,7 +7,8 @@ description: Bazilion release workflow for the rullopat/bazilion monorepo. Use w ## Scope -Use this workflow in `/home/patri/coding/bazilion` for the `rullopat/bazilion` monorepo. +Use this workflow from the root of the `rullopat/bazilion` monorepo. Confirm the root with +`git rev-parse --show-toplevel`; do not rely on a machine-specific absolute path. Public release packages are fixed together by `.changeset/config.json`: @@ -58,8 +59,15 @@ Run validation before committing a release: ```sh pnpm typecheck pnpm test +pnpm --filter @bazilion/web typecheck +pnpm --filter @bazilion/web build ``` +Because Bazilion currently has a clean-install-only alpha database contract, also bootstrap a +fresh temporary `BAZILION_HOME` whenever the release changes the schema, canonical Team APIs, or +filesystem layout. Verify that `schema_migrations` contains only `0001_init` and that no removed +Group, Profile Team, Harness, or compatibility route is advertised by current docs or CLI help. + If sandbox loopback/process restrictions cause failures such as `listen EPERM 127.0.0.1` or empty CLI subprocess output, rerun the same test command outside the sandbox with escalation. Treat the sandbox failure as environmental only after the escalated run passes. For dependency/provider releases, also verify pi provider coverage when relevant: diff --git a/.codex/skills/refresh-pi-models/SKILL.md b/.codex/skills/refresh-pi-models/SKILL.md new file mode 100644 index 0000000..9f04d92 --- /dev/null +++ b/.codex/skills/refresh-pi-models/SKILL.md @@ -0,0 +1,53 @@ +--- +name: refresh-pi-models +description: Update Bazilion to current @earendil-works Pi packages and reconcile its provider catalog, curated model examples, setup copy, tests, lockfile, and release notes. Use when Pi publishes a release, new model families appear, model examples become stale, provider catalogs change, or a Bazilion dependency refresh is requested. +--- + +# Refresh Pi Models + +Refresh the engine and the user-visible model guidance from upstream evidence. Do not guess model +ids or update examples merely because a model was announced elsewhere. + +## Workflow + +1. Read repository `AGENTS.md` and inspect the worktree. Preserve unrelated changes. +2. Query npm for the latest versions of `@earendil-works/pi-ai`, `pi-agent-core`, and + `pi-coding-agent`. Treat npm package metadata and the installed Pi catalog as authoritative. +3. Record the current dependency declarations in `apps/daemon/package.json`, + `apps/cli/package.json`, and `pnpm-lock.yaml`. Keep all three Pi packages on one compatible + release unless upstream explicitly publishes a supported mixed set. +4. Update both the daemon and published CLI workspace dependencies with pnpm. Never hand-edit the + lockfile. +5. Inspect upstream release/API changes. Fix Bazilion imports and adapters instead of pinning an + old release. In particular, verify whether catalog helpers still live at the root or require + `@earendil-works/pi-ai/compat`/the provider collection API. +6. Run `node .codex/skills/refresh-pi-models/scripts/audit-pi-models.mjs`. Use its exact catalog + output to update: + - `apps/web/src/routes/config/index.tsx` examples and its Pi-version comment; + - first-run examples in `apps/web/src/routes/welcome.tsx`; + - root and CLI README model examples/provider text; + - current AGENTS.md model examples when stale. +7. Prefer a current generally useful tool-capable model per provider. Do not rewrite historical + changelogs or tests whose ids are deliberate fixtures. For OAuth `openai-codex`, confirm the + model exists in that provider's catalog rather than copying an `openai` API model id. +8. Add or update focused catalog tests so key newly added families and example ids are proven to + exist. Add a Changeset for the published `bazilion` package when the bundled engine changes. +9. Validate in order: + + ```sh + node .codex/skills/refresh-pi-models/scripts/audit-pi-models.mjs + pnpm vitest run apps/daemon/test/runtime/providers.test.ts apps/daemon/test/core/provider-models.test.ts + pnpm typecheck + pnpm --filter @bazilion/web typecheck + pnpm test + pnpm lint + pnpm build + git diff --check + ``` + +10. Audit the final diff: dependency versions agree, advertised examples are present in the + installed catalog (or explicitly identified live/local aliases), and no unrelated files were + staged or reverted. + +If network or subprocess tests fail only because of the sandbox, rerun the same command with the +required approval. Report upstream versions, notable new models, API adaptations, and validation. diff --git a/.codex/skills/refresh-pi-models/agents/openai.yaml b/.codex/skills/refresh-pi-models/agents/openai.yaml new file mode 100644 index 0000000..f78cc51 --- /dev/null +++ b/.codex/skills/refresh-pi-models/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Refresh Pi Models" + short_description: "Update Pi packages and Bazilion model examples" + default_prompt: "Use $refresh-pi-models to update Bazilion to the latest Pi packages and refresh its model examples." diff --git a/.codex/skills/refresh-pi-models/scripts/audit-pi-models.mjs b/.codex/skills/refresh-pi-models/scripts/audit-pi-models.mjs new file mode 100755 index 0000000..d05ac96 --- /dev/null +++ b/.codex/skills/refresh-pi-models/scripts/audit-pi-models.mjs @@ -0,0 +1,61 @@ +#!/usr/bin/env node + +import { existsSync, readFileSync } from 'node:fs' + +const piDist = new URL( + '../../../../apps/daemon/node_modules/@earendil-works/pi-ai/dist/', + import.meta.url, +) +const compat = new URL('compat.js', piDist) +const pi = await import(existsSync(compat) ? compat.href : new URL('index.js', piDist).href) +const { getModels, getProviders } = pi +if (typeof getModels !== 'function' || typeof getProviders !== 'function') { + throw new Error('installed pi-ai exposes neither the compat nor legacy catalog helpers') +} + +const source = readFileSync('apps/web/src/routes/config/index.tsx', 'utf8') +const examples = new Map() +for (const match of source.matchAll(/case '([^']+)':\s+return '([^']+)'/g)) { + examples.set(match[1], match[2]) +} + +const aliases = new Map([ + ['bedrock', 'amazon-bedrock'], + ['azure-openai', 'azure-openai-responses'], +]) +const providers = getProviders() +const report = [] +const full = process.argv.includes('--full') + +for (const provider of providers) { + const models = (getModels(provider) ?? []).map((model) => model.id) + report.push({ provider, count: models.length, models }) +} + +const failures = [] +for (const [provider, example] of examples) { + const piProvider = aliases.get(provider) ?? provider + if (!providers.includes(piProvider)) continue // dynamic/local Bazilion provider + const models = (getModels(piProvider) ?? []).map((model) => model.id) + if (models.length > 0 && !models.includes(example)) { + failures.push(`${provider}: example '${example}' is absent from Pi provider '${piProvider}'`) + } +} + +for (const entry of report) { + console.log(`${entry.provider}: ${entry.count}`) + if (full) console.log(entry.models.map((model) => ` ${model}`).join('\n')) +} + +const newOpenAI = report + .find((entry) => entry.provider === 'openai') + ?.models.filter((model) => /^gpt-5\.\d/.test(model)) +console.log(`\nRecent OpenAI families: ${(newOpenAI ?? []).slice(-12).join(', ')}`) + +if (failures.length > 0) { + console.error('\nStale catalog-backed examples:') + for (const failure of failures) console.error(`- ${failure}`) + process.exitCode = 1 +} else { + console.log('\nAll catalog-backed web examples resolve in the installed Pi catalog.') +} diff --git a/AGENTS.md b/AGENTS.md index 9b5c54a..6d6a81c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,9 +38,9 @@ Three invariants: 1. **Nothing outside `apps/daemon` imports daemon-internal code at runtime.** Clients talk to the daemon over HTTP via `@bazilion/client` + `@bazilion/api-types`. The CLI's tests are the one pragmatic exception: they reach into `apps/daemon/src/...` via relative imports for setup/inspection. No other consumer should follow that pattern. 2. **The daemon owns the DB, scheduler, agent-cancel registry, secrets table, and the bootstrap token in auth.json.** Other processes are stateless clients. -3. **The daemon self-bootstraps on first `bazilion serve`** — there is no `bazilion init` command. `apps/daemon/src/lib/ctx.ts:bootstrap()` runs idempotently at startup: creates `~/.bazilion/{profiles,agents,skills,groups,logs}`, opens the DB, runs migrations, and (if `auth.json` is missing) mints a bootstrap web_tokens row + writes the plaintext to `auth.json`. +3. **The daemon self-bootstraps on first `bazilion serve`** — there is no `bazilion init` command. `apps/daemon/src/lib/ctx.ts:bootstrap()` runs idempotently at startup: creates `~/.bazilion/{profiles,agents,skills,teams,logs}`, opens the DB, runs migrations, and (if `auth.json` is missing) mints a bootstrap web_tokens row + writes the plaintext to `auth.json`. -- **`packages/api-types`** — **hermetic** wire-shape package. Owns the canonical type definitions for everything that crosses the HTTP/IPC wire: entity shapes (`Agent`, `Group`, `Profile`, `Message`, `WebToken`, `AgentTrigger`, `ResolvedAgent`, `LoadedProfile`, `OpenAICodexStatus`, …) in `entities.ts`, chat/provider events (`ChatFrame`, `SessionEvent`, `ProviderMessage`, `ToolCall`, `ToolDef`) in `events.ts`, memory wire types in `memory.ts`, and request/response envelopes + `PROFILE_FILES` in `index.ts`. **Zero deps** — no node-only modules, no daemon code. The daemon imports its entity/wire types FROM here; that's what keeps `apps/web`, `apps/mobile`, and `@bazilion/client` from ever reaching Node-only code (`node:sqlite`, undici, pi-ai, the worker spawner). +- **`packages/api-types`** — **hermetic** wire-shape package. Owns the canonical type definitions for everything that crosses the HTTP/IPC wire: entity shapes (`Agent`, `Team`, `Profile`, `Message`, `WebToken`, `AgentTrigger`, `ResolvedAgent`, `LoadedProfile`, `OpenAICodexStatus`, …) in `entities.ts`, chat/provider events (`ChatFrame`, `SessionEvent`, `ProviderMessage`, `ToolCall`, `ToolDef`) in `events.ts`, memory wire types in `memory.ts`, and request/response envelopes + `PROFILE_FILES` in `index.ts`. **Zero deps** — no node-only modules, no daemon code. The daemon imports its entity/wire types FROM here; that's what keeps `apps/web`, `apps/mobile`, and `@bazilion/client` from ever reaching Node-only code (`node:sqlite`, undici, pi-ai, the worker spawner). - **`packages/client`** — HTTP client for **cross-origin consumers that need explicit auth headers**: the CLI today, a future React Native / mobile app tomorrow. Exports `createClient({ serverUrl, token })`, `ApiClientError`, `BazilionClient`. Pure `fetch` + `TextDecoder` + NDJSON stream async generator — no `node:*` imports, no node-only deps. `token` is `string | (() => string | Promise)` so rotating credentials (OAuth refresh, mobile keychain reads) plug in without rebuilding the client; the package builds `Authorization: Bearer ` + `Origin: ` headers internally. `apps/cli/src/client.ts` wraps it with the Node-specific `loadClientConfig()` (reads `~/.bazilion/auth.json` + `BAZILION_SERVER`/`BAZILION_TOKEN` env via the CLI's local `apps/cli/src/{paths,auth-file}.ts` helpers). **Not used by `apps/web` browser-side code**: the web UI hits its own server same-origin with relative URLs, so the `bz_token` cookie auto-attaches. ### Auth model @@ -70,11 +70,11 @@ Expo SDK 54 + Expo Router 6 (file-based), React 19, RN 0.81, new-architecture-en - **`apps/daemon`** — Hono on `@hono/node-server`, binds `127.0.0.1:4321` by default (`HOST`/`PORT` env override). Owns all server-side code under one roof: - `src/index.ts` — entry: eagerly calls `getCtx()` so the bootstrap message + auth.json land before the port binds, then starts Hono. - `src/lib/` — daemon-only glue: `ctx.ts` (singleton + self-bootstrap), `middleware-auth.ts`, `auth.ts`, `agent-cancel.ts`, `agent-id.ts`, `agent-turn.ts`, `api-key.ts`, `cron.ts`, `messaging-host.ts`, `scheduler.ts`. - - `src/routes/` — HTTP routes by resource family: `agents.ts` (CRUD + group + skills + triggers + messages + sessions + chat NDJSON + `/cancel`), `groups.ts` (CRUD + USER.md + per-group shared memory at `/api/groups/:slug/memory*`), `profiles.ts`, `skills.ts`, `triggers.ts`, `messages.ts`, `config.ts`, `auth-login.ts` (ChatGPT OAuth + `/providers/test` + `/login`), `misc.ts` (`/health`, `/backup`, `/tokens`). - - `src/core/` — what used to be `packages/core`: SQLite schema + migrations (`db/`), repos (`repos/`), domain ops (`agent/`, `group/`, `profile/`, `skills/`), `paths.ts`, `secrets.ts`, `services.ts`, `availableModels.ts`. Barrel export at `src/core/index.ts`. + - `src/routes/` — HTTP routes by resource family: `agents.ts` (CRUD + team + skills + triggers + messages + sessions + chat NDJSON + `/cancel`), `teams.ts` (CRUD + USER.md + per-team shared memory at `/api/teams/:slug/memory*`), `profiles.ts`, `skills.ts`, `triggers.ts`, `messages.ts`, `config.ts`, `auth-login.ts` (ChatGPT OAuth + `/providers/test` + `/login`), `misc.ts` (`/health`, `/backup`, `/tokens`). + - `src/core/` — what used to be `packages/core`: SQLite schema + migrations (`db/`), repos (`repos/`), domain ops (`agent/`, `team/`, `profile/`, `skills/`), `paths.ts`, `secrets.ts`, `services.ts`, `availableModels.ts`. Barrel export at `src/core/index.ts`. - `src/runtime/` — what used to be `packages/runtime`: pi-coding-agent integration (`pi/`), provider adapters (`providers/`), memory backends (`memory/`), Bazilion-specific tools (`tools/`), per-turn worker subprocess (`worker/{entry,spawn,ipc-protocol}.ts`), heartbeat helpers (`auto-reply/`), OpenAI Codex OAuth (`auth/`). Barrel export at `src/runtime/index.ts`. - Auth + first-run middleware (`src/lib/middleware-auth.ts`) gates every route; public paths whitelisted inside the middleware. The daemon installs its own SIGINT/SIGTERM handlers and shuts the HTTP server gracefully. -- **`apps/web`** — TanStack Start (React 19 + Vite 8) frontend, **daemon-only client**. The canonical end-user UI. Canonical navigation is Templates, Agents, Groups, Approvals, Skills, and Config. Agent templates live under `/templates/agents`; Team templates under `/templates/teams`; each Group owns its production policy, membership, activity, memory, and context surfaces under `/groups/:id`; `/approvals` owns the communication-approval queue. Legacy `/profiles` and `/profile-groups` URLs are compatibility redirects. Tailwind v4 (CSS-first) + shadcn/ui. SSR loaders use `apps/web/src/lib/daemon-client.ts` (server-only) inside `createServerFn` handlers; that helper reads the request's `bz_token` cookie and forwards it as `Authorization: Bearer …` to `http://127.0.0.1:4321` (overridable via `BAZILION_DAEMON`). Browser fetches hit relative `/api/*` URLs that pass through the catch-all reverse proxy. The dev server binds `WEB_PORT` (default 4322) — pair with `bazilion serve` on 4321. **CLI/web parity is mandatory**: every HTTP endpoint must have both a CLI surface and a web UI surface. Chat markdown is rendered via `marked` + DOMPurify; styles for the `.md-content` class live in `apps/web/src/styles.css`. +- **`apps/web`** — TanStack Start (React 19 + Vite 8) frontend, **daemon-only client**. The canonical end-user UI. Canonical navigation is Templates, Agents, Teams, Approvals, Skills, and Config. Agent templates live under `/templates/agents`; Team templates under `/templates/teams`; each Team owns its production policy, membership, activity, memory, and context surfaces under `/teams/:id`; `/approvals` owns the communication-approval queue. Tailwind v4 (CSS-first) + shadcn/ui. SSR loaders use `apps/web/src/lib/daemon-client.ts` (server-only) inside `createServerFn` handlers; that helper reads the request's `bz_token` cookie and forwards it as `Authorization: Bearer …` to `http://127.0.0.1:4321` (overridable via `BAZILION_DAEMON`). Browser fetches hit relative `/api/*` URLs that pass through the catch-all reverse proxy. The dev server binds `WEB_PORT` (default 4322) — pair with `bazilion serve` on 4321. **CLI/web parity is mandatory**: every HTTP endpoint must have both a CLI surface and a web UI surface. Chat markdown is rendered via `marked` + DOMPurify; styles for the `.md-content` class live in `apps/web/src/styles.css`. ### SQLite driver @@ -88,10 +88,10 @@ State lives in `~/.bazilion/` (overridable via `$BAZILION_HOME`): ~/.bazilion/ bazilion.db # ALL DB state: entities + secrets + config (encrypted) + tokens auth.json # {token, remote?} — bootstrap bearer + optional CLI remote target - groups// # collaboration root, mounted as cwd; may be a symlink (--link) - memory/ # group-shared qmd index (.qmd-index.sqlite + markdown notes) + teams// # collaboration root, mounted as cwd; may be a symlink (--link) + memory/ # team-shared qmd index (.qmd-index.sqlite + markdown notes) ... project files / work product ... - agents// # agent's PRIVATE home — strictly outside the group tree + agents// # agent's PRIVATE home — strictly outside the team tree SOUL.md / IDENTITY.md / AGENTS.md / TOOLS.md / HEARTBEAT.md / [BOOTSTRAP.md] sessions/.jsonl # pi's append-only transcript agent.json @@ -100,15 +100,15 @@ State lives in `~/.bazilion/` (overridable via `$BAZILION_HOME`): logs/ ``` -Path resolution is centralized in `apps/daemon/src/core/paths.ts`. The `Paths` struct has `home`, `db`, `authFile`, `profilesDir`, `agentsDir`, `groupsDir`, `skillsDir`, `logsDir`, plus `agentDir(id)` / `groupDir(slug)` / `profileDir(id)` / `skillDir(name)` computed helpers. The CLI has its own minimal `apps/cli/src/paths.ts` (`resolveCliPaths` returns just `{home, authFile}`) for the filesystem-level commands (`uninstall`, `backup`, `login`, `token show-local`) — they don't need the full struct. **There is no longer a `configFile` field — `config.json` and `secrets.enc` were collapsed into the DB.** `bazilion uninstall` mirrors this layout as a two-tier teardown: data tier = DB + `profiles/` + `agents/` + `groups/`; full wipe adds `auth.json`, `logs/`, `skills/`. Symlinked groups (registered with `--link`) only have the slot under `~/.bazilion/groups/` removed; the symlink target is never touched. +Path resolution is centralized in `apps/daemon/src/core/paths.ts`. The `Paths` struct has `home`, `db`, `authFile`, `profilesDir`, `agentsDir`, `teamsDir`, `skillsDir`, `logsDir`, plus `agentDir(id)` / `teamDir(slug)` / `profileDir(id)` / `skillDir(name)` computed helpers. The CLI has its own minimal `apps/cli/src/paths.ts` (`resolveCliPaths` returns just `{home, authFile}`) for the filesystem-level commands (`uninstall`, `backup`, `login`, `token show-local`) — they don't need the full struct. **There is no longer a `configFile` field — `config.json` and `secrets.enc` were collapsed into the DB.** `bazilion uninstall` mirrors this layout as a two-tier teardown: data tier = DB + `profiles/` + `agents/` + `teams/`; full wipe adds `auth.json`, `logs/`, `skills/`. Symlinked teams (registered with `--link`) only have the slot under `~/.bazilion/teams/` removed; the symlink target is never touched. -### Group registration +### Team registration -Groups always live at `~/.bazilion/groups//`. The CLI (`bazilion group add [--link ]`) and the web `/groups` create form pass only the slug + optional name + optional link target; the daemon decides where the slot goes. `--link ` materializes the slot as a symlink to an existing directory (the "agents working on my existing project tree" path); the target must exist and be a directory. Without `--link`, a fresh real directory is created. `groupRepo.get/list/insert(db, ..., paths)` derive `Group.path` from `paths.groupDir(id)` at read time — there is no `path` column anymore. +Teams always live at `~/.bazilion/teams//`. The CLI (`bazilion team add [--link ]`) and the web `/teams` create form pass only the slug + optional name + optional link target; the daemon decides where the slot goes. `--link ` materializes the slot as a symlink to an existing directory (the "agents working on my existing project tree" path); the target must exist and be a directory. Without `--link`, a fresh real directory is created. `teamRepo.get/list/insert(db, ..., paths)` derive `Team.path` from `paths.teamDir(id)` at read time — there is no `path` column anymore. ### Memory model -Memory is **per-group**, shared across every agent in the group. The qmd backend lives at `/memory/`; each turn's worker calls `qmdBackend(join(agent.group.path, 'memory'))`. The `memory_*` tool descriptions explicitly tell the LLM the store is shared and direct personal notes (persona quirks, preferences) to `home_write IDENTITY.md` instead. The schema's per-agent memory dir is gone — `spawnAgent` only creates `agents//sessions/`. External surfaces match the ownership: HTTP at `/api/groups/:slug/memory*`, web UI at `/groups/:slug/memory`, CLI at `bazilion memory ...`. There are no `/api/agents/:id/memory*` routes — clients always address the group. +Memory is **per-team**, shared across every agent in the team. The qmd backend lives at `/memory/`; each turn's worker calls `qmdBackend(join(agent.team.path, 'memory'))`. The `memory_*` tool descriptions explicitly tell the LLM the store is shared and direct personal notes (persona quirks, preferences) to `home_write IDENTITY.md` instead. The schema's per-agent memory dir is gone — `spawnAgent` only creates `agents//sessions/`. External surfaces match the ownership: HTTP at `/api/teams/:slug/memory*`, web UI at `/teams/:slug/memory`, CLI at `bazilion memory ...`. There are no `/api/agents/:id/memory*` routes — clients always address the team. ### Worker subprocess + IPC @@ -146,21 +146,24 @@ API in `apps/daemon/src/core/`: `openSecrets(db, password)` and `openConfig(db)` - Biome formatter: single quotes, no semicolons, trailing commas, 2-space indent, 100-col width. - TS is strict with `noUncheckedIndexedAccess` and `verbatimModuleSyntax`. `.ts` extensions are required on all relative imports (`allowImportingTsExtensions: true`) — `tsx` and Vite both handle it. - `apps/web/` is excluded from the root `tsconfig.json` and from biome (it has its own Vite/TanStack-managed tooling). Run `pnpm --filter @bazilion/web typecheck` for web-only TS checks. The whole-tree `typecheck` intentionally skips it. -- Migrations are numbered SQL files in `apps/daemon/src/core/db/migrations/`. The schema is consolidated into a single `0001_init.sql` (the project is alpha; we wiped the DB and collapsed the prior chain on every shape change rather than maintaining an ALTER chain). Add new migrations as new numbered files going forward; the runner is idempotent and runs from the daemon's self-bootstrap (`apps/daemon/src/lib/ctx.ts:bootstrap()`) on every startup. Existing installs pick new files up automatically. +- **The alpha database contract is clean-install only.** The complete canonical schema lives in + `apps/daemon/src/core/db/migrations/0001_init.sql`. Until a stable migration contract is + announced, schema changes edit that file directly and require wiping/rebootstrapping + `~/.bazilion`; do not add ALTER migrations, legacy-table importers, or API/URL/filesystem + compatibility adapters. The bootstrap runner remains idempotent for an already-current schema. ## Already-shipped invariants (don't re-implement) - **Daemon = sole owner of `~/.bazilion`** — the worker subprocess delegates anything DB-backed (messaging tools, provider gate, agent resolution, secrets) back to the daemon over Node IPC + stdin. There is no per-worker SQLite handle. -- **Groups = single filesystem root + USER.md + roster + shared memory.** One agent → one group. `groups.user_md` is a DB column (agents can't clobber it via `write`/`edit`). Groups always live under `~/.bazilion/groups//` (real dir or symlink via `--link`). -- **Memory is group-shared.** `qmdBackend(group.path/memory)` — every member writes to and reads from the same store. Personal notes go to `IDENTITY.md` via `home_write`. -- **Canonical production harness model.** Agent templates are reusable Profiles. Team templates - (`harness_templates`) own one revisioned, stable-slot roster and directed policy. A live - Group owns exactly one effective revisioned policy; Agents are permanent resources with - exactly one Group membership. Do not create a second Team roster or detached live-harness - identity. Canonical HTTP surfaces are `/api/harness-templates` and - `/api/groups/:id/harness`; legacy Profile Group surfaces are compatibility adapters. -- **Harness enforcement covers every communication boundary.** With - `BAZILION_HARNESS_ENFORCEMENT=on`, the shared authorizer gates user/peer/cross-Group, +- **Teams = single filesystem root + USER.md + roster + shared memory.** One agent → one team. `teams.user_md` is a DB column (agents can't clobber it via `write`/`edit`). Teams always live under `~/.bazilion/teams//` (real dir or symlink via `--link`). +- **Memory is team-shared.** `qmdBackend(team.path/memory)` — every member writes to and reads from the same store. Personal notes go to `IDENTITY.md` via `home_write`. +- **Canonical production Team Policy model.** Agent templates are reusable Profiles. Team templates + (`team_templates`) own one revisioned, stable-slot roster and directed policy. A live + Team owns exactly one effective revisioned policy; Agents are permanent resources with + exactly one Team membership. Do not create a second Team roster or detached live Team Policy + identity. Canonical HTTP surfaces are `/api/team-templates` and `/api/teams/:id/policy`. +- **Team Policy enforcement covers every communication boundary.** With + `BAZILION_TEAM_POLICY_ENFORCEMENT=on`, the shared authorizer gates user/peer/cross-Team, scheduler, inbox, HTTP/worker turn, and Telegram ingress/egress boundaries. Missing edges deny. Durable block events contain policy evidence but never message payloads. The compiled management contract is version 1; do not add a bypass or a second authorization path. @@ -172,19 +175,22 @@ API in `apps/daemon/src/core/`: `openSecrets(db, password)` and `openConfig(db)` assignment engines. - **No runs/events tables, no stats CLI.** The runs/events audit layer was dropped in favor of pi's session JSONL files (which are the authoritative transcript). There is no `bazilion run list/show/cancel/prune` and no `bazilion stats`. Cancel is `bazilion agent cancel ` (keyed by agentId). - **Bootstrap auth lives in `auth.json`.** The daemon reads it once at startup (`getCtx().authToken`) and uses it as the PBKDF2 seed for the secrets table. The CLI reads it as its loopback bearer. The `bootstrap` row in `web_tokens` cannot be revoked — `DELETE /api/tokens/:id` rejects a hash match against the auth token. -- **First-run gate** — `isSetupComplete(db)` returns true iff at least one enabled provider has ≥1 curated model. Web middleware redirects non-API routes to `/welcome` while the gate is closed; API routes return 409. Allowed prefixes during setup: `/welcome`, `/login`, `/config`, `/api/config`, `/api/auth`, `/api/health`, `/api/login`. Crossing the threshold triggers `ensureSetupSeeded(db, paths)` which creates the `default` profile (skillsMode: `'all'`) + `default` group (at `~/.bazilion/groups/default/`). +- **First-run gate** — `isSetupComplete(db)` returns true iff at least one enabled provider has ≥1 curated model. Web middleware redirects non-API routes to `/welcome` while the gate is closed; API routes return 409. Allowed prefixes during setup: `/welcome`, `/login`, `/config`, `/api/config`, `/api/auth`, `/api/health`, `/api/login`. Crossing the threshold triggers `ensureSetupSeeded(db, paths)` which creates the `default` profile (skillsMode: `'all'`) + `default` team (at `~/.bazilion/teams/default/`). - **Spawn-time skill override is gone.** Skills come from the profile only — `skillsMode: 'all'` attaches every installed skill at spawn, `'selected'` uses `profile_default_skills`. Per-agent tweaks happen post-spawn via `bazilion agent skill add/rm` (or the per-agent skills card on the detail page). -- **Web client constants live in `apps/web/src/lib/wire-constants.ts`** (`DEFAULT_GROUP_ID`, `DEFAULT_PROFILE_ID`, `REASONING_LEVELS`). `apps/web/src/lib/daemon-client.ts` is server-only — Vite's import-protection rejects it from any client-bundled module. +- **Web client constants live in `apps/web/src/lib/wire-constants.ts`** (`DEFAULT_TEAM_ID`, `DEFAULT_PROFILE_ID`, `REASONING_LEVELS`). `apps/web/src/lib/daemon-client.ts` is server-only — Vite's import-protection rejects it from any client-bundled module. - **OpenClaw skill model: prompt-only.** Skills under `~/.bazilion/skills//` get their SKILL.md body injected into the system prompt of every agent they're attached to; helper scripts run via the agent's generic `bash` tool. No framework `entry:` extension, no trust gate. -- **qmd memory backend** (`apps/daemon/src/runtime/memory/qmd.ts`) — wraps `@tobilu/qmd`'s `searchLex` (BM25) for all memory routes. One `.qmd-index.sqlite` per group. Hybrid/vector paths are intentionally not enabled (pulls `node-llama-cpp` and multi-GB GGUF models; excluded in `pnpm.onlyBuiltDependencies`). +- **qmd memory backend** (`apps/daemon/src/runtime/memory/qmd.ts`) — wraps `@tobilu/qmd`'s `searchLex` (BM25) for all memory routes. One `.qmd-index.sqlite` per team. Hybrid/vector paths are intentionally not enabled (pulls `node-llama-cpp` and multi-GB GGUF models; excluded in `pnpm.onlyBuiltDependencies`). - **Heartbeats / cron triggers** (`agent_triggers` table; `apps/daemon/src/lib/scheduler.ts`) — in-process tick loop (default 5s, `BAZILION_SCHEDULER_TICK_MS`; disable with `BAZILION_SCHEDULER=off`) pinned to `globalThis[Symbol.for('bazilion.scheduler')]`. Interval kind uses `last_fired_at + every ≤ now` with `created_at` as baseline; cron kind parses 5-field expressions via `apps/daemon/src/lib/cron.ts`. Firing reuses `runAgentTurn`; `last_fired_at` is updated *before* the run kicks off so a restart mid-fire doesn't double-trigger. CLI: `bazilion trigger add|list|rm|enable|disable`. - **Inbox / messaging surfaces** — the `send_message` / `read_inbox` / `wait_for_reply` tools (`apps/daemon/src/runtime/tools/messaging.ts`) are wired with `MessagingHost` injection: in the daemon (compact/context routes) the host is `createDbMessagingHost(db)`; in the worker the host is `createIpcMessagingHost()` which proxies every method through Node IPC. Outside-the-loop surfaces: `GET /api/agents/:id/messages?unread=1` (list), `POST /api/agents/:id/messages` (now accepts `replyTo`), `GET|PATCH /api/messages/:id` (detail + mark-read). CLI: `bazilion inbox list [--unread]`, `inbox show `, `inbox read `. Web: `/agents/:id/inbox`. - **`web_fetch` hardening: Readability + markdown + cache + SSRF guard** — `@mozilla/readability` over `linkedom`, output as markdown (or text via `extract_mode`). SSRF guard at `apps/daemon/src/runtime/tools/web-ssrf.ts` blocks loopback/private/link-local + DNS rebinding (re-validates resolved IPs, pins them into undici's `Agent.connect.lookup`). 15-min in-memory LRU per `${mode}|${url}` (100-entry cap). UA spoofs desktop Safari. 20s default timeout, 3 max redirects. - **ChatGPT OAuth / `openai-codex` provider** — credentials in `secrets:OPENAI_CODEX_OAUTH`. CLI runs the loopback flow (port 1455) client-side and PUTs credentials to `/api/auth/openai`; web `/config` has a "Connect ChatGPT" card. `apps/daemon/src/lib/api-key.ts:resolveAgentApiKey` is the single helper every session-creating call site uses to pre-fetch the access token; for daemon-side sessions it also wires a refresher for mid-turn JWT swaps. Worker turns don't get the refresher today (they'd need a new IPC method). -- **Profile Groups — preconfigured team templates** (`profile_groups` + `profile_group_members` tables, migration `0002_profile_groups.sql`; shipped in v0.2.0). A profile group is an ordered list of *members*, each pointing at a profile with optional per-member overrides (`agent_name`, `model_override`, `reasoning_level`). `apps/daemon/src/core/profile-group/spawn.ts:spawnProfileGroup` is one transactional call that materializes the whole team — pre-flight validates every `profileId`, snapshots both `agents/` and `groups/` dirs before the tx, and on any failure cleans the dir-diff with `rmWithRetry` (100/500/2000 ms backoff, extracted into `core/profile-group/rm-with-retry.ts` for testability). Name collisions auto-suffix `-2`, `-3`, … against agents already in the target group. Target group is auto-created if its slug doesn't exist; `userMd` from the request seeds USER.md only on a freshly-created group. HTTP: `/api/profile-groups[/:id[/members|spawn]]`. CLI: `bazilion profile-group create|list|show|update|edit|delete|spawn`. Web: top-nav `templates` tab (shared with profiles via `apps/web/src/components/TemplatesTabs.tsx`), `/profile-groups` list + detail, sidebar `+ new ▾` two-section dropdown (spawn agent from template / spawn group from template), empty-group CTA on `/groups/:id`. Wire types `ProfileGroup`, `ProfileGroupMember`, `ProfileGroupDetail`, `ProfileGroupWithCount` in `@bazilion/api-types`. Strictly additive — the single-profile `POST /api/agents` spawn path is untouched. +- **Team Templates are the only reusable Team roster.** `team_templates` owns revisioned + stable slots and policy edges. HTTP uses `/api/team-templates`, CLI uses + `bazilion team-template`, and web uses `/templates/teams`. Do not add a second roster model + or compatibility alias. - **Shared web ` )} - {prototypeInputBlocked && ( -
- Composer disabled: this harness denies direct user input to {agentName}. -
- )} ) } @@ -1102,7 +1054,7 @@ function Bubble({ entry, isLastUser, isWillDrop, onEdit }: BubbleProps) { ) } return ( -
+
you {entry.images && entry.images.length > 0 && (
@@ -1140,7 +1092,7 @@ function Bubble({ entry, isLastUser, isWillDrop, onEdit }: BubbleProps) { type="button" onClick={onEdit} title="edit and resend — replaces the last turn" - className="absolute left-1 top-1 rounded-sm border border-fawn bg-ivory px-2 py-0.5 text-[0.78em] font-medium text-mocha opacity-65 transition hover:border-sapphire hover:bg-sapphire-glow hover:text-sapphire hover:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100" + className="absolute left-1 top-1 rounded-sm border border-fawn bg-ivory px-2 py-0.5 text-[0.78em] font-medium text-mocha opacity-65 transition hover:border-sapphire hover:bg-sapphire-glow hover:text-sapphire hover:opacity-100 team-hover:opacity-100 team-focus-within:opacity-100" > edit diff --git a/apps/web/src/components/CreateGroupDialog.tsx b/apps/web/src/components/CreateTeamDialog.tsx similarity index 90% rename from apps/web/src/components/CreateGroupDialog.tsx rename to apps/web/src/components/CreateTeamDialog.tsx index c6954fb..b78386b 100644 --- a/apps/web/src/components/CreateGroupDialog.tsx +++ b/apps/web/src/components/CreateTeamDialog.tsx @@ -1,6 +1,6 @@ -// Quick-create a group from the sidebar's spawn dropdown. POSTs to -// /api/groups with the current shape: { id, name?, link? }. The daemon -// puts the slot under ~/.bazilion/groups// — a fresh directory by +// Quick-create a team from the sidebar's spawn dropdown. POSTs to +// /api/teams with the current shape: { id, name?, link? }. The daemon +// puts the slot under ~/.bazilion/teams// — a fresh directory by // default, or a symlink to `link` if provided. import { useRouter } from '@tanstack/react-router' @@ -10,7 +10,7 @@ interface Props { onClose: () => void } -export function CreateGroupDialog({ onClose }: Props) { +export function CreateTeamDialog({ onClose }: Props) { const router = useRouter() const [id, setId] = useState('') const [name, setName] = useState('') @@ -30,7 +30,7 @@ export function CreateGroupDialog({ onClose }: Props) { const body: Record = { id: id.trim() } if (name.trim()) body.name = name.trim() if (link.trim()) body.link = link.trim() - const res = await fetch('/api/groups', { + const res = await fetch('/api/teams', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), @@ -59,10 +59,10 @@ export function CreateGroupDialog({ onClose }: Props) { onClick={(e) => e.stopPropagation()} className="w-full max-w-md rounded-2xl border bg-card p-6 shadow-lg" > -

Create a new group

+

Create a new team

- A group is a collaboration context — one filesystem root, one USER.md, one roster. The - slot lives at ~/.bazilion/groups/<slug>/. + A team is a collaboration context — one filesystem root, one USER.md, one roster. The + slot lives at ~/.bazilion/teams/<slug>/.

{err &&

{err}

} - {preview &&

Creating this Agent advances Group revision {preview.currentRevision} → {preview.resultingRevision} and adds {preview.addedEdges.length} directed edges to the existing {preview.existingEdges.length}.

    {preview.addedEdges.map((edge,index)=>
  • ${edge.targetKind}:${edge.targetId??''}:${index}`}>{edge.sourceKind}{edge.sourceId?`:${edge.sourceId}`:''} → {edge.targetKind}{edge.targetId?`:${edge.targetId}`:''}
  • )}
{preview.addedEdges.length===0&&

The new Agent starts isolated.

}
} + {preview &&

Creating this Agent advances Team revision {preview.currentRevision} → {preview.resultingRevision} and adds {preview.addedEdges.length} directed edges to the existing {preview.existingEdges.length}.

    {preview.addedEdges.map((edge,index)=>
  • ${edge.targetKind}:${edge.targetId??''}:${index}`}>{edge.sourceKind}{edge.sourceId?`:${edge.sourceId}`:''} → {edge.targetKind}{edge.targetId?`:${edge.targetId}`:''}
  • )}
{preview.addedEdges.length===0&&

The new Agent starts isolated.

}
}