Commit Graph

39 Commits

Author SHA1 Message Date
Mason Daugherty feb5736dfb feat(code): support DEEPAGENTS_HOME (#5773)
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>
2026-08-25 22:09:15 -04:00
Mason Daugherty 25aa2735da chore(code): render stale-PATH hint as next step, not warning (#5618)
When the installer successfully updates the shell profile, the running
shell still lacks `~/.local/bin` on PATH until it's reloaded. That case
was rendered with the same yellow warning styling as a genuine profile
setup failure, even though the install succeeded and new terminals work
immediately. Now it prints a cyan info line with a paste-ready command
instead:

    ▸ To use dcode in this shell, run:
      > source ~/.local/bin/env

---

`ensure_path_setup` returns distinct codes for "profile setup failed"
(rc=1, already accompanied by a specific warning) and "profile written,
running shell just stale" (rc=2). The footer previously lumped both
under one `log_warn "Restart your shell, or run:"` block, so a fully
successful install could end on a warning. The rc=2 path now renders as
a `log_info` next-step hint styled like a prompt (`> command`), matching
how other installers (e.g. fx) present the same affordance. The rc=1
path keeps the warning styling since something genuinely went wrong.

Adapted from the fx install script (https://releases.fx.sh).
2026-08-18 18:26:53 -04:00
Mason Daugherty f4da16e7fd chore(code): print live tail command and say "Upgraded." in installer (#5371)
During an update, the installer now prints the live `tail -f` command
for the install log (e.g. `Update log: tail -f
~/.cache/deepagents-code/install.log`) so you can watch progress in
another terminal, and a successful update ends with `Upgraded.` instead
of the neutral `Version changed.`.

---

Two user-visible installer changes:

**Live update-log hint.** Previously uv's output went to a mktemp
scratch file and was only copied to
`~/.cache/deepagents-code/install.log` after the install finished, so
there was nothing to watch mid-install. Unprivileged runs now stream
uv's output directly to the persistent log file — matching what the
built-in updater already does — and the script prints the `tail -f`
command whenever it starts an update.

**"Upgraded." footer.** An unpinned, default-prerelease run that moved
versions always printed `Version changed.` because the script couldn't
distinguish a real upgrade from uv resolving an older package via a
custom index. The script now records when it deliberately moved to the
PyPI latest it had just fetched and confirmed differed from the
installed version, and only that path prints `Upgraded.` — pinned
downgrades and custom-index moves stay neutral as before.
2026-08-11 11:15:22 -07:00
Mason Daugherty f69804bfce fix(code): store update logs under the OS cache dir (#5363)
Update logs from self-updates (`uv tool install` / `pip install` output)
are ephemeral diagnostics, not application state. This moves
`UPDATE_LOG_DIR` from `~/.deepagents/.state/update_logs/` to
`<cache>/deepagents-code/update_logs/`, where `<cache>` is the
platform-native cache directory (`~/Library/Caches` on macOS,
`%LOCALAPPDATA%` on Windows, `$XDG_CACHE_HOME` or `~/.cache` elsewhere).

---

Before this change, every self-update wrote a timestamped log under
`~/.deepagents/.state/update_logs/`. That directory also holds OAuth
tokens, the sessions database, and input history — things users expect
to persist — while the update logs are transient `uv`/`pip` output that
the retention policy (`UPDATE_LOG_RETENTION_DAYS = 14`,
`UPDATE_LOG_MAX_FILES = 10`) already treats as disposable. A cache
directory is the more correct home.

- New `default_cache_dir()` resolver lives next to `DEFAULT_STATE_DIR`
in `model_config.py`: `~/Library/Caches` on macOS, `LOCALAPPDATA` on
Windows (falling back to `~/AppData/Local`), and `XDG_CACHE_HOME`
elsewhere when it is an absolute path (falling back to `~/.cache`).
Relative `XDG_CACHE_HOME` values are invalid per the XDG spec and are
ignored rather than resolved against the launch directory.
- Resulting `UPDATE_LOG_DIR` values:
    - macOS: `~/Library/Caches/deepagents-code/update_logs/`
    - Windows: `%LOCALAPPDATA%/deepagents-code/update_logs/`
- Linux with `XDG_CACHE_HOME` set:
`$XDG_CACHE_HOME/deepagents-code/update_logs/`
    - Linux without it: `~/.cache/deepagents-code/update_logs/`
- The install script writes its own `install.log` under
`${XDG_CACHE_HOME:-~/.cache}` on every platform, so on macOS the two
logs intentionally land under different roots. Each side follows the
convention appropriate to it — a portable one-shot POSIX bootstrap (like
the rustup and uv installers) versus a long-lived app (like
`platformdirs`, and `uv`'s own cache at `~/Library/Caches/uv`). Both
docstrings now state this explicitly so the divergence isn't later
"fixed" by mistake.
- `create_update_log_path` / `cleanup_update_logs` signatures and the
`OSError`-tolerant debug-log-and-continue behavior in `perform_upgrade`
are unchanged — a non-writable cache dir must not break updates. The
timestamped `<stamp>-update.log` naming and retention policy are
untouched. Existing logs under the old
`~/.deepagents/.state/update_logs/` path are simply orphaned; nothing
reads them back.
- Nothing in the codebase reads update logs back (verified: write-only
across the package), so no reader migration is needed. No docs/help text
referenced the old path.

<details>
<summary>Test plan</summary>

- New `TestDefaultCacheDir` in `test_model_config.py`: XDG
set/unset/empty/relative, macOS with and without `XDG_CACHE_HOME`,
Windows with and without `LOCALAPPDATA`.
- `test_update_check.py` (388 tests, including the existing
`UPDATE_LOG_DIR` patch-based fixture) passes unchanged.
- `make -C libs/code lint` (ruff + ty + format) passes.

</details>
2026-08-06 21:59:50 -04:00
Mason Daugherty 6eb3748ce6 chore(code): harden installer ripgrep gating and prompt semantics (#5344)
Hardens the `dcode` install script (`curl ... | bash`) in two ways,
adapted from patterns in Prime's installer.

---

## Gate system ripgrep on a minimum version

The `system` ripgrep path (brew/apt/dnf/pacman/zypper/apk/nix/cargo)
used to count any `command -v rg` after install as success — an ancient
distro package would satisfy it and then behave differently at runtime.
Each branch now checks the resulting `rg --version` against a floor
(12.0.0, below the 14.1.1 the managed installer pins) and only succeeds
when it clears it; a too-old or unprobeable pre-existing `rg` warns
instead of silently passing, and a failed version probe no longer aborts
the installer.

## Three-valued `prompt_yn`

`prompt_yn` returned the same code for "user said no" and "no terminal
exists to ask on," forcing callers to guess. It now returns 0 (yes), 1
(no), or 2 (no usable terminal). The upgrade prompt completes the update
on the no-terminal case (cron/CI/systemd) rather than conflating it with
a decline, the PATH-setup prompt declines only on an explicit "no," and
the extras-removal prompts continue (with a warning) when nobody could
be asked.
2026-08-06 20:42:19 -04:00
Mason Daugherty 21fd0d6794 chore(code): surface update status, log path, and extras loss in installer (#5342)
The install script now leads an available update with an explicit
`Update available: deepagents-code X → Y` verdict before the changelog
link, prints the persistent uv install log path on every run (not just
on failure), ends a real upgrade with `Upgrade complete.` instead of
`Setup complete.`, and warns before a bare re-run silently drops extras
the existing install was built with — including the
`DEEPAGENTS_CODE_EXTRAS` re-run command that preserves them.

---

Before / after:

**Update prompt** — the changelog link previously appeared without first
stating that an update exists:

```console
# Before
▸ What's new: https://github.com/langchain-ai/deepagents/releases/tag/deepagents-code%3D%3D0.2.0
Update deepagents-code 0.1.0 → 0.2.0? [y/N]

# After
▸ Update available: deepagents-code 0.1.0 → 0.2.0
▸   What's new: https://github.com/langchain-ai/deepagents/releases/tag/deepagents-code%3D%3D0.2.0
Install update? [y/N]
```

**Extras loss** — re-running the installer without
`DEEPAGENTS_CODE_EXTRAS` rebuilds the environment against the bare
package and silently removes every extra the install was built with
(model providers, sandbox backends, etc.). The extras are now read off
uv's own `uv-receipt.toml` and called out, with the exact re-run
command, before that happens. An interactive user can also back out;
explicitly passing extras or doing an editable→PyPI swap already
expresses intent, so those skip the warning:

```console
# Before
(no output; the extras are gone after the reinstall)

# After
⚠ This install has extras that a bare re-run will remove: anthropic,daytona
⚠   To keep them, re-run with: DEEPAGENTS_CODE_EXTRAS="anthropic,daytona"
Continue anyway and remove them? [y/N]
```

A receipt that exists but can't be read or parsed (symlink, unreadable
permissions, unexpected format) warns too — a false negative here costs
the user the exact packages this check protects.

**Footer** — the footer now distinguishes a version move from a fresh
install:

```console
# Before (any successful run)
✔ Setup complete. Run: dcode

# After (unpinned run that moved version)
✔ Upgrade complete. Run: dcode
```

Other outcomes get their own wording: `Dependencies updated.` for a
same-version dependency bump and `Already installed.` for a no-op. A
pinned move (`bash -s -- 0.1.0` over 0.2.0 is a downgrade, and there's
no version comparison to tell) keeps `Setup complete.`

**Log path** — uv's full stderr (dependency diff, rebuild notice) is
captured to a persistent log, but the script only mentioned it on
failure or a same-version dependency bump, so on a normal upgrade the
details scrolled away with no way back. It's now printed after any
successful install that wrote a log:

```console
# After
✔ deepagents-code upgraded to 0.2.0.
▸ Full log: ~/.cache/deepagents/install.log
```

The log stays under `~/.cache` (not `~/.deepagents/.state`) because it's
ephemeral output, not application state.
2026-08-06 02:10:05 -04:00
Mason Daugherty 63aa129036 chore(code): harden installer downloads, PATH setup, and shell coverage (#5336)
Hardens the installer's download path and reworks PATH setup to cover
every shell the user might open, adopting patterns from Meta's `muse`
installer.

## PATH setup

- The PATH block is written to **every shell startup file that exists**
— e.g. both `~/.bashrc` and `~/.bash_profile`, or a `ZDOTDIR` zshrc plus
a legacy `~/.zshrc` — so `dcode` resolves even if the user later
switches shells. One interactive prompt names every file the answer
covers.
- The line written to each file is chosen by **the target file's syntax,
not the current shell**: only fish's `conf.d` file gets fish syntax;
`.zshrc`, `.bashrc`, `.bash_profile` and `.profile` always get the POSIX
export.
- Fish gets a standalone `conf.d/deepagents-code.fish` (auto-sourced by
fish, honors `XDG_CONFIG_HOME`) instead of an edit to `config.fish`.
Re-running replaces the managed block rather than appending another one.
- `ZDOTDIR` is respected, from the environment or parsed out of
`~/.zshenv`, and is honored even when the directory doesn't exist yet —
zsh reads only `${ZDOTDIR}/.zshrc`, so the installer creates the
directory and writes there rather than falling back to a `~/.zshrc` zsh
would never read. A pre-existing `~/.zshrc` is still updated alongside,
so unsetting `ZDOTDIR` later doesn't strand the entry.
- **Only a top-level `ZDOTDIR=` counts.** The portable-dotfiles idiom
guards the relocation:

  ```sh
if [ -d "$HOME/.config/zsh" ]; then export ZDOTDIR="$HOME/.config/zsh";
fi
  ```

While that directory is absent the guard is false and zsh reads
`~/.zshrc`. Honoring the assignment anyway would make the installer
*create* the directory, flipping the guard true — after which zsh reads
only the new dir, holding nothing but the PATH block, and silently stops
sourcing the user's aliases, prompt and plugins. Assignments nested in a
block are skipped. An unreadable `~/.zshenv` warns rather than silently
falling back.
- Symlinked startup files keep their links: the rewrite follows the
chain to the final regular file and replaces it with a temp-file + `mv`
in that file's directory, so an interrupted install can't leave a
chezmoi/stow/home-manager source truncated and the next `apply`/`restow`
can't revert the entry. The original mode is carried over, and a failure
to restore it is reported rather than silently tightening the file to
0600.
- `DEEPAGENTS_CODE_NO_MODIFY_PATH` opts out for version-managed dotfiles
and MDM fleets: the binary is still installed and verified, no startup
files are touched, and the manual export line is printed. It accepts the
same *truthy* spellings as `DEEPAGENTS_CODE_YES` and treats an
unrecognized value as "do not modify" — an opt-out protecting managed
dotfiles has to fail toward leaving them alone.
- Writing a startup file never reports "PATH is fixed for the current
shell", so the reload hint is always emitted and a failure on any
candidate is surfaced rather than masked by a success elsewhere. The
inverse holds too: **declining the prompt writes nothing, so it does not
print "Restart your shell"** — a restart can't pick up an edit that was
never made.
- Failure diagnostics name what actually failed. `mkdir`/`touch`
failures keep their error text, and the managed-block rewrite hedges its
hint, since that path can also fail on an unresolvable symlink, a
`mktemp` failure or a failed `mv`.

Three deliberate divergences from muse's profile coverage:

- On macOS, a missing `~/.bashrc` is not created — Terminal runs login
shells that never read it. An existing `.bashrc` is still updated.
- `~/.profile` is written when it is the file a bash login shell will
actually read (bash files exist but neither `~/.bash_profile` nor
`~/.bash_login` does), or when the current shell is neither zsh, bash,
nor fish.
- The login-file fallback tests what the run is *going to create*, not
only what is on disk. On macOS, bash with no dotfiles has
`~/.bash_profile` queued as its primary file; checking the filesystem
alone would add `~/.profile` too and create both.

## Downloads

- curl pins HTTPS for the request and any redirects (`--proto '=https'
--proto-redir '=https'`) and caps redirect chains at 3.
- wget has no `--proto-redir` equivalent: `--https-only` only applies in
recursive mode, so it doesn't stop a 3xx from downgrading a one-shot
fetch (verified against wget 1.25.0 — it follows a 302 to plaintext and
exits 0 where curl refuses). `wget_download` therefore rejects non-HTTPS
URLs outright and audits the response headers for a downgrading
`Location`, failing closed. `--https-only` and `--max-redirect=3` are
still passed where supported.
- Hardening flags are probed against `wget --help` first, so BusyBox
wget (Alpine) still works instead of dying on an unrecognized option.
The probe **captures** the help text before matching rather than piping
into `grep`: BusyBox exits 1 from `--help`, and under `set -o pipefail`
that status would become the pipeline's even on a match, reporting every
option as unsupported. That fails *open* — it drops `-S` and reduces the
redirect audit to a no-op on exactly the minimal systems the probe
exists for. When `-S` genuinely isn't available, the missing audit is
reported rather than degrading in silence.
- The downloaded uv installer is parse-checked before execution with the
interpreter named in its own shebang, catching truncations that leave
unbalanced syntax (the shebang check alone would pass them). That same
interpreter then runs it — checking with bash and executing with `sh`
would let bash-only syntax pass the check and fail at execution, with
the user pointed at a nonexistent truncated download.
- Signal traps exit with the conventional 128+signal codes (129/130/143)
so CI and wrappers can distinguish an interrupted install from an
ordinary failure. The handler defaults its signal argument: under `set
-u` an unbound `$1` would abort the handler and let the EXIT trap print
the contradictory "Installation failed" line.
- Download retry backoff doubled (2s then 4s) — a 1s/2s wait is too
short to outlast the transient it absorbs, and 6s total is still well
inside a user's patience.
2026-08-05 21:14:41 -04:00
Mason Daugherty e3d2a870b7 feat(code): show changelog link before install-script update prompt (#5034)
The `deepagents-code` install script now shows a "What's new" changelog
link before prompting to update.

---

When the `deepagents-code` installer detects a newer version, the
interactive `curl … | bash` path now prints a `What's new: <release
URL>` line just before the `Update … → …? [y/N]` prompt, so users can
review what changed before agreeing to upgrade.

The link points at the version-specific GitHub release tag
(`deepagents-code==X.Y.Z`, with `==` percent-encoded so the tag URL
resolves). It is shown only on the interactive prompt path — the
auto-update/no-TTY, assume-yes, and pinned-version paths are unchanged.

Made by [Open
SWE](https://openswe.vercel.app/agents/7ce89a24-4597-59c8-d31e-2031bd2f90ed)

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-07-24 01:44:32 -04:00
Mason Daugherty 9a5bbeb755 fix(code): harden installer downloads and paths (#4871)
The install script now respects the configured uv tool binary directory,
retries transient downloads, and avoids unsafe fallback files and broad
ownership changes. Existing installations that are current but shadowed
on `PATH` are reinstalled into the uv tool directory instead of being
incorrectly treated as up to date.

---

The installer previously assumed uv placed executables in
`~/.local/bin`, so custom uv configuration could make version detection,
verification, `PATH` setup, and ownership repair target the wrong files.
A single transient PyPI or uv bootstrap failure also immediately fell
back or failed, and predictable `/tmp` fallbacks were used when `mktemp`
was unavailable.

This resolves the tool bin through `uv tool dir --bin` with legacy
fallbacks, compares executable paths by inode, preserves existing
regular files when adding links, retries downloads three times, and
fails closed for required temporary files. Root and MDM installs avoid
running pre-existing user executables and narrow ownership changes to
paths created by the installer.

Regression tests cover custom and legacy uv bin locations, shadowed
installs, root execution, retries, and secure temporary-file failures.
2026-07-21 11:22:59 -04:00
Mason Daugherty aad530c099 chore(code): compare installs by inode in shadowing check (#4577)
The installer no longer prints a spurious "shadowing install" warning
when `~/.local/bin` appears on `PATH` under a non-normalized alias (such
as `~/.local/share/../bin`) that resolves to the same directory.

---

The `deepagents-code` installer printed a spurious "shadowing install"
warning on every run for users whose `PATH` contains a non-normalized
alias of `~/.local/bin` — most commonly `~/.local/share/../bin`, where
`share/..` collapses back to `.local`. Some third-party tools write that
un-normalized spelling into a shell profile (derived from
`$XDG_DATA_HOME/../bin`), and it can sort before the canonical entry.
Both spellings resolve to the same uv-tool symlink the installer just
created, but `detect_shadowing_install` compared `PATH` strings, so the
alias looked like a separate, older install.

The primary fix makes the guard compare by inode using bash's `-ef` test
(same device+inode, resolves `..` and symlinks) in addition to the
existing string equality, so same-file aliases short-circuit and never
warn. Both operands are guaranteed to exist at that point, and a
genuinely different binary at a distinct inode still fails `-ef` and
correctly warns, so only the false positive is suppressed. Secondarily,
`local_bin_in_profile` now also recognizes the `~/.local/share/../bin`
spelling so the installer doesn't append a duplicate `~/.local/bin` PATH
line; this covers the common alias only and isn't a full path
normalizer.

Made by [Open
SWE](https://openswe.vercel.app/agents/870c16ab-41c7-3eee-e0d1-55385367ec4f)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-07-08 18:50:56 -04:00
Mason Daugherty cac389c344 chore(code): trim up-to-date installer output (#4576)
The installer's "already installed / up to date" message is now more
concise.

Before:
```
▸ deepagents-code 0.1.34 found — checking for updates...
✔ deepagents-code is already up to date.
```
After:
```
▸ dcode 0.1.34 found — checking for updates...
✔ Already up to date!
```

Made by [Open
SWE](https://openswe.vercel.app/agents/6b29a6dc-fc2b-12bc-52d1-98a3bed3785d)

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-07-08 18:35:47 -04:00
Mason Daugherty f183c1b217 chore(code): install script enhancements (#4571)
**deepagents-code installer (`install.sh`):**
- Accepts a positional version: `curl -LsSf https://langch.in/dcode |
bash -s -- 0.1.0rc1`
- Uses a managed, rewriteable PATH block in shell profiles (`# >>>
deepagents-code installer >>>`)
- Warns when an existing `dcode`/`deepagents-code` on `PATH` shadows the
uv tool install
- Shows actionable hints for signal kills (e.g., OOM on Linux: exit 137)
- Serializes concurrent installs with a cross-process lock
(`flock`/`mkdir` fallback)
- Accepts `DEEPAGENTS_CODE_YES=true|yes|TRUE| YES ` for non-interactive
installs
- Propagates uv's real exit code on failure

---

### User-facing changes
- **Positional version argument**: `curl ... | bash -s -- 0.1.0rc1` now
works alongside the existing `DEEPAGENTS_CODE_VERSION` env var
- **Managed PATH block**: Shell profiles now get a delimited `# >>>
deepagents-code installer >>>` block that the installer owns and
rewrites in place (instead of appending loose lines)
- **Shadowing detection**: Warns when an older `dcode`/`deepagents-code`
binary earlier on `PATH` would be used instead of the newly installed uv
tool
- **Signal-aware failure hints**: Exit codes ≥128 now surface actionable
messages (e.g., exit 137 on Linux → "ran out of memory, free up memory
or use a larger machine")
- **Concurrent install safety**: A cross-process lock (`flock`
preferred, `mkdir` fallback with stale-lock reclamation) prevents racing
`curl | bash` runs from corrupting the shared uv tool directory

### Internal hardening
- `DEEPAGENTS_CODE_YES` now accepts truthy values (`true`, `TRUE`,
`yes`, ` YES `)
- Installer propagates uv's real exit code instead of flattening to `1`
- Early up-to-date exit no longer creates a lock directory
- macOS path avoids `lockf` (command-scoped, not fd-based)
2026-07-08 17:31:50 -04:00
Mason Daugherty bdd4f1e014 chore(code): add --help/--version flags and verify uv installer download (#4503)
The `dcode` install script now supports `--help`/`-h` and
`--version`/`-v` flags, and the uv installer download is verified before
execution to guard against non-shell responses from proxies or captive
portals.

## Release note

- `curl -LsSf https://langch.in/dcode | bash -s -- --help` now prints a
full usage reference (all environment variables, valid extras, and a
docs link) and exits 0 without touching the network. `--version`/`-v`
prints the installer version string and exits 0. Unknown flags (e.g. a
typo like `--verison`) are rejected with a non-zero exit and an
actionable message instead of silently proceeding to a full install.
- The uv bootstrap no longer pipes the downloader directly into `sh`. It
downloads the installer to a tempfile first, verifies the first line is
a shell shebang, and only then executes it. A 200 response containing
HTML from a transparent proxy or captive portal is caught and rejected
with a clear error before it can reach `sh`.
- On a failed download, the downloader's own stderr (curl DNS errors,
TLS failures, HTTP status) is now surfaced to the user alongside the
generic failure message, instead of being discarded.

## Changes

- Added a CLI-flag loop at the top of `install.sh` that handles
`--help`/`-h` (prints a `print_help` reference of all env vars and valid
extras), `--version`/`-v` (prints `INSTALLER_VERSION`), and rejects
unknown flags with exit code 2 — all before any install work begins.
- Rewrote `install_uv` to download the uv installer to a tempfile via
`curl -o` / `wget -O` (capturing the downloader's stderr) instead of
piping directly to `sh`. On download failure, the captured stderr is
printed before the generic error. On success, the script's first line is
checked for a shell shebang (`^#!.*(sh|bash)`) before execution; a
non-shell response exits with an actionable message.
- Updated the `media` extra in the env-var documentation within the help
text (was previously listed only as `quickjs` under standalone
integrations).
- Added unit tests covering: `--help`/`-h` output and early exit,
`--version`/`-v` output and early exit, unknown-flag rejection, shebang
verification rejection (HTML response), download-failure error
surfacing, and the wget download path with verbose output.
2026-07-04 21:29:56 -04:00
Mason Daugherty aa9ccc6201 chore(code): automatic PATH setup, snap-curl detection, and temp cleanup in install script (#4464)
The installer now actively fixes `dcode` PATH resolution instead of just
warning the user, detects snap-sandboxed `curl` that silently fails
downloads, and cleans up temp files on Ctrl-C without printing a
contradictory failure message.

## Changes
- **Automatic PATH setup** — `ensure_path_setup` symlinks the verified
binary into an existing PATH dir (`~/.local/bin`, `~/bin`, `~/.bin`); if
none are on PATH, creates `~/.local/bin`, symlinks there, and appends a
PATH export to the detected shell profile (`.zshrc`, `.bashrc`,
`.bash_profile`, `config.fish`). Prompts interactively before modifying
a profile, auto-adds in non-interactive mode, and skips entirely when
uv's `~/.local/bin/env` already handles PATH.
- **Snap `curl` detection** — `is_snap_curl` checks whether `curl` lives
under `/snap/`, in which case it has sandbox permission issues that
cause silent download failures. `install_uv` and the new
`download_to_stdout` helper fall back to `wget`, or emit a clear error
if no working downloader exists.
- **Interrupt-safe temp cleanup** — a `TEMP_FILES` registry
(`register_temp` / `cleanup_temp_files`) tracks every `mktemp` so
they're removed on both normal exit and Ctrl-C. A dedicated
`cleanup_on_interrupt` handler for INT/TERM disarms the EXIT trap first,
so the user sees only "Installation interrupted." rather than that
followed by a contradictory "Installation failed (exit code 1)."
- **`download_to_stdout` helper** — consolidates the duplicated
curl-or-wget logic from `install_uv` and `fetch_latest_version` into one
function with snap detection built in.
2026-07-03 02:30:02 -04:00
Mason Daugherty a52c18d3ef fix(code): quiet routine ripgrep installer output (#4417)
`dcode` install no longer prints routine ripgrep setup messages unless
verbose mode is enabled or ripgrep setup fails.

---

Routine ripgrep provisioning is an implementation detail of `dcode`
installation, so successful managed setup no longer adds extra status
lines to default installer output. Verbose installs and failure paths
still expose the setup command output for debugging.
2026-07-01 20:20:51 -04:00
Mason Daugherty 8fca61dc03 feat(code): add rubric-backed goal workflow (#4365)
Adds rubric-driven acceptance criteria to Deep Agents Code and layers a
goal workflow on top of it.

A rubric is the explicit definition of done. Use it when you already
know the criteria the agent should satisfy before it considers the work
complete. Criteria can be sticky for the thread, one-shot for the next
turn, or loaded from a file. While the agent works, the TUI shows
grading lifecycle messages so the user can see when the work is being
checked, revised, satisfied, or stopped.

Usage examples:

```text
/rubric set tests pass; no unrelated files changed; help text is updated
/rubric next only change the auth callback; do not refactor unrelated code
/rubric file acceptance.md
/rubric show
/rubric clear
```

A goal is the objective-oriented workflow. Use it when you know the
outcome, but want `dcode` to propose acceptance criteria before
execution. `/goal <objective>` asks the model to draft criteria and
displays them in an inline review prompt. The user can accept the
criteria, edit them directly, reject them with feedback to regenerate,
or cancel. Accepted goal criteria become the sticky rubric for the
thread.

Usage examples:

```text
/goal add OAuth refresh handling
/goal show
/goal clear
```

The `--goal` CLI flag starts the same goal-review workflow when
launching the TUI. It is intentionally interactive: the generated
criteria must be reviewed before execution. After the user accepts the
proposal, the accepted goal is sent as the first task.

```bash
dcode --goal "add OAuth refresh handling"
```

`--goal` cannot be combined with `-n`, `-m`, `--skill`, or `--rubric`.
For non-interactive/headless runs, users should provide criteria
explicitly with `--rubric` instead:

```bash
dcode -n "implement OAuth refresh handling" --rubric "tests pass; no unrelated files changed"
dcode -n "implement OAuth refresh handling" --rubric @acceptance.md
```

Accepted goal/rubric state is persisted on the thread and restored on
resume. For example, a user can set `/goal add OAuth refresh handling`,
accept the proposed criteria, quit the TUI, and later resume the same
thread with the goal and criteria still active. That matters because the
acceptance criteria continue to guide future turns and remain visible in
`/goal show` instead of becoming hidden context that disappears between
sessions.

When the agent believes the goal is done, it does not get to declare
victory on its own. For example, after implementing OAuth refresh
handling, the agent can ask to mark the goal complete with evidence such
as "tests pass." `dcode` keeps that request pending until the rubric
check finishes. If the rubric still needs revision, the goal stays
active and the user sees why it was not completed. If the rubric is
satisfied, auto-approve mode records the completion automatically;
manual mode asks the user before changing the goal status. This keeps
the user's accepted criteria as the source of truth for whether the goal
is actually finished.

The active criteria and goal are also visible to the agent through
constrained tools. `get_rubric` lets the agent inspect the current
criteria and whether they came from a goal, a sticky rubric, or the
current invocation. `get_goal` lets it inspect the active objective,
status, criteria, and any prior note. `update_goal` lets it report when
it believes the goal is `complete` or `blocked` with evidence. The
constraints matter: the agent can read criteria and update progress, but
it cannot create, pause, resume, clear, or replace goals. Those
lifecycle actions stay user/system controlled so the model cannot
silently redefine or remove the user's objective.
2026-06-29 16:20:18 -04:00
Mason Daugherty e645adf7ec hotfix(code): avoid reinstalling existing uv (#4314)
The install script now resolves an existing `uv` before invoking the
upstream installer, including installs that are present in
`~/.local/bin` but hidden by a minimal MDM or cron PATH. Bad `UV_BIN`
values fail fast with a clear error instead of falling through to a
network install attempt.

## Changes
- Added `resolve_uv_bin` to check `UV_BIN`, PATH, uv’s generated env
file, and `~/.local/bin/uv` before installing uv.
- Hardened env-file sourcing so stale or hand-written `~/.local/bin/env`
files with non-zero commands do not abort the installer.
- Tightened `UV_BIN` path validation so directories are rejected before
`uv tool install` runs.
- Added installer regression coverage for minimal-PATH uv detection,
defensive env-file sourcing, and invalid `UV_BIN` values.
2026-06-26 03:18:52 -04:00
Mason Daugherty 09ad92f637 hotfix(code): allow prerelease dependencies in installer (#4308)
Follow-up to fd0b17deb0, which changed the
installer to use `--prerelease if-necessary` but still allowed uv to
resolve `deepagents-code==0.1.22` instead of the latest release that
pins `deepagents==0.7.0a2`.

`if-necessary` is not enough here because uv can satisfy the top-level
`deepagents-code` requirement with an older stable candidate. The
installer now defaults to `--prerelease allow`, which lets the latest
stable `deepagents-code` release resolve even when it depends on a
prerelease SDK.
2026-06-26 02:23:19 -04:00
Mason Daugherty fd0b17deb0 hotfix(code): use if-necessary for installer prerelease resolution (#4306)
Stable `deepagents-code` releases can temporarily depend on pre-release
packages, which made the default installer path fail when uv refused
pre-release resolution. The installer now applies uv's `if-necessary`
strategy for unpinned installs while keeping exact version pins mutually
exclusive with explicitly requested pre-release settings.

## Changes

- Default unpinned installs to `--prerelease if-necessary`, but avoid
forwarding that default when `DEEPAGENTS_CODE_VERSION` selects an exact
package version.
- Keep the mutual-exclusion error for explicit
`DEEPAGENTS_CODE_PRERELEASE` plus `DEEPAGENTS_CODE_VERSION`, preserving
pinned install behavior.
- Filter uv download/build progress and newer timing summary variants
from non-verbose output while preserving them under
`DEEPAGENTS_CODE_VERBOSE=1`.
- Shorten the managed ripgrep setup message so the installer no longer
advertises the opt-out path during the normal setup flow.
2026-06-26 02:15:49 -04:00
Mason Daugherty 220dfc0e6b fix(code): defer server graph construction (#4300)
Import-only checks were constructing the server graph, which let runtime
startup behavior leak into local validation and touch a developer’s real
dcode config. The server graph is now exposed as a LangGraph factory so
MCP discovery runs only when the server actually builds the graph, and
the import checker runs against an isolated home directory.

## Changes

- Switch the generated LangGraph reference from `server_graph.py:graph`
to `server_graph.py:make_graph`, relying on LangGraph’s factory support
instead of constructing the graph at module import time.
- Keep startup error marker behavior inside `make_graph()` so server
startup failures still surface cleanly to the parent process, while
plain imports remain side-effect free.
- Run `check_imports.py` with a temporary `HOME` so import validation
cannot read or depend on local `~/.deepagents` config, MCP auth tokens,
or other user state.
- Update server graph tests to assert MCP discovery does not happen on
import and still happens when `make_graph()` is invoked.
- Override `UV_FROZEN` only for the `uv lock --check` command so `make
check` performs the real lockfile freshness check without warning.

## Testing

- `make -C libs/code check PYTHON_FILES=
PYTEST_EXTRA="tests/unit_tests/test_server_graph.py
tests/unit_tests/test_server_manager.py -q" COV_ARGS=`
- `make -C libs/code check_imports`
- Focused ruff, ty, and pytest checks for the touched server
graph/server manager files
2026-06-26 00:11:29 -04:00
Mason Daugherty cf536f3399 fix(code): eager managed ripgrep install via dcode tools install (#4199)
`dcode tools install` provisions the managed ripgrep binary; the install
script now sets it up by default (opt out with
`DEEPAGENTS_CODE_RIPGREP_INSTALLER=system` or
`DEEPAGENTS_CODE_OFFLINE=1`).

---

Today the pinned, SHA-256-verified ripgrep binary only lands on first
run (via `managed_tools.ensure_ripgrep`), so a fresh user pays a
one-time download the first time they launch. This makes the install
script provision it eagerly so `rg` is ready immediately, without
reaching for `sudo` package managers by default.

Rather than re-encoding the pinned version + checksum table in bash
(drift risk), a new `dcode tools install` verb reuses the existing
managed-install code, and `scripts/install.sh` invokes the freshly
installed binary. The verb is also handy on its own to repair a missing
or stale `rg`.

Power users can keep their own toolchain with
`DEEPAGENTS_CODE_RIPGREP_INSTALLER=system`, which preserves the existing
brew/apt/cargo path in the install script and skips the managed download
at runtime. A system `rg` already on `PATH` is reused under either
setting, and `DEEPAGENTS_CODE_OFFLINE` / `DEEPAGENTS_CODE_SKIP_OPTIONAL`
continue to apply. The default eager install is announced and honors
those opt-outs, per the package's "default shell-outs must announce +
offer a documented opt-out" rule.

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-25 01:28:38 -04:00
Mason Daugherty 5d080df277 fix(code): drop redundant version from "already up to date" message (#4223)
The `libs/code` install script already prints the version on the
preceding `deepagents-code <ver> found — checking for updates...` line,
so repeating it on the `is already up to date` status line is redundant.
Simplified both the `log_info` (rebuild-with-extras) and `log_success`
(no-op exit) lines to `deepagents-code is already up to date`.

Made by [Open
SWE](https://openswe.vercel.app/agents/deff72b5-ac91-9dff-439e-934b65a8e99f)

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-24 16:43:21 -04:00
Mason Daugherty 785c8d0061 fix(code): quiet uv installer output and require Xcode CLT on macOS (#4180)
The install script now hides the uv installer's output by default and
exits early on macOS when Xcode Command Line Tools are not installed.

---

The uv installer's verbose download/PATH output cluttered a fresh
`dcode` install; it's now hidden by default and shown only with
`DEEPAGENTS_CODE_VERBOSE=1` or on failure. On macOS, the installer now
fails fast with an actionable message when the Xcode Command Line Tools
are missing, instead of letting a downstream tool (uv interpreter
discovery / git) trigger the blocking macOS "install developer tools"
GUI popup mid-install.

## Test Plan
- [ ] On a fresh macOS without CLT, run the installer and confirm it
exits with the `xcode-select --install` message instead of showing the
GUI popup.
- [ ] On a machine without uv, run the installer and confirm the uv
installer's output is hidden unless `DEEPAGENTS_CODE_VERBOSE=1`.

Made by [Open
SWE](https://openswe.vercel.app/agents/c4c6c178-52dd-957a-68b7-dcba7f3223e0)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-23 21:17:50 -04:00
Mason Daugherty 156e118524 fix(code): report same-version dependency updates (#4146)
The installer now distinguishes a true no-op reinstall from a
same-version reinstall that still moves transitive packages. When `uv`
reports dependency changes, the final status and footer say dependencies
were updated, and the raw install output is saved so users can inspect
the full details after abbreviated terminal output.
2026-06-22 17:34:04 -04:00
Mason Daugherty e5094037f9 docs(repo): add architecture and development onboarding guides (#3983)
New-contributor onboarding currently requires stitching knowledge
together from `AGENTS.md`, per-package `Makefile`s, and external docs,
and neither the cross-layer request flow nor the `libs/code` process
model is mapped anywhere in the repo.

This adds:
- Root `ARCHITECTURE.md` (the three-layer `deepagents` → `create_agent`
→ LangGraph stack and request flow) and `DEVELOPMENT.md` (a single
bootstrap + command reference), cross-linked from `README.md` and
`libs/README.md`.
- `libs/code/ARCHITECTURE.md` (process model, interactive/headless
request lifecycles, module map, and a "where do I change X" cheat
sheet), linked from `DEV.md` and `AGENTS.md`.

Made by [Open SWE](https://openswe.vercel.app)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-20 03:04:04 -04:00
Mason Daugherty de2c9fd8c7 feat(code): quiet install script's full dependency list (#4058)
The `dcode` install script printed every transitive dependency (~130
lines) on a fresh install, which is noise for the common `curl | bash`
path. The full list only ever appeared on fresh installs, since uv's
diff reports every package as newly added.

Now a fresh install suppresses the per-package list entirely — the user
still sees the `Installing…` line and the final `✔ deepagents-code
<version> installed.` / `Setup complete.` confirmation, just not the
wall of transitive dependencies. The full list stays available behind
the existing `DEEPAGENTS_CODE_VERBOSE=1` env var.

Upgrades are unaffected: they still show the compact changed-package
diff under `Updated packages:` (`→` for version bumps, `(new)` for added
packages, `(removed)` for dropped ones), which is small and genuinely
useful.

The two cases are told apart by whether uv's diff contains any removals:
a fresh install — or a pure-addition re-run, e.g. adding an extra to an
existing env — has only added rows and is suppressed, while an upgrade
mixes removed/added rows and is printed.

Made by [Open
SWE](https://openswe.vercel.app/agents/b1a5995a-8994-73b4-9bc3-05755afa197e)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-17 15:32:54 -04:00
Mason Daugherty 619207c8d4 feat(code): prompt to install provider when selecting an uninstalled model (#3981)
The `/model` picker now lists recommended models from uninstalled
providers and offers to install the provider automatically when you
select one.

---

The `/model` selector only listed models from already-installed
providers, so common OSS providers (e.g. `baseten`) were invisible until
the user manually ran `/install`. This surfaces recommended models from
not-yet-installed providers in the selector and, when one is picked,
opens a confirmation modal that installs the provider's extra (reusing
the existing `/install` flow + restart offer) before switching to the
model. Installed models always rank above install-required ones in
search so the common case is never displaced.

Made by [Open SWE](https://openswe.vercel.app)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-16 23:52:08 -04:00
Mason Daugherty a5ec6dd0fe feat(code): prompt before updating an out-of-date dcode install (#3995)
The `deepagents-code` install script now prompts before updating an
out-of-date install in interactive shells (and skips work when already
up to date); set `DEEPAGENTS_CODE_YES=1` to auto-accept.

---

The `deepagents-code` installer (`scripts/install.sh`) ran `uv tool
install -U` unconditionally, silently pulling the latest version on
every run. It now probes PyPI for the latest release so it can be
deliberate about updates: it exits early when already up to date, and
for an existing default-path install it prompts before upgrading in an
interactive shell. Piped/non-interactive runs with no usable TTY still
upgrade automatically (preserving prior behavior), and
`DEEPAGENTS_CODE_YES=1` accepts the update without prompting. Pinned
versions (`DEEPAGENTS_CODE_VERSION`) and pre-release strategies
(`DEEPAGENTS_CODE_PRERELEASE`) express explicit intent and install
directly without a prompt.

A `can_prompt` helper verifies `/dev/tty` is actually openable, since
`IS_INTERACTIVE` only access-checks it and can be wrong under
cron/systemd/CI.

Made by [Open SWE](https://openswe.vercel.app)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 19:10:43 -04:00
Mason Daugherty c99cc47f79 docs(code): list valid DEEPAGENTS_CODE_EXTRAS options in install.sh (#3975)
The install script documented the `DEEPAGENTS_CODE_EXTRAS` environment
variable but only pointed readers to `pyproject.toml` for valid values.
This enumerates the available extras directly in the header comment —
grouped into model providers, sandbox providers, and standalone
integrations — so users can discover supported options without leaving
the script. The authoritative list still lives in `pyproject.toml`,
which the comment notes.

Made by [Open SWE](https://openswe.vercel.app)

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-15 10:26:03 -04:00
Mason Daugherty d1c6946218 chore(code): support pinned and pre-release installs (#3945)
The `dcode` install script can now install exact `deepagents-code`
versions or ask `uv` to resolve pre-release versions using an explicit
strategy. Invalid version pins, invalid pre-release strategies, and
conflicting version/pre-release settings fail before invoking `uv` so
the installer does not forward ambiguous or unsafe arguments.
2026-06-13 02:58:27 -04:00
Mason Daugherty 727d022cd1 fix(code): allow recovery commands when startup fails (#3706)
When a configured model's provider package can't be imported, the model
is built *before* the LangGraph server starts, so construction fails and
the app holds a `_server_startup_error` state that parks queued slash
commands until a successful start drains them. `/install` — the command
that installs the missing package — is `QUEUED`, so it was trapped
behind the very failure it repairs: typing `/install <pkg>` appeared to
hang because it never ran. `/model` and `/auth` already escaped this
wedge via `IMMEDIATE_UI`, but the package-recovery path did not.
2026-06-02 14:16:46 -04:00
Mason Daugherty 61f7753b8d style(code): quiet install script output by default (#3605)
Trim the `curl … | bash` install output to the lines users actually
need. The full `dcode -v` dump, the "Checking optional tools" header,
and the multi-line docs footer were noisy on every run — particularly on
the common already-up-to-date path. Verbose behavior is preserved behind
`DEEPAGENTS_CODE_VERBOSE=1`.
2026-05-26 16:43:31 -04:00
Mason Daugherty af9d04a9a8 chore(code): namespace install-script env vars under DEEPAGENTS_CODE_* (#3604)
The install-script env vars (`DEEPAGENTS_EXTRAS`, `DEEPAGENTS_PYTHON`,
`DEEPAGENTS_SKIP_OPTIONAL`) didn't signal that they only configured the
`deepagents-code` installer, conflicting with the per-package prefix the
rest of the ecosystem already uses (`DEEPAGENTS_CLI_*`, etc.). Renames
them under a `DEEPAGENTS_CODE_*` namespace and adds one new flag for
debugging the install output.
2026-05-26 16:13:32 -04:00
Mason Daugherty 475f470c01 style(code): tighten install-script output (#3603)
The `curl … | bash` install flow currently dumps every line uv prints —
timing, prep counts, an unformatted `- pkg==X` / `+ pkg==Y` diff — and
always logs `deepagents-code installed.` even when nothing changed.
Re-frames the output so the user immediately sees what changed and what
actually happened.
2026-05-26 16:03:39 -04:00
Mason Daugherty 5e4306feed feat(code): clarify install-script messaging for editable installs (#3600)
The `curl … | bash` install for `deepagents-code` now tells users
plainly when it's about to replace an existing editable install or
rebuild the venv, instead of leaking uv's internal warning verbatim.
Motivated by a confusing run where a previous `uv tool install -e …`
install made uv print `Ignoring existing environment for
`deepagents-code`: the requested Python interpreter does not match the
environment interpreter` — accurate, but unhelpful to non-technical
readers.
2026-05-26 15:04:06 -04:00
Mason Daugherty f8977a6376 fix(code): install script binary checks reference dcode (#3546)
The bundled install script probed for a `deepagents` binary that the
`deepagents-code` package never installs (its entry points are `dcode`
and `deepagents-code`), so the pre-install upgrade detection and
post-install verification were effectively no-ops on a real install.
2026-05-22 15:45:12 +00:00
Mason Daugherty a19579ae94 chore(code): command registry file (#3519) 2026-05-20 17:13:43 -05:00
Mason Daugherty b0e8d83f97 fix(code): correct LangSmith sandbox working directory (#3415)
The `deepagents-code` package still referenced `deepagents/cli` in its
documentation URLs and had an incorrect working directory for the
`langsmith` sandbox backend. Both issues are corrected across the
package and its tests.
2026-05-15 17:06:15 -07:00
Mason Daugherty 2ac7d41533 feat(code): port from libs/cli (#3388)
Release-As: 0.1.0
2026-05-12 20:45:09 +00:00