Closes#5757
`DEEPAGENTS_HOME` now selects the dcode user profile and trust root at
launch while preserving `~/.deepagents` as the default.
---
This makes the configured home an immutable, normalized path captured
before dotenv loading and shared by the client, server, reloads, and
child processes. A central path snapshot separates profile data,
installation-owned resources, and project configuration so cwd changes
and mutable environment state cannot move the trust root. Absolute
profiles also remain usable when the launch user's home cannot be
resolved; optional home-based integrations are skipped in that case.
Profile resolution rejects ambiguous or unsafe roots, including relative
and `~user` forms, filesystem and launch-home aliases, dangling
symlinks, non-directories, and unreadable or unsearchable directories.
The installer applies the same validation as Python and changes
ownership only for exact leaves it creates.
MCP discovery now retains explicit user or project provenance. Only the
exact configured user `.mcp.json` receives user-level trust, and
filesystem aliases or collisions fail closed to project scope. This
prevents project dotenv files, ancestor homes, checkout-contained
profiles, and case or symlink aliases from self-approving project MCP
servers.
Install and update locks and managed ripgrep prefer installation-scoped
locations, with profile-scoped fallbacks when the shared locations are
unusable. A fallback ripgrep is checksum-verified and exposed through a
process-private `PATH` shim so profile-controlled sibling executables
never enter subprocess lookup. Runtime consumers, prompts, bundled
skills, UI messages, diagnostics, and token permission hints use the
effective configured paths, and failed write-probe cleanup is surfaced
without repeated warnings.
<details>
<summary>Test plan</summary>
- Added deterministic path, dotenv, cwd, reload, client/server,
missing-home, and subprocess regressions.
- Added root-validation and installer parity coverage, including
symlink, permission, ownership, and write-probe cases.
- Added MCP provenance, filesystem-identity, trust-classification, and
project self-approval security regressions.
- Added shared-lock, managed-ripgrep fallback, checksum,
private-`PATH`-shim, and optional-ripgrep regressions.
- Added prompt, bundled-skill, diagnostics, and MCP token-path consumer
regressions.
- Current-head validation is covered by the `deepagents-code` lint job
and Python 3.12–3.14 test matrix.
</details>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Remove tests that only exercise local expressions, language/dataclass
mechanics, annotations, or source text, plus an exact duplicate and
assertions already covered by stronger behavior tests. Production
behavior and meaningful regression coverage are unchanged.
Made by [Open
SWE](https://openswe.vercel.app/agents/105f8f81-8407-5183-be73-7f5df20dd8f7)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Session-scoped CLI flags now use shared configuration resolution, so
managed policy wins consistently and users are warned when a flag is
overridden or rejected.
---
CLI values previously reached runtime consumers outside the shared
resolver, causing inconsistent behavior across launches, configuration
inspection, ACP, thread listing, and tools.
This adds an immutable `CliProvider` between managed configuration and
the environment and gives every resolver the same CLI snapshot.
Consumers now read effective resolved values, while deduplicated
warnings explain managed overrides and rejected flags. Existing flag
behavior and startup fast paths are preserved.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
`dcode -v` and `dcode --version` now warn when the fresh local update
cache knows about a newer release. The check stays offline, honors
update-check opt-outs, and fails softly when cached status is
unavailable.
**Before**
```text
deepagents-code 0.1.61
deepagents (SDK) 0.7.8
```
**After (when a cached update is known)**
```text
deepagents-code 0.1.61
deepagents (SDK) 0.7.8
Update available: v0.1.62. Run: uv tool install -U deepagents-code
```
---
The warning uses the existing fresh local update cache and
install-method-aware upgrade command, so version output does not make a
network request.
Made by [Open
SWE](https://openswe.vercel.app/agents/b0010396-c78a-554c-a946-99194751d0aa)
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Coding-agent middleware spans no longer serialize duplicate message and
state payloads.
---
This extends the SDK trace policy from #5377 to all middleware owned by
`deepagents-code`, while avoiding a process-wide policy that could
change third-party middleware tracing. A contract test keeps every owned
middleware class opted in.
Made by [Open
SWE](https://openswe.vercel.app/agents/a391e2dc-ba5f-53fb-82fe-ad847a895937)
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Keep the macOS Textual interface visible while filtering unmanaged
native diagnostics.
---
`TerminalStderrGuard` points fd 2 at `/dev/null`. Textual 8.2.8 renders
every Unix frame to `sys.__stderr__`. #5813 therefore sent the whole
interface to `/dev/null`:
```
$ deepagents # macOS, stdout and stderr on the same terminal
# (blank screen, no prompt, no error)
```
When suppression is active, Textual now renders through the matching
stdout terminal instead. Native fd-2 writes stay suppressed, and
redirected stderr and terminal handoffs stay preserved:
```
$ deepagents # macOS, stdout and stderr on the same terminal
╭─ deepagents ─────────────────────────────────────────╮
│ > │
╰──────────────────────────────────────────────────────╯
```
## When stdout cannot carry the interface
The override applies only when it is safe. `stdout_driver_class()`
declines, and the caller then drops stderr suppression, in two cases:
- `sys.__stdout__` is `None` or closed. An active guard proves only that
fd 1 is a terminal, not that the Python object is usable. Frames written
to a dead stream kill Textual's writer thread, and the event loop then
blocks forever on the full write queue.
- `TEXTUAL_DRIVER` names an explicit driver. That driver renders to
stderr, so the guard would hide it. The user's choice wins.
In both cases the interface stays visible on an unguarded stderr. A
visible interface with native noise beats an invisible one.
## Coupling to Textual internals
The subclass replaces the private `_file` attribute, so it follows the
rule the patches in `_textual_patches.py` follow: do not guess when the
ground shifts. If Textual stops keeping its output stream in `_file`,
the subclass raises instead of assigning a dead attribute. The error
surfaces after the caller's `finally` restores fd 2, so it reaches the
terminal.
The test asserts the upstream contract as well as the override, so a
rename in Textual fails in CI rather than in a user's terminal.
Made by [Open
SWE](https://openswe.vercel.app/agents/35f98f77-db79-5fa8-bf6b-12877813719d)
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Stacked on #5234. Review that one first; this PR's diff is only the
`libs/code` half.
## The bug
`ReliableRubricMiddleware` overrides `_grade`/`_agrade` to retry a
transient grader transport failure. Those are the same methods that hold
the SDK's coverage retry, and the override never calls `super()`, so
dcode replaced the coverage retry rather than composing with it.
The consequence is asymmetric, because the *downgrade* that the coverage
retry protects is in `_finalize_evaluation`, which dcode does inherit:
| | grader calls before downgrade |
|---|---|
| SDK `RubricMiddleware` | 2 (under-report, re-ask, still short) |
| dcode `ReliableRubricMiddleware` | 1 (under-report) |
So a grader that miscounts its criteria once turns a `satisfied` into
`needs_revision` in dcode with no second chance. At `max_iterations=3` a
slip on the final iteration becomes `max_iterations_reached`, and
`_resolve_pending_goal_completion` then refuses to complete a `/goal`
whose work was actually satisfied.
Second, smaller bug: on that downgrade path no criterion is marked
failed, so the transcript rendered "Acceptance criteria not yet
satisfied" plus "Address every unmet criterion, then retry the check."
above an empty list.
## The fix
- The transport retry moves down to `_invoke_grader`/`_ainvoke_grader`,
which wrap exactly one grader call and delegate to `super()`. The two
retries now nest correctly: a transport fault is retried within a call,
an under-report across calls.
- `_grader_input` takes the SDK's `correction` argument and builds its
payload through `super()`, so the retry's corrective feedback reaches
the grader and the delimiter sanitization is no longer re-implemented
here.
- The `after_agent`/`aafter_agent` overrides are deleted. They existed
only to re-raise `GraphBubbleUp` and to thread `runtime.context`; the
SDK does both now. This also drops the private `_strategy_from_result`
import.
- `_format_rubric_event`/`_format_rubric_details` read the new
`unverified` flag and describe the verdict as a verification gap ("could
not be verified", "the grader could not account for every criterion")
instead of pointing at criteria that do not exist.
## Tests
`libs/code`: 11680 passed, 3 skipped. `make lint` and `make type` clean.
New coverage:
- the coverage retry fires for an under-reporting grader, sync and
async, and the second call carries the correction
- a transport fault and an under-report in the same pass produce three
grader calls
- `unverified` rendering for `needs_revision` and
`max_iterations_reached`, plus a guard that the ordinary revision
wording is unchanged
---------
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Native macOS diagnostics no longer corrupt the Textual interface while
it owns the terminal.
---
macOS frameworks can write directly to fd 2, bypassing Textual's
Python-level stderr capture. Suppress those writes only when stdout and
stderr share a TTY, while preserving redirected stderr and restoring it
for external editors, process suspension, and teardown.
Tests cover fd suppression/restoration, platform and TTY gates, editor
suspension, and app cleanup.
Made by [Open
SWE](https://openswe.vercel.app/agents/35f98f77-db79-5fa8-bf6b-12877813719d)
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
The stale editable-dependency prompt shown before the TUI launches now
defaults to "Refresh environment now" instead of "Abort launch": a bare
Enter (or clicking through with the default highlight) repairs the
environment rather than killing the launch. "Abort launch" keeps its
leading position in the list, one keystroke up from the new default.
---
When an editable `dcode` install falls behind the checkout's dependency
floors, the prompt's whole purpose is to steer the user toward
refreshing — yet it opened with "Abort launch" highlighted, so the path
of least resistance was the least useful outcome and users could
reflexively Enter their way out of a launch they wanted. In the text
fallback the prompt advertised `Abort launch [N]` as the default, so a
bare Enter aborted there too.
The picker now computes its initial highlight by action identity and
prefers the refresh action when one is offered; deny-only prompts
(project trust, MCP approvals) still default to refusing. The text
fallback mirrors the picker: hints read `Abort launch [n] · Refresh
environment now [U]` and an empty answer maps to `REFRESH`, while
explicit `n`/`no` still aborts.
`/offload` now runs only as a server-owned operation on built-in dcode
servers. Local and ACP agents no longer support it, and custom or older
servers without the route are unsupported.
---
The graph-operation prototype coupled the TUI to LangGraph run routing
and lifecycle behavior. This revision puts the ownership boundary in
dcode's server backend: the built-in LangGraph deployment registers a
custom HTTP app that resolves the same cached runtime, compaction
policy, hooks, model configuration, and `CompositeBackend` as the
interactive agent.
```mermaid
flowchart LR
user["User runs /offload"] --> tui["TUI"]
subgraph client["Client"]
tui --> remote["RemoteAgent"]
hookexec["Configured hook executor"]
remote <--> hookexec
end
subgraph server["Built-in dcode LangGraph server"]
api["Offload HTTP boundary"]
runtime["Shared server runtime"]
operation["OffloadOperation"]
hooks["PreCompact + PreToolUse"]
compact["Agent compaction service"]
state[("Thread checkpoint")]
backend["Agent CompositeBackend"]
cost["Cost recorder"]
api -->|"resolve"| runtime
runtime --> operation
operation --> hooks --> compact
api -->|"read + validate"| state
compact -->|"plan archive"| backend
cost -->|"priced delta"| api
api -->|"summary event + cost"| state
api -->|"then append archive"| backend
end
remote -->|"thread ID + runtime context"| api
api -->|"typed result or hook request"| remote
compact -->|"summarize"| model["Configured model provider"]
style api fill:#dcfce7,stroke:#16a34a
style operation fill:#dcfce7,stroke:#16a34a
style state fill:#dcfce7,stroke:#16a34a
```
The server operation:
- reads and hydrates checkpoint state itself; the request contains no
graph name, checkpoint, or conversation messages;
- refuses a thread that holds work in flight — active, interrupted,
carrying pending graph tasks, or advanced past the checkpoint it read. A
thread whose last run *failed* is still offloadable: LangGraph leaves
that thread on `error` until the next run completes, which is exactly
when a user reaches for `/offload` to recover from an overflow;
- commits only the channels `OffloadStateUpdate` declares. The runtime
guard is an allowlist derived from that type, so a future merge adding
any other channel is refused, not just `messages`. No synthetic
assistant or tool message is persisted, and the operation cannot replace
conversation history;
- resolves the summarizer's model and transport from the checkpoint, not
from the request. A client cannot point the server's credentialed
provider calls at an endpoint of its choosing;
- runs the agent's `PreCompact` and `PreToolUse` hooks, transporting
interrupt/resume payloads opaquely through the client and returning
denials as typed results. The session's approval mode is carried across,
so a configured hook sees the same mode it sees on an interactive turn;
- reserves the summary in the checkpoint *before* appending the archive,
and rolls the append back if the link cannot be committed. A per-session
lock serializes an archive's read/write cycle against concurrent
compactions;
- records priceable model cost in the same checkpoint update. Every
prepared charge is explicitly committed or rolled back, and an abandoned
one warns rather than vanishing;
- uses the agent's existing compaction backend/policy and archive guard,
so server-side archives remain readable by the agent;
- accepts an explicit cancellation for an in-flight operation and
confirms the outcome, so a client that gives up learns whether the
operation finished or was cancelled.
The client calls the server route directly. There is no capability probe
and no seeded tool-call fallback. A local in-process or ACP agent gets a
short unsupported message. A server that does not register the route
gets a message naming that cause instead of a bare transport error.
Careful review is warranted around the custom-route/thread-state
boundary, the archive reserve-then-append ordering, and hook replay
identity. The real integration test launches the production server
configuration, checks message preservation around `/offload`, verifies
route authentication, and reads the archive back through the running
agent.
### User-visible output
Success is unchanged in shape:
```
Offloaded 6 older messages, freeing up context window space.
Conversation: ~1.0K → ~250 tokens (75% decrease), 4 messages kept.
```
Three failure paths now say something actionable:
| Situation | Before | Now |
| --- | --- | --- |
| Last turn failed (thread on `error`) | `Offload failed: Cannot offload
while the thread has an active or interrupted run.` — and no way out
until a turn succeeds | Offloads normally |
| Server does not register the route | `Offload failed: 404 Not Found` |
`Offload failed: This server does not provide dcode's /offload
operation. Use the built-in dcode server, or upgrade the server to a
version that registers it.` |
| Reporting fails after the server committed | `Offload failed:
<exception>` — prompting a second offload of an already compacted
conversation | `The conversation was offloaded, but the result could not
be displayed. Check logs for details.` |
A dropped endpoint override is now logged with the key names, so a user
whose gateway configuration is being ignored has something to find.
<details>
<summary>Test plan</summary>
- Full `make test`: 14,199 passed, 2 skipped.
- Real-server integration: 3 passed.
- `make format` and `make lint`: passed, including Ruff, `ty`, and
command-catalog validation.
- Three added tests were mutation-verified: re-keying the per-thread
lock on `operation_id`, renaming the route's path converter, and
deleting the hook round-limit `break` each fail the new test and passed
before it.
</details>
Made by [Open
SWE](https://openswe.vercel.app/agents/0ecc91e2-f151-5f52-94aa-e6ed75c6dfc1)
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Fixed misleading hover feedback on non-expandable tool results in the
terminal UI.
---
Tool rows now expose the hover gutter only when clicking can expand or
collapse hidden detail. Fully visible results ignore row clicks, while
expandable output, arguments, and task descriptions retain their
affordance.
Made by [Open
SWE](https://openswe.vercel.app/agents/7d257f30-8a87-5613-9143-5d5effa4b19c)
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Conversation-history archives older than 30 days are now removed by a
best-effort startup worker; `[history].retention_days` overrides the
window and `0` disables cleanup.
---
The sweep is restricted to regular `.md` files directly under the
archive directory, logs and swallows filesystem/config failures, and
never blocks startup.
Made by [Open
SWE](https://openswe.vercel.app/agents/cd8f03c0-256c-5cb1-86f2-e0897df89b24)
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
`dcode` can now be restricted to an approved set of models. Add a
`models.allowed` list to `config.toml` (or push it via managed config)
and every model the app tries to use — the launch default, `--model`
flag, `/model` selections, saved defaults, and subagent model
declarations — is checked against it. Entries are exact `provider:model`
specs or `provider:*` wildcards that admit a whole provider's lineup.
Unset means all models are allowed; an empty list means none are.
---
**Why:** organizations rolling out `dcode` need to guarantee agents only
run on approved models (cost, compliance, data routing). Previously any
model with a credential was usable, and there was no enforcement point.
**How:**
- A new `models.allowed` config key (user or managed layer) parses into
an ordered, deduplicated allowlist of exact `provider:model` specs and
`provider:*` wildcards. A malformed list fails closed (deny-all) with an
error naming the defect, so a typo can't silently disable the guardrail.
- Every model-resolution path is gated: CLI flags, the model selector
(blocked entries are hidden from recommendations; typing a blocked spec
explains the policy and lists what's allowed), saved defaults (a blocked
stored default is ignored with a log, not advertised as `(default)`),
and subagent declarations (the error names the declaring file).
- Bare model names are canonicalized the same way `create_model`
resolves them before the policy check, so `gpt-5.6-terra` matches
`openai:gpt-5.6-terra` — or an `openai:*` wildcard — instead of being
spuriously rejected; a name whose provider can't be established stays
unmatchable, so inference is never a bypass.
- A `provider:*` wildcard admits that provider's whole lineup where an
exact list would prune unlisted models. The wildcard names no model
itself, so default resolution expands it to the provider's discovered
models — the registry profile lineup merged with any configured list —
rather than selecting it literally; a wildcard for a provider with no
discovered or configured models fails closed with a "no discoverable
models" message instead of a phantom credential prompt.
- Blocked selections raise `ModelNotAllowedError`, whose message names
the policy layer (user file vs. administrator-managed), quotes the
offending spec, and lists the allowed entries — instead of the previous
generic I/O-style failure. When the allowlist is active but no allowed
model has credentials, a distinct `NoAllowedModelCredentialsError` keeps
`/auth` from accepting a key and appearing to do nothing.
Edge cases covered: remote no-auth providers (e.g., a LAN Ollama
endpoint) remain valid allowlist fallback candidates; bare Bedrock IDs
must be written `bedrock:<id>` in the list since they otherwise split at
the version colon and could never match; `dcode tools list` skips
enforcement because it only compiles graphs it never invokes.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Kimi K3 <kimi-k3@moonshot.ai>
Environment-variable guidance in the auth dialog is now shorter and
identifies both supported `.env` locations plus the exact scope of
`Ctrl+R`.
---
The previous copy was verbose and made the reload shortcut sound global
rather than dialog-specific.
**Before**
> Alternatively, environment variables can be used in place of the key
stored above. Set `DEEPAGENTS_CODE_TAVILY_API_KEY` for a dcode-only key;
it has the highest priority. Set `TAVILY_API_KEY` to share a key with
other provider SDK tools; it is used only when no scoped or stored key
exists. After setting one in a .env file, press Ctrl+R to reload without
restarting. A variable exported in a separate shell after launch is
invisible to this process; it needs a full relaunch. Configuration docs.
**After**
> Environment variables: `DEEPAGENTS_CODE_TAVILY_API_KEY` (dcode only,
highest priority) or `TAVILY_API_KEY` (shared, lowest priority). Put
either in the project `.env` or `~/.deepagents/.env`; press Ctrl+R in
this dialog to reload. New shell exports require restarting the app.
Configuration docs.
**Example scenario:** A user has the Tavily auth dialog open and adds
`DEEPAGENTS_CODE_TAVILY_API_KEY` to the project `.env`. They can press
`Ctrl+R` in that dialog to load it immediately. If they instead export
the variable from another shell, they must restart dcode because the
running process cannot inherit later shell changes.
Focused auth-widget tests cover precedence, dotenv locations, reload
scope, and restart guidance.
Made by [Open
SWE](https://openswe.vercel.app/agents/3e770dd7-d5a5-5aa2-b7e7-5fd34e54f359)
---------
Signed-off-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com>
`ConfigResolver` (with its cached per-process snapshot) shipped in
#5736, but every production reader still went through the legacy
`resolve_scalar` / `resolve_ranked_scalar` wrappers, which re-parse
`config.toml` on every call. This PR retires those wrappers and settles
the question the wrappers had been hiding: **when is configuration
read?**
## Behavior change
Configuration files are now read once into a single process-wide
generation. Every reader resolves against that generation, so no two
parts of the process can disagree about a setting.
**Editing `config.toml` while the app is running therefore has no effect
until the generation advances**, which happens in exactly two places: an
in-app write (toggling a preference refreshes the generation itself) and
`/reload`. A file that fails to parse leaves the previous generation in
force rather than half-applying the new one.
This is the convention every long-running Unix service uses — read at
start, change on an explicit signal. Watching files for edits is
deliberately not done: a partly applied configuration is a worse failure
than a stale one, and per-option exceptions (some live, some cached)
would make the effective configuration unpredictable per setting. The
policy is now written down in `ARCHITECTURE.md` and pinned by tests, so
it stops being an accident of which call path a reader happens to take.
Users who edit the file by hand and expect immediate effect will need
`/reload`. Worth a release note.
## Retiring the wrappers
1. **Plain single-key readers move to the shared resolver.** `app.py`,
`config.py`, `cold_cache.py`, `cost_tracking.py`, `main.py`,
`plugins/discovery.py`, and the two TUI widgets now call
`get_config_resolver().get(option)`, replacing per-call file parsing
with the resolver's cached snapshot. Diagnostics behavior is preserved
by calling `_emit_ranked_diagnostics(option, resolved)` explicitly at
each site. `Settings._reload_values` resolves through
`get_config_resolver(refresh_managed=refresh_managed)`, so `/reload` and
later readers observe the same generation instead of the cache going
stale; the env tier still comes from the method's `env` argument, and
the "a failed or blocked reload never drops policy in force" invariant
is unchanged.
2. **Explicit-snapshot callers build ad-hoc resolvers.** Callers that
pass `toml_data=`/`managed_toml_data=` deliberately inspect a specific
file generation rather than process state, so pointing them at the
shared cache would be wrong. The decision rule: if the caller snapshots
one generation itself — the `config` CLI (one read per invocation),
`update_check` (health reported next to the value), the sandbox/theme
loaders (a non-default `config_path` excludes managed policy), and the
managed-policy validators (a candidate generation not yet in force) — it
builds `resolver_from_snapshots(TomlSnapshot(...), TomlSnapshot(...))`
and calls `.get(option)`.
3. **The legacy path is deleted.** `resolve_scalar`,
`resolve_ranked_scalar`, and the now-dead `_coerce_env` helper are gone
from `config_manifest`. The manifest's remaining bespoke readers share a
private `_resolve_option`, which resolves through the shared generation
when the caller supplies no tables. The migration-parity equivalence
test is deleted with them — its job (proving the two paths agree) is
done. Tests that drove the wrappers directly now exercise the same
coercion, precedence, and diagnostics assertions through
`ConfigResolver`/`resolver_from_snapshots` or real TOML files under the
test-redirected config path.
4. **The remaining fresh-parse readers, and why.** Two keep a concrete
parse, and neither is on a live path:
- `resolve_read_project_dotenv` runs during dotenv bootstrap, before the
project `.env` is layered into `os.environ`; seeding the shared
generation there would capture an env tier later readers do not see.
- `resolve_startup_mode_with_source` inspects the raw user table on its
fall-through path, which the resolver does not expose. Its only
production caller (`dcode config`) passes an explicit generation.
## Fixes found while reviewing this branch
- **`/reload` previews read a stale user tier.** The preview path shares
`_reload_values` with `refresh_managed=False`, so its cached user
snapshot could predate the `[shell].allow_list` edit being previewed.
The preview then reported no change while the accepted reload applied it
— diverging on a security-sensitive auto-approval setting, and reaching
the cwd-switch consent prompt as
`project_settings_change_detected=False`. The preview now reads the user
file fresh while keeping the managed snapshot the process is enforcing
(a preview must not refresh policy in force). This is the atomic-swap
half of the policy, not a liveness exception: preview and apply must
agree on one generation.
- **A lost diagnostic on UI preference writes.** `_save_ui_bool_result`
was the one migrated reader that did not pick up the wrapper's implicit
diagnostics call, so a malformed managed `[ui]` entry was reported
nowhere — removing the one signal an administrator has that their policy
is inert.
- **`/reload` reloaded every provider twice.**
`get_config_resolver(refresh_managed=True)` already reloads on a cache
hit, so the explicit `.reload()` was redundant: one `/reload` read the
managed file four times and re-ran `managed_policy_violations` with
each.
- **Merge strategies for real manifest options were unasserted.** The
deleted `test_populated_tiers_actually_reach_the_resolver` did not
depend on the `resolve_scalar` oracle, and `merge_strategy` appeared
nowhere else in the suite — so flipping `threads.columns` from
`DEEP_MERGE` to `REPLACE`, or `mcp.disabled_servers` from `UNION` to
`REPLACE`, passed the full suite. Restored and mutation-checked in both
directions.
- **`refresh_managed` was untested.** Removing the refresh from
`/reload` kept the suite green, because the existing reload tests drive
env rather than a file edit and monkeypatch `DEFAULT_CONFIG_PATH` (which
changes the resolver cache key and rebuilds a fresh resolver, hiding
staleness). Now pinned in both directions.
- **Comment rot.** Roughly ten comments still described per-call file
parsing, including one that promised live edits took effect without a
restart — the exact behavior the migration removed.
`get_option`, `get_config_options`, `_emit_ranked_diagnostics`, and the
manifest types stay public and unchanged; managed config remains
read-only; no new dependencies.
## Follow-ups
- `_emit_ranked_diagnostics` and `_ranked_source` now have nine external
importers, and the snapshot-construction block appears at six production
sites. A small `resolve_for_display(option)` seam (get + emit + label)
would restore the encapsulation the wrapper provided and make the
missing-emit bug above structurally impossible.
- A file corrupted *after* its generation is taken is not reported.
Corruption present when the file is read still logs with exact line and
column; only mid-session corruption is silent. A cheap mtime/size check
on `/reload` would close it.
- Five `cast(...)` calls exist only because `ConfigResolver.get` returns
`ResolvedValue[object]`; three sit behind validating predicates that
could be `TypeGuard`s. `AGENTS.md` treats `cast` as a last resort.
Incognito shell commands now show the same transcript widget as regular
shell commands, using the existing incognito color while remaining
excluded from model context.
---
This preserves the `!!` prefix when mounting `UserMessage`, allowing the
shared shell widget’s existing mode detection and styling to apply.
Made by [Open
SWE](https://openswe.vercel.app/agents/5d95e03a-8235-5a88-bcb2-8974878a4ffa)
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Adds a searchable prompt clipboard for reusing previously submitted
prompts, with a two-tier Ctrl+R interaction modeled on codex and Claude
Code.
The first <kbd>Ctrl</kbd>+<kbd>R</kbd> opens an inline search panel
directly above the prompt input — a query field, up to five newest-first
matches, and a hint line — so recalling a recent prompt never leaves the
composer. Focus and the blinking cursor move into the query field while
the panel is open; typing filters matches case-insensitively while the
draft stays frozen underneath. <kbd>Enter</kbd> inserts the selected
prompt at the cursor, arrow keys navigate,
<kbd>Tab</kbd>/<kbd>Shift</kbd>+<kbd>Tab</kbd> page by five rows, and
<kbd>Esc</kbd> (or Backspace on an empty query) restores the draft and
cursor exactly as they were.
Pressing <kbd>Ctrl</kbd>+<kbd>R</kbd> again while the panel is open
escalates to the full-screen prompt clipboard, carrying the typed query
into its filter. The full view adds a scrollable list, a multi-line
preview pane, the same <kbd>Tab</kbd>/<kbd>Shift</kbd>+<kbd>Tab</kbd>
paging, and <kbd>Ctrl</kbd>+<kbd>C</kbd> to copy a prompt without
inserting it. `/prompts` opens this full view directly, as does Ctrl+R
while autocomplete is open.
---
Prompt history is local-only: it reads the shared JSONL history file,
deduplicates newest-first, and refreshes on open, with no telemetry or
sync. Both tiers insert through the undoable edit path, so a recalled
prompt can be undone with <kbd>Ctrl</kbd>+<kbd>Z</kbd> like any other
edit.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Closes#4489
`dcode --model google_anthropic_vertex:claude-sonnet-4-6` now works with
GCP ADC when `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION` are
configured.
---
Claude on Vertex uses Anthropic's Messages API rather than Google's
`generateContent` transport, so it needs a separate provider from
`google_vertexai`. This reuses LangChain's existing
`google_anthropic_vertex` registry name and integration.
Made by [Open
SWE](https://openswe.vercel.app/agents/979e343f-42ad-580d-ab0a-69827de47d40)
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
A headless launch that receives `--auto-approve` or `--yolo` now warns
and continues. Before, it exited 2.
## Why
Neither flag ever reached the headless path. `run_non_interactive`
derives its approval mode from `settings.shell_allow_list` alone, and
`_resolve_approval_mode` — the only reader of `args.auto_approve` and
`args.yolo` — is called only in the interactive branch. The exit was a
usage error about a no-op flag, not a security control.
That exit broke a common pattern: one alias or wrapper that passes `-y`
unconditionally, used both interactively and headlessly. The run is
still fail-closed either way. Shell access needs `--shell-allow-list`,
and MCP actions are denied unless routed.
## Before
```console
$ echo "summarize this" | dcode -y --shell-allow-list ls
Error: --auto-approve is only supported in interactive mode. Headless mode uses
fail-closed MCP routing and --shell-allow-list for shell access.
$ echo $?
2
```
## After
```console
$ echo "summarize this" | dcode -y --shell-allow-list ls
Warning: --auto-approve has no effect in headless mode; ignoring it. Shell access is governed by --shell-allow-list, and MCP routing is fail-closed.
… run proceeds …
$ echo $?
0
```
The warning prints on one line with `soft_wrap=True`. Rich otherwise
hard wraps at width 80 off a TTY, and the break moves with the flag
name, so no single substring matches both `--auto-approve` and `--yolo`.
With the exit gone, this line is the only signal a CI job has that the
requested mode was dropped, so it must stay greppable.
## `--auto-classifier-model` still exits 2
The dependent flag keeps its hard error. It has no interactive-reuse
case, so ignoring it would only hide a typo in a setting that governs
action authorization.
That left one command line producing two verdicts:
```console
$ dcode -y --auto-classifier-model anthropic:claude-haiku-4-5 -n "task"
Warning: --auto-approve has no effect in headless mode; ignoring it. …
Error: --auto-classifier-model is only supported in the interactive TUI, where
Auto approval mode runs; it runs headlessly.
```
The classifier guard now runs before the approval warning, so the exit
prints alone.
## Notes
- The warning keys off a flag name captured at parse time, before
`_apply_managed_runtime_policy` can revoke it. Managed policy only ever
revokes, so without the early capture a policy-cleared flag the user did
type would warn about nothing.
- Clearing `args.auto_approve` and `args.yolo` is defense in depth. No
headless consumer reads them today; the guarantee lives at
`client/non_interactive.py:2132`.
- Two pre-existing gaps this does not touch: `THREAT_MODEL.md:526`
claims HTTP tools pass the HITL gate in non-interactive mode, which the
code contradicts; and the interactive-piped route (`-m` plus a pipe) has
no test.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
dcode now automatically supplies active goal objectives and acceptance
criteria as hidden internal model context, so the agent can begin and
continue goal-directed work without `get_goal` or `get_rubric` tool
calls.
---
`/goal` still owns the persistent, user-reviewed objective and generated
criteria. The checkpoint fields remain the source of truth;
`GoalToolsMiddleware` renders a current goal-state notice from them
whenever the model needs it. The notice uses a framework-owned
`lc_source`, so it is excluded from user-facing conversation history,
thread titles, and derived grading transcripts even though it travels
through the model as a `HumanMessage`.
The notice includes:
- An actionable goal (`active` or `blocked`): objective and any
blocker/status note.
- An active rubric: acceptance criteria, including a one-turn rubric
that remains applicable while a saved goal is paused.
`update_goal` remains the model’s constrained status-reporting tool.
Creation, amendment, pause, resume, and clearing stay user-controlled.
The middleware refreshes the notice after goal/rubric changes and
re-pins it when compaction makes the saved copy invisible. It also
replaces superseded notices in the model request so stale objectives do
not compete with current state.
<details>
<summary>Safeguards</summary>
- State is validated and bounded before it can become automatic model
context; oversized restored state produces recovery guidance instead.
- Embedded objective, criteria, and status text is escaped and labeled
as user-provided context data, not higher-priority instructions.
- Regression coverage verifies detailed goal context cannot become a
user-visible thread title.
</details>
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
dcode now shows `Reviewing approval request` while Auto classifies
approval-gated actions.
---
Auto classifier latency previously looked like tool execution because
proposed rows stayed running and the global spinner stayed on
`Thinking`. This adds opaque lifecycle events, strict TUI validation,
and approved-only row resumption without changing authorization policy.
## References
- Plan:
https://openswe.vercel.app/agents/5f85a49d-3874-5a51-a563-5a242310ba5b/plan
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
`/trace` no longer claims a thread is empty while its first user turn is
still finishing.
---
The empty-thread check previously relied only on checkpoint state, which
can lag behind the visible response until the turn exits. It now
recognizes the locally active user message while preserving the existing
checkpoint and fail-open behavior.
Verified with focused `/trace` and conversation-message unit tests plus
scoped Ruff and ty checks.
Made by [Open
SWE](https://openswe.vercel.app/agents/af6a9bd2-0d10-5856-afdc-b7cca575d418)
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Deep Agents Code traces now expose whether the client is an editable
install as a filterable boolean.
---
Trace consumers currently must parse `lc_versions["deepagents-code"]`
for a `+editable` suffix. `build_stream_config` now emits an
always-present `editable` boolean derived from the same cached PEP 610
lookup, keeping both values consistent.
Tests: focused metadata contract and stream-config tests; `make
format_diff`; `make lint_diff`.
Made by [Open
SWE](https://openswe.vercel.app/agents/6e666be6-44be-5afd-abb0-945fac4d7912)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
<kbd>Shift</kbd> + <kbd>click</kbd> now extends an existing text
selection from its original anchor.
**Known limitation:** this only works when the terminal delivers the
modified click to the app. Ghostty binds <kbd>Shift</kbd> +
<kbd>click</kbd> to its own selection and never forwards the event while
mouse reporting is active, so it's a no-op there (users can `keybind =
shift+click=unbind` to opt out). Verified working in iTerm2; kitty and
WezTerm also forward the shift modifier bit.
---
Textual resets selection state on every mouse press. This preserves the
prior selection anchor for modified clicks while retaining stock
behavior when no selection exists or the target is not selectable.
Regression tests cover forward, backward, and multi-widget ranges.
The rebuilt selection state can field-match the finished drag selection,
which Textual reactives treat as unchanged — so the patch invokes the
(patched, now-async) select-state watcher directly to repaint the
highlight. The module docstring documents the terminal-delivery
limitation for maintainers.
Made by [Open
SWE](https://openswe.vercel.app/agents/4d90066c-60b9-51ed-b7a3-cce83ee4fd0b)
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Pressing Enter on "Notification settings" no longer opens the pane with
a beat of unselected content and a late-arriving footer hint, and the
trailing triangle no longer flips between glyphs as the section opens
and closes — the leading cursor now carries the disclosure state.
---
Two papercuts from #5698:
**Lag before the footer hints update.** The app constructs the center
without the suppressed-keys snapshot, so the first expand read
`config.toml` (four `asyncio.to_thread` calls) before mounting any
checkboxes, focusing the first one, or refreshing the footer. The pane
was visibly open but nothing was selected, and only then did the hint
switch to "Space/Enter toggle" — a one-frame flicker. The screen now
preloads the suppressed keys in a background worker when it mounts, and
`_expand_settings` flips the expanded state, disclosure glyph, and
footer hint synchronously before its first await, so the expanded hints
render in the same frame the pane opens. A rapid Esc during an in-flight
read both collapses the section and stops the expand worker from
reopening it.
**Trailing disclosure triangle.** The `▸`/`▾` after "Notification
settings" is removed. The leading glyph carries the state instead: `›`
when the row is selected and collapsed (Enter opens), `▾` whenever the
section is expanded (Esc collapses).
Modal footers now consistently advertise forward and reverse Tab
navigation, with specialized hints where Tab autocompletes, jumps
servers, or switches tabs.
---
Centralizes the standard copy, fixes app-level Shift+Tab routing for the
effort and launch preference selectors, and keeps the MCP footer compact
enough to preserve list navigation at standard terminal sizes.
Made by [Open
SWE](https://openswe.vercel.app/agents/1e0aca0d-8819-5754-acdc-4a08b1b2ac0c)
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Auto and Manual approval selections now carry across bare `dcode`
launches, while YOLO remains session-only unless explicitly configured.
---
Approval-mode switches now write an app-managed `[startup].recent`
value, resolved behind the intentional `[startup].mode` default and
validated fail-closed. This mirrors the existing recent model and agent
behavior.
Verified with the focused approval-mode, startup-config,
argument-resolution, and config-manifest tests (353 passed), plus
package format, lint, type, and command-catalog checks.
Made by [Open
SWE](https://openswe.vercel.app/agents/d0bca443-1400-5356-aecc-a6ee03f86560)
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
`Ctrl+N` and `/notifications` now open one notification hub where
warning settings expand inline under a "Notification settings"
disclosure row, alongside pending notices — including when the inbox is
empty.
---
This removes the separate warning-preferences modal. Toggling the
settings row mounts the warning checkboxes directly inside the hub, so
preferences never leave the notices list and closing them just collapses
the section. Existing notification detail, suppression, and in-place
action flows are unchanged.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
The `/auth` manager footer now names the action for the highlighted row
instead of listing every action the screen supports.
Before, one line had to cover every row:
```
↑/↓ or Tab/Shift+Tab navigate • Enter add/replace/delete/install • Esc close
```
Now the action follows the cursor:
| Highlighted row | Footer |
| --- | --- |
| `anthropic [env set: ANTHROPIC_API_KEY]` | `Enter replace` |
| `openai [stored]` | `Enter replace/delete` |
| `tavily` | `Enter add` |
| `groq [not installed]` | `Enter install` |
| `OpenAI Codex (ChatGPT login) [chatgpt: plus]` | `Enter manage` |
---
**An expired ChatGPT access token counts as signed in.** Expiry is the
normal state for a returning user: the saved refresh token renews the
access token when a model is constructed, which is why
`_get_codex_auth_status` reports the row as `CONFIGURED` and badges it
`[chatgpt]`. The footer reads that same status, so the badge, the
footer, and Enter agree. Treating expiry as signed-out would have told
the user to re-authorize a session that never lapsed, and would have
hidden sign-out — which only the signed-in overlay offers — behind that
unnecessary OAuth round trip.
Reading the cached status also means highlighting a row never touches
the token file. Arrowing over the ChatGPT row used to read
`chatgpt-auth.json` on every keypress.
**A stored key with no known env var offers replace/delete.** The set of
providers with a credential in `auth.json` is snapshotted when the
option list is built. Without it, a provider whose env var is absent
from `PROVIDER_API_KEY_ENV` — a custom `class_path` provider, or a
well-known one renamed across an upgrade — resolved to `UNKNOWN` and the
footer said `add`, while the prompt behind Enter offered `Ctrl+D delete`
for the key it had just denied existed.
**The footer follows the row, not the cursor index.** `_refresh_options`
preserves the highlighted index while configured rows float to the top,
so saving or deleting a key changes which provider sits under an unmoved
cursor. The rebuild recomputes the footer from the row that ended up
there.
Both footer writers share one helper that tolerates a dismissed manager.
A queued `OptionHighlighted` can arrive after the screen is gone, where
an unguarded `query_one` raises `NoMatches` and Textual escalates it
into a callback error that tears down the REPL.
Made by [Open
SWE](https://openswe.vercel.app/agents/d9dd1ec2-f3c6-5ff0-aaa2-da82e97fcbc7)
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Wrapped Debug Console snapshot values now align with the start of their
value column.
---
Long snapshot values previously wrapped at the modal's left edge, making
labels hard to scan. The snapshot view now applies a hanging indent
while preserving copy and link spans.
Made by [Open
SWE](https://openswe.vercel.app/agents/88a043f9-b5fc-5cea-9ade-548ea4d3c0dc)
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
The chat input's draggable top bar now follows the active theme whenever
`/theme` changes it.
---
The resize handle uses an inline color so hover feedback survives
captured drags, but that color was only refreshed for mode and hover
changes. Watching Textual's app theme keeps the existing mode-aware
styling synchronized without changing resize behavior.
Made by [Open
SWE](https://openswe.vercel.app/agents/23698746-0a5b-561b-8353-5799c952bbe7)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
A malformed `ask_user` call still surfaces to the model as a retryable
error `ToolMessage`. The validation now lives on the tool's pydantic
schema. The hand-rolled validation function, the `ToolErrorMiddleware`,
the `ToolArgumentError` type, the `on_error` handler, and the scope list
are all deleted.
## The rules on the schema
Per-field, on `Question`:
- `question` has an `AfterValidator` that rejects blank and
whitespace-only text. The field keeps `min_length=1`, but that
constrains only the JSON schema the model reads. The validator is the
inner annotation, so it runs first and is the only runtime gate.
- `type` stays a `Literal`. `required` stays a strict bool.
- Each `Choice` has an `AfterValidator` that rejects a blank `value`. A
blank value renders as an unlabelled option whose answer reads as "no
answer".
Cross-field, on the `ValidatedQuestion` alias and the `questions`
parameter:
- A choice question must have a non-empty `choices` list.
- A non-choice question must not have `choices`.
- `questions` must not be empty.
`AskUserRequest.questions` is `list[ValidatedQuestion]` too. The
cross-field rules then also apply where `tui.textual_adapter`
re-validates the interrupt payload with `TypeAdapter(AskUserRequest)`.
Before this, a choice question with no `choices` reached the client and
degraded to a text box.
## The tool wires no error handling
A rejection is a pydantic `ValidationError` raised during argument
parsing. `ToolNode` converts it to an error `ToolMessage`. The tool sets
neither `handle_validation_error` nor `handle_tool_error`, and that is
deliberate.
An earlier revision of this PR set `handle_validation_error` to get a
shorter message. That field is not part of the documented LangChain v1
surface, and the v1 migration guide lists schema mismatches under "do
NOT handle — already auto-handled by the framework". It also intercepts
inside `BaseTool.run`, which costs three things:
**Harness faults reach the model.** `tool_call_id` and `runtime` are on
the same `args_schema` as `questions`, so pydantic reports them the same
way. LangGraph strips them in `_filter_validation_errors`; a handler on
the tool runs earlier and bypasses that. The model cannot rewrite those
fields, so it retries until the recursion limit.
**Tracing records a success.** A handled error leaves `error_to_raise`
unset, so `BaseTool.run` calls `on_tool_end`. With no handler the
exception escapes, `on_tool_error` fires, and a rejected call traces as
an error.
**The model corrects one rule per retry.** The formatter named only the
first error. `ToolNode` lists all of them.
Measured on one call that breaks two rules:
```
with handle_validation_error:
callbacks: on_tool_end
content: `ask_user` failed: questions.0.question: Value error, question text
must not be blank. Fix the input and retry.
without (this PR):
callbacks: on_tool_error(ValidationError)
content: Error invoking tool 'ask_user' with kwargs {'questions': [...]} with error:
questions.0.question: Value error, question text must not be blank
questions.1: Value error, multiple_choice question 'Pick' requires a
non-empty 'choices' list
Please fix the error and try again.
```
The message is longer and echoes the model's own arguments. That is the
cost of the framework default, and it buys correct tracing,
injected-argument filtering, and one retry instead of two.
## Known limitation
A `ValidationError` raised by the tool body after parsing succeeds is
still reported to the model as bad input. `BaseTool.run` and `ToolNode`
both wrap the body in the same `try` as argument parsing, so this is
framework behavior, not specific to `ask_user`. Nothing in the body
raises one today: `_parse_answers` raises plain `ValueError`, which
stays fatal.
## Unchanged
The raw-payload backstop in `auto_mode` (`_ask_user_question_count`
re-reading unvalidated tool-call args off message history) does not
depend on how the tool validates. It still guards the same-turn
authorization path.
`dcode` can now be told not to read the *project* `.env` file. A new
`startup.read_project_dotenv` option (default `true`, preserving current
behavior) controls whether the nearest `.env` discovered walking up from
the working directory is applied to the process environment. Set
`DEEPAGENTS_CODE_READ_PROJECT_DOTENV=0`/`false`, or
`[startup].read_project_dotenv = false` in `~/.deepagents/config.toml`,
to skip the project file. The global `~/.deepagents/.env` still loads
either way.
---
Loading the project `.env` is the trust boundary tracked as T12 in
`libs/code/THREAT_MODEL.md`: the file travels with a cloned repo, so its
values are attacker-controlled input that lands in `os.environ` and
reaches every subprocess `dcode` spawns (including the startup `git`
detection that runs before any approval prompt). The denylist from #4288
and the git keys from #5723 are best-effort enumerations of known
execution hooks — this option gives a user (or an org's managed config)
a way to close the boundary entirely when working in untrusted trees,
rather than waiting for the next unlisted hook to be reported.
Design points:
- **Default-on** so existing workflows (API keys, `DEEPAGENTS_CODE_*`
settings carried in a project `.env`) keep working; this is strictly an
opt-out.
- **Scoped to the project file only.** The user's own global
`~/.deepagents/.env` is trusted and unaffected.
- **Cannot self-veto.** The option resolves from managed config →
`DEEPAGENTS_CODE_READ_PROJECT_DOTENV` → user `~/.deepagents/config.toml`
→ default. All of those are read *before* any project `.env` is applied,
and the env var lives in the user-controlled process env (not a repo
file), so a project `.env` cannot turn the toggle off (or back on) for
itself.
- Wired as a first-class manifest option (`startup.read_project_dotenv`,
BOOL, default `True`) so `dcode config get startup.read_project_dotenv`
reports the resolved value and source like any other option. Resolution
is a thin `resolve_read_project_dotenv` over the standard ranked engine;
`_load_dotenv` consults it before the project-file branch and skips to
the global file when false. The dry-run `_preview_dotenv_environ` is
unchanged — it reports what a real reload would load, and a real reload
now loads nothing from the project file when the option is off.
Auto fallback prompts now explain that review is needed without exposing
internal denial and availability counters.
---
Threshold reasons and counters remain available at `DEBUG`. The
transcript and approval description use plain language while preserving
the Switch-to-Manual flow and shell allow-list guard.
Made by [Open
SWE](https://openswe.vercel.app/agents/434260c2-4bed-5449-a0b4-95de592be3ec)
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>