Commit Graph
14 Commits
Author SHA1 Message Date
tbenandClaude Opus 5.5 d7dfe0e78e Let bots ask multiple-choice questions before acting
ask_user puts up to six questions on one card (radios for one pick,
checkboxes for several, an optional typed Other) as a [question:ID] chip.
The user's answers are checked against the questions, stored in
bot_questions and sent to the bot as their chat message; Skip says so.
Offered in chat replies and user-started tasks, never in routines, hook
calls or chatroom sessions.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 11:10:23 +01:00
tbenandClaude Opus 5.5 7b762fd384 Let workers ask each other through the coordinator in chatrooms
A worker asks for another bot's help with ask_crew. The coordinator
reads the request, asks the right bot (in a workspace, only its
members), and sends the answer back into the asking bot's
conversation, waking it. Each exchange is kept as a read-only
chatroom: listed in the main rail after the workspaces, or in the
workspace's rail for requests made there, with live status rows for
each bot while it's open and a Stop button.

Room sessions are hidden from every conversation list, other bots'
words reach the model fenced as data, and turns, nesting and open
rooms are capped. Closed rooms are pruned after a retention period
and past a cap per place (Settings > General).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 18:57:34 +01:00
tbenandClaude Opus 5.5 bea656ade8 Add bot graphics
Bots can now show a concept as a visual: create_graphic (with
update_graphic, read_graphic and list_graphics, on every run, no computer
needed) stores one HTML document in bot_graphics, per user like the
conversation, and the chat renders its [graphic:ID] ref as an inline card
sized to fit, with a large view and a page of its own (no rail).

