Custom bots you can actually trust: pin a model, grant only the capabilities you choose, and scope a bot to the projects it belongs to — enforced at runtime across CLI, Mac, VS Code and the dashboard.
Added
- Portable, enforceable custom bots.
custom-bot/v1personalities can pin
an exactprovider/model, expose only selected Files/Terminal/Tests/Git/Web/
MCP capabilities, and limit availability to all projects, selected project
names, or personal mode. Legacy prompt-only personalities remain compatible
and unrestricted. CLI, ACP, VS Code, macOS, and Dashboard Agent Studio share
the same Markdown contract. Versioned files fail closed when tool metadata is
missing or malformed, and invalid model/scope metadata cannot activate. - Native ACP custom-bot controls. Clients can list, activate, and sync bots
throughsession/list_personalities,session/set_personality, and
session/sync_personalitieswithout scraping chat output.
Changed
-
Capabilities now say what they actually grant. The builder promised that
unselected tools are "removed from this agent's runtime", which reads as a
guarantee that a Git-only bot cannot see file contents. It can:
git show HEAD:fileis functionallycat file, and history inspection
(log -p,diff,blame) cannot be separated from the content it inspects.
Nor is Git read-only:git rm,git commitandgit pushare on the
allowlist, so a Git-only bot can rewrite the repo and publish the result.
Every capability now carries a description, and choosing Git without Files
spells out both halves — in the builder, in/personality, in the Mac persona
sheet and in the VS Code picker. The enforcement is unchanged; the promise is
now true. -
Dashboard personality edits now reach local runtimes safely. A manual
cloud pull atomically applies changed personality bodies and backs up every
divergent local copy under~/.codeep/backups/personalities/. Custom command
pulls remain additive. -
App.tsrefactor begins: HunkPicker extracted. The interactive
/apply --interactivepicker (state + key handling + rendering) moved from
the 3.3k-lineApp.tsmonolith intocomponents/HunkPicker.ts, following
the same{ State, handleKey, render }convention as Settings/Export/
Search. App.ts now owns a single state field and wires it in. The picker
logic is now unit-tested in isolation (12 tests pinning the y/n/a/q/↑/↓
semantics and the fires-exactly-onceonCompletecontract) — previously
untestable inline. First of several planned extractions (mention picker,
paste dialog, autocomplete) to bring App.ts down to a manageable size. -
PasteDialog extracted from App.ts. The large-paste confirmation
("Paste Detected" with Add/Send/Cancel) moved to
components/PasteDialog.tsin the same shape. The key handler returns
{ state, action }— a discriminated action union (add-to-input/
send-directly/cancel/none) — so App keeps the side effects
(editor insert, message submit, notification) while the decision logic is
pure and unit-tested (7 tests). -
MentionPicker extracted from App.ts. The mid-sentence
@file
autocomplete (5 state fields) moved tocomponents/MentionPicker.ts.
The load-bearing@-sigil buffer math (re-adding the@after slicing,
without which a completed path silently stops being a mention and the file
never gets attached) now lives in a pureapplyMentionToBuffer()covered
by tests, including cursor positioning and mid-buffer replacement (9
tests). -
CommandAutocomplete extracted from App.ts. The
/commandpicker (3
state fields) moved tocomponents/CommandAutocomplete.ts— same shape
as MentionPicker (pure key handler +commandToBuffer()buffer math,
9 tests). With this, all four picker-style widgets live outside App.ts as
testable components. -
Shared command core established (
commands/core/)./telemetryand
/keysyncare the first commands whose semantics (env-var hard-off
checks, config toggling, status facts) live in one place used by BOTH the
TUI and ACP dispatch — previously two hand-maintained copies that could
(and did) drift in wording and behavior. Surfaces now only render the
CommandResult. The env-var invariants (CODEEP_NO_TELEMETRY,
CODEEP_NO_KEY_SYNC overriding any config flag) and the
server-readable-keys disclosure are pinned by 11 unit tests. Remaining
~38 shared commands migrate incrementally, same pattern. -
Default rate limits lowered from effectively-unlimited. New configs get
rateLimitApi: 240/min andrateLimitCommands: 120/min — generous for a
full 50-iteration agent run, but a runaway loop now stops instead of
burning quota. Existing configs are untouched; tune via/settings.
Fixed
-
Stray characters no longer survive in the TUI. The screen paints
differentially — a cell whose value already matches the shadow copy is
skipped — which meant a BLANK cell was never emitted at all. Column 0 of the
header is blank (the wordmark starts at x = 1), so whatever the terminal
happened to show there before Codeep started stayed for the whole session; the
session picker and confirm prompt kept it on screen, and only resizing the
window cleared it. Both overlays now invalidate the shadow on the way in and
out, and the invalidation fills it with a sentinel no real cell can hold, so
blanks repaint too. -
An aborted or failed turn reports its tokens again. 2.18.1 shipped a
reportTurnStatshelper that was defined but never called: the success path
kept an inline duplicate, so cloud stats kept working there, while the catch
path reported nothing at all. Tokens burned by a turn you stopped with Esc, or
that errored, reached no one — andgracefulShutdownno longer sends the
cumulative catch-all that used to sweep them up. Both paths now go through the
one helper, and the inline copy is gone.tscstayed silent about the dead
function becausenoUnusedLocalsis off. -
SSRF guard now covers
curl/wget/http/httpsinexecute_command.
Thefetch_urltool already blocked private/loopback/metadata IPs
(169.254.169.254), but the same model-controlled URL could simply be passed
tocurlinstead and sail through. URL arguments (including scheme-less
host forms likecurl 169.254.169.254/latestand hostnames that resolve
privately) now go through the identicalassertFetchUrlAllowedcheck. The
guard moved to a sharedutils/ssrfGuard.tsmodule sofetch_urland the
shell path can't drift apart. -
envremoved from the agent command whitelist. A singleenvcall
dumpedprocess.envinto the model's context — including every provider
API key riding in environment variables. Runenvyourself outside the
agent if you need environment info. -
Exec-escape flags blocked on whitelisted utilities.
find -exec,
-execdir,-ok,-okdirandtar --to-commandspawn arbitrary commands
as arguments, silently bypassing the command whitelist
(find . -exec rm -rf / \;). Plainfind/tarusage is unaffected. -
Rate limiting is now enforced everywhere. Previously
checkApiRateLimit
was called only on the TUI's manual-chat path andcheckCommandRateLimit
had no production call sites at all — an autonomous agent run (up to 50
iterations, each with its own API call and shell commands) was completely
unthrottled. The guards now live at the transport layer:chat()in
api/index.tsandagentChat()/agentChatFallback()in
utils/agentChat.ts(covering TUI, ACP sessions, sub-agents and session
titles), plusexecute_commandinutils/toolExecution.ts. Local no-key
providers (Ollama) bypass the API limiter — there's no quota to protect on
localhost. The duplicate check in the TUI submit path was removed so a
request isn't counted twice. Source-level regression tests in
rateLimitWiring.test.tskeep the guards from being silently dropped.
Removed
- Nothing removed — the earlier note in this section was wrong: on
case-insensitive filesystemsreadme.mdandREADME.mdare the same file,
so "the duplicate" never existed and deleting it would have deleted the
README itself. (Caught before release; restored from git.)