Graphic HTML is treated as hostile: it is only served at /graphics/<id>
with a CSP sandbox allowing scripts under an origin of its own and no
network, loading only the vendored Chart.js and Mermaid from
/graphics-lib/. The document carries the app's theme vars and scrollbars,
reports its height to the thread, and a frame that navigates away is
closed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 08:11:15 +01:00
tbenandClaude Opus 5.5 69988e08fc Move secrets between users and computers without the model seeing them
A new personal env scope (paradigm user: one user's secret for one bot)
beats every other scope, reaches only that user's runs of that bot, and
is redacted like any secret. Bots take secrets in with request_secret,
which puts a [secret-request:ID] card in the chat: the user types the
value into a masked input there, never into a message, and the bot is
told only the key. Bots show secrets with capture_secret (a task's
computer tool that saves a file or a command's output straight to a
personal env, then redacts it from the rest of the run) and show_secret
(an env by key): both give a [secret:ID] chip whose Reveal and Copy fetch
the value on click, for its own user (personal) or a super admin
(anything else), logged by key. Values never go into messages, page
props, history, Copy chat or logs. Users manage their own secrets on
Settings > Secrets; the Environment page lists everyone's, remove only.
Deleting a bot or user removes its personal secrets, and SecretsNote
tells bots never to ask for or print secrets in chat.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 16:51:32 +01:00
tbenandClaude Opus 5.5 cdb93ac1bc Fix file access, XSS and access-control bugs from a security scan
- Media rows can no longer be created through the CRUD API, updates only
  take description fields on the caller's own row, and every media read
  or delete goes through mediaFilePath, which refuses paths outside
  DATA_DIR or inside the database folder.
- Uploads strip the file name and extension to safe characters and keep
  only title, description and summary from the request's media object.
- Folder imports no longer follow symlinks (walkRegularFiles), and the
  source path is quoted in the computer's shell.
- User and bot files are served with nosniff, and HTML, SVG, XML and
  scripts are sandboxed by CSP.
- Sandbox cookies are kept to the computer's prefix, and websocket
  upgrades on /ws and the computer proxy must come from the app's own
  address (README and Help note nginx's Host header).
- user_types CRUD writes go through the assignment matrix; members only
  read their own user record; read-only users can read CRUD again;
  grabUsers builds valid SQL for array search terms and bad pages; an
  admin media DELETE needs an id; read-only users can't delete media.
- Rate limits and the audit log use the connection's address, trusting
  forwarded headers only from a proxy, and login limits are split per
  address, per account from that address and per account overall.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 03:38:07 +01:00
tben f2802c1bf3 Let bots set a picture from a file they can see
A coordinator asked to set an uploaded logo as a workspace picture could
not: the picture tools take a media id, and nothing ever gave the bot one.
Attachments reached the model as "[Attached image \"logo.png\"]", every
file tool answers with a [file:ID] ref, and an SVG is deliberately stored
as a document rather than an image.

- copyMediaAsPicture copies an approved image into a new picture row
  (bot-image/workspace-image). The source keeps its row and bytes, so a
  message that attached it still loads and changing a picture can never
  take a file away from a conversation.
- grabPictureSource resolves a picture by [file:ID] (the same scope gate
  every file tool uses) or by the user's own upload; parseFileRef reads
  the [file:12] form.
- The new set_picture tool ships on every run: a bot sets its own face,
  the coordinator can also dress a worker. update_workspace gained
  picture_file_id, and setBotPicture/setProfileImage share the copy and
  the delete.
- removeReplacedPicture deletes a replaced picture only when it really is
  one, never a chat upload.
- uploadAndRecordMedia writes images in their own format: PNG stays PNG
  with its transparency, WebP stays WebP, GIF becomes PNG, everything
  else JPEG, and the row records the real mime type (it used to say jpeg
  for every image).
- Pictures change live: BotUpdatedData carries image_media_id and a new
  server:workspace-updated pushes workspace pictures, relayed between
  processes.
- Attachments are labelled with their media id, and BotFilesNote tells a
  bot that a [file:ID] can become a picture.
- grabRunScopes is now the single source of a run's reachable folders,
  shared by the file tools, imports and the picture tools.
2026-10-23 05:24:55 +01:00
tben d3357cb7df Stop storing raw file bytes in media.text_content
`text_content` is an extracted-text cache, but nothing stopped it from
holding a dump of bytes:

- `fileToText` ended its switch with `default: return buffer.toString("utf-8")`
  and defaulted a missing extension to "txt", so any binary was decoded.
- Both extraction callers gated on `media_type == "document"`, and
  `attachmentFileKind` classes everything non-image/audio/video as a
  document: an SVG, a zip, a spreadsheet, a .bin.
- Nothing capped the length, so a 20 MB attachment could leave 20 MB of
  text in the database.
- `storeChatAttachments` copied the text onto a second `media` row, and
  `SELECT m.*` pulled the column into page props and API responses.

On a real install an SVG held 6852 characters of markup and a
`.designcraft` file held 142 KB of raw bytes.

Changes:

- `file-to-text.ts` lists the text extensions explicitly and returns
  undefined for anything else; no extension is no text.
- `can-extract-text.ts` is the one allowlist (reusing
  `BotTextFileExtensions`), used by both upload routes and by
  `uploadAndRecordMedia`; `extension-of.ts` is shared with the storage
  catalog instead of duplicated.
- `BotAttachmentTextStoreMaxChars` (200k) caps the column via
  `cap-text-content.ts`.
- `store-chat-attachments.ts` no longer copies the text; ingestion
  re-reads the bytes once.
- `media-columns.ts` selects every media column but `text_content`, which
  now has to be asked for (`grab-messages-media`'s reply path and
  ingestion do); the media CRUD routes sanitize rows they write.
- `ingest-bot-file.ts` decides on size alone, since a cached slice made
  an oversized file look already read.
- `bun run media:prune-text` cleans rows that were already stored
  (rerunnable, with a VACUUM hint).

Verified on the dev database: both bad rows cleared (146 KB), rerun
reports nothing to do.
2026-10-23 05:22:04 +01:00
tben aebd6b8ccc Serve every media file privately at /media/<id>
There is no public media any more. `/media/public/<id>` served a media
row with no session at all, so anyone with the id could read a file.
The public/private split is gone: the path is `/media/<id>` (and
`/media/<id>/thumbnail`), every request is signed in first, and
`grabUserMedia` decides whether the user may see it: their own upload,
a bot's or workspace's picture or file they reach, or a file on a task
they reach. Anything else is a 404 before a path is built.

The `media.is_private` column and the two per-user directories went
with it: uploads live in `media/<user_id>/` and there is nothing to
pick between. The files browser's media volume is now the viewer's
own folder whatever their role.

Bots also say which of the two things they mean. A `[file:ID]` ref is a
saved file and its chip opens the bytes; a folder is a new
`[folder:volume:owner:path]` ref (`src/utils/folder-ref.ts`) whose chip
opens the files browser there and downloads nothing. The old `[path:...]`
ref and the extension sniffing that guessed file from folder are gone,
so `import_file` names the folder holding a file it copied rather than
the file itself.

The prompts say what the Assistant got wrong: list before importing or
saving, one copy per artifact, never split a file into parts unless
asked, never claim the store is empty without a fresh list, and never
invent a size cap. A re-import now says the path already existed
instead of silently overwriting and reporting a fresh copy.
2026-10-22 10:10:53 +01:00
tbenandClaude Opus 5.5 e45d54d30b Share one crew across the install, keep user memory per user
Bots, workspaces and computers now belong to the install. Users reach
workers only through bot_users (assigned by a super admin or admin) and
workspace_users; the coordinator and main computer are one per install,
and only the super admin sees the coordinator. Conversations are per
user (bot_sessions.user_id), jobs run for the session's user, and live
updates go only to who should see them.

A bot's core memory is now general knowledge shared by every user it
works for; what it learns about a user lives in bot_user_memories.

One-off scripts move existing installs over: move-bot-memory-to-owners
(run first), then share-crew.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 11:37:14 +01:00
tbenandClaude Opus 5.5 4dd34ed251 Share one user form, add user photos and Revoke, confirm every form
- Settings > Users uses the people UserForm (useFormInit) instead of its
  own copy; it saves in one request through users/save-user, which now
  takes an optional profile photo (setUserProfileImage)
- Revoke action on user rows adds the revoked type, as the edit form would
- Every admin form asks for confirmation before it saves

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 11:03:57 +01:00
tbenandClaude Opus 5.5 f24c0d7e82 Run computers with resources and health checks, add settings for bots, workspaces, computers and users
- Computers get CPUs and memory (default 1 CPU, 2 GB), set at creation and
  editable; new resources stop, remove and recreate the container, its home
  dir kept. Every computer container has a docker health check and stays
  "creating" until it passes; only healthy computers are reached, by
  hostname, else by their IP on the network (dev outside docker).
- Docker is required: the app creates the cobble network on startup and
  exits without docker.
- Production container: deploy/run-app-container.sh mounts .env, the data
  dir and the docker socket; the database (DB_DIR), media, computers' homes
  and bun-sqlite's live schema now live on the data volume.
- Settings pages for bots, workspaces, computers and users, sharing list
  components; bots can be deleted; users are created with a password and
  confirmation, edited and deleted through RBAC-gated APIs.
- User types trimmed to super_admin, admin, member, read_only and revoked;
  admins manage members. UserAvatar shows profile photos from the user row.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 10:26:44 +01:00
tbenandClaude Opus 5.5 6a2665dae9 Add the harness, envs, per-bot models and chat attachments
- Harness: providers, model catalog synced hourly, automatic free picks,
  cooldowns, tool loop with context trimming, MCP computer tools
- Bots queue runs replies and tasks through the harness; jobs wait
  (attempt refunded) while every model is resting
- Envs: Settings > Environment, scoped general/workspace/computer,
  written to computers and redacted from tool output
- Models: Settings > Models, per-harness provider select defaulting to
  OpenRouter, and a models modal on each bot's chat (the coordinator's
  edits the defaults)
- Chat: thinking row while a reply runs, file attachments (upload,
  paste, drop) that replies read as images, audio and document text
- Fix media uploads failing after a user's first file, and private
  files being served from the public media path

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 03:38:38 +01:00
tbenandClaude Opus 5.5 3286b80029 Build Cobble project structure from the handoff
- Remove the preset document suite (letters, emails, invoices, quotes,
  PDF generation, line items) with its routes, API handlers and tables
- Rebrand to Cobble: site data, Lato, handoff design tokens (light and
  dark) in main.css, brand assets in public/brand, ember stone logo and
  regenerated favicons
- Add bots, bot_messages, bot_tasks and bot_task_steps tables, regenerate
  DB types (BUN_SQLITE_COBBLE_*) and add a sample crew seed (seed:bots)
- Add the chat shell at /admin/bots: rail with pinned coordinator and
  user popover, bottom-anchored conversation with composer, working-on
  panel, and a send-message API scoped to the session user
- Add BotStone and Wordmark components, crew status and relative time
  utils with tests, and update AGENTS.md

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 17:10:36 +01:00
tben 81e4a8aecc First Commit. Duplicate bunext-sqlite preset 2026-10-18 16:47:11 +01:00