@monster-orange/den 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -0
- package/README.md +88 -158
- package/docs/distributions.md +151 -28
- package/docs/reference.md +177 -0
- package/package.json +1 -1
- package/src/shared/agents/den-claude.ts +57 -12
- package/src/shared/agents/generic.ts +1 -0
- package/src/shared/agents/runtime-search.ts +416 -0
- package/src/shared/agents/runtime.ts +26 -1
- package/src/shared/agents/runtimes.ts +30 -3
- package/src/shared/den-config.ts +88 -3
- package/src/shared/edge/render.ts +31 -0
- package/src/shared/sessions/client.ts +7 -0
- package/src/shared/sessions/den-pty.ts +10 -2
- package/src/shared/sessions/host.ts +33 -8
- package/src/shared/sessions/types.ts +8 -0
- package/src/ui/cli/agent-ops.ts +109 -21
- package/src/ui/cli/agents-command.ts +100 -4
- package/src/ui/cli/index.ts +33 -1
- package/src/ui/cli/sessions-command.ts +26 -9
- package/src/ui/cli/web-command.ts +60 -9
- package/src/ui/server/agent-routes.ts +11 -3
- package/src/ui/server/edge-exposure.ts +143 -0
- package/src/ui/server/index.ts +105 -23
- package/src/ui/server/public/agent-start.js +202 -0
- package/src/ui/server/public/agents-page.js +90 -18
- package/src/ui/server/public/agents-section.js +6 -18
- package/src/ui/server/public/agents.html +70 -0
- package/src/ui/server/public/i18n/es.js +11 -0
- package/src/ui/server/public/i18n/ja.js +11 -0
- package/src/ui/server/public/local-dev.html +43 -14
- package/src/ui/server/public/settings-registry.js +1 -1
- package/src/ui/server/public/shell.js +40 -0
- package/src/ui/server/public/topbar.js +127 -28
- package/src/ui/server/request-guard.ts +122 -7
- package/src/ui/server/ui-assets.ts +2 -0
package/docs/distributions.md
CHANGED
|
@@ -383,6 +383,14 @@ works; only the saved password needs `refs.sudo`.
|
|
|
383
383
|
`configured`, `backendAvailable`, `reason` and `hasStored` (`opAvailable`
|
|
384
384
|
is a deprecated alias of `backendAvailable`, kept for one minor).
|
|
385
385
|
|
|
386
|
+
The lock (🔒 in the top bar) is asked for only when an action needs it,
|
|
387
|
+
on any page: a page that thinks it's locked asks the server again first
|
|
388
|
+
(another tab or device may have unlocked it), then, when a password is
|
|
389
|
+
stored, loads it from the vault (once a page; a page view alone never
|
|
390
|
+
wakes the vault), and only then shows the password field. Its popover is
|
|
391
|
+
always on top, even over a dialog, and a dialog that needs the lock (Start
|
|
392
|
+
agent) asks for the password itself.
|
|
393
|
+
|
|
386
394
|
A `1password` distribution with no refs gets one warning, in the web
|
|
387
395
|
server's startup log and in `<cli> validate`, with the YAML to add.
|
|
388
396
|
|
|
@@ -1183,9 +1191,11 @@ are the same directory.
|
|
|
1183
1191
|
|
|
1184
1192
|
On localhost nothing asks you to sign in: every page, the agents and the
|
|
1185
1193
|
terminal work as they are, and the lock (R8) is the only prompt (for
|
|
1186
|
-
privileged actions).
|
|
1187
|
-
this machine
|
|
1188
|
-
|
|
1194
|
+
privileged actions). The edge counts as local too (0.4.3): opening
|
|
1195
|
+
`https://acme.acme.localhost` from this machine asks for nothing.
|
|
1196
|
+
Signing in is for when the server is reached from another machine: the
|
|
1197
|
+
edge for a client the edge says is elsewhere, a LAN address, or a bind
|
|
1198
|
+
that isn't loopback (`DEN_BIND_HOST`).
|
|
1189
1199
|
|
|
1190
1200
|
There is no password. Reading `web.secret` (32 random bytes, mode 0600 in
|
|
1191
1201
|
the state dir, made at the first `web up`) proves you are this computer's
|
|
@@ -1205,7 +1215,11 @@ user, and the CLI does that for you:
|
|
|
1205
1215
|
within 60 seconds, and only this server's secret makes one. It is printed
|
|
1206
1216
|
only on the one line you asked for (`web login`, or `web up` when it
|
|
1207
1217
|
couldn't open the browser), never logged. Through the edge or a LAN name,
|
|
1208
|
-
the cookie belongs to that name: `web login --
|
|
1218
|
+
the cookie belongs to that name: `web login --host acme.acme.localhost`
|
|
1219
|
+
mints the link for an edge hostname (any edge domain or alias, a VPN name
|
|
1220
|
+
among them; its URL comes from the edge config, and a name the edge
|
|
1221
|
+
doesn't route here is refused, saying what to add), and
|
|
1222
|
+
`web login --origin http://<lan name>:<port>` for any other allowed name
|
|
1209
1223
|
(the signed-out page there shows the exact command).
|
|
1210
1224
|
- **The cookie** (`den_session_<port>`) is HttpOnly, SameSite=Strict and
|
|
1211
1225
|
Path=/, and Secure when the request came over HTTPS through the edge. The
|
|
@@ -1222,10 +1236,38 @@ user, and the CLI does that for you:
|
|
|
1222
1236
|
open, and changes (an agent's start or resume and the terminal included)
|
|
1223
1237
|
need only the page's own Origin and CSRF token, which the shell sends.
|
|
1224
1238
|
The Host allowlist, the Origin check and the CSRF token are what stop
|
|
1225
|
-
another site or a rebinding DNS name.
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1239
|
+
another site or a rebinding DNS name. From another machine, every page
|
|
1240
|
+
and call needs a signed-in browser (or a bearer). A page there shows
|
|
1241
|
+
"Sign in to use <name>" with the command to run; it never asks for a
|
|
1242
|
+
secret. `/api` answers 401 `signin_required`.
|
|
1243
|
+
- **The edge counts as local.** A request for the edge hostname from the
|
|
1244
|
+
edge (an edge peer, below) is treated as loopback: no sign-in on any
|
|
1245
|
+
route, the agents and the terminal included. Host, Origin, CSRF and the
|
|
1246
|
+
agent-token rules still apply. Who the client is comes from the left-most
|
|
1247
|
+
`X-Forwarded-For` entry the edge sends:
|
|
1248
|
+
- loopback, or one of this machine's own interface addresses (a LAN
|
|
1249
|
+
address, or a VPN's such as a Tailscale `100.x`): local;
|
|
1250
|
+
- missing, or a container/VM network's address (the Docker ranges, or a
|
|
1251
|
+
subnet of one of this machine's Docker/OrbStack/VM bridges): Den can't
|
|
1252
|
+
tell (Docker Desktop and OrbStack on macOS hand the edge their
|
|
1253
|
+
gateway's address, and a TCP passthrough in front of the edge hides the
|
|
1254
|
+
client), so local;
|
|
1255
|
+
- any other address: another machine, which needs a sign-in.
|
|
1256
|
+
|
|
1257
|
+
With `DEN_BIND_HOST` beyond loopback (a deliberate exposure, other than
|
|
1258
|
+
the Linux edge's Docker bridge address) the edge needs a sign-in as
|
|
1259
|
+
before. With `forwardedHeaders.insecure` (the edge's default) Traefik
|
|
1260
|
+
keeps an `X-Forwarded-For` the client sent, so this is a convenience,
|
|
1261
|
+
not a boundary: whoever can reach the edge can use the terminal.
|
|
1262
|
+
|
|
1263
|
+
- **"Your edge is reachable from other machines".** When Den can tell, at
|
|
1264
|
+
start, that the edge is reachable beyond this machine (an edge hostname
|
|
1265
|
+
that resolves to an address that isn't loopback, such as a LAN or
|
|
1266
|
+
Tailscale address, or the edge container publishing a port on every
|
|
1267
|
+
interface, from `docker inspect`), it writes "Your edge is reachable from
|
|
1268
|
+
other machines; anyone who can reach it can use terminals. Auth is
|
|
1269
|
+
coming." once to `web.log`, and every page shows it as a banner (dismissed
|
|
1270
|
+
per browser).
|
|
1229
1271
|
- **localhost is the canonical name.** A cookie set on `localhost` isn't
|
|
1230
1272
|
sent to `127.0.0.1`, so the links Den prints use `localhost`.
|
|
1231
1273
|
- **Browsers.** Chromium, Firefox and Safari 16.4 or later: a sign-in link
|
|
@@ -1581,9 +1623,16 @@ the profile's `state_dir` (`~/.<name>`), or `<PREFIX>_HOME`.
|
|
|
1581
1623
|
- Stopping the host (`sessions host --stop`, or a reboot) ends its
|
|
1582
1624
|
sessions. They stay listed as exited, with their scrollback, until
|
|
1583
1625
|
`sessions kill` removes them.
|
|
1584
|
-
- `
|
|
1626
|
+
- `sessions.max_concurrent` caps the plain sessions (`sessions start`)
|
|
1627
|
+
running at once (default 8), and `agents.max_concurrent` the agents
|
|
1628
|
+
(default 4). Each counts only its own, so a few plain shells never stop
|
|
1629
|
+
an agent from starting, nor agents a shell. Past either, the start is
|
|
1630
|
+
refused, naming the key ("2 sessions are running and
|
|
1631
|
+
sessions.max_concurrent is 2"); `<cli> validate` names a wrong value.
|
|
1585
1632
|
|
|
1586
1633
|
```yaml
|
|
1634
|
+
sessions:
|
|
1635
|
+
max_concurrent: 8
|
|
1587
1636
|
agents:
|
|
1588
1637
|
max_concurrent: 4
|
|
1589
1638
|
```
|
|
@@ -1609,12 +1658,13 @@ server scoped to its workspace.
|
|
|
1609
1658
|
<cli> agents stop <id> [-w <ws>]
|
|
1610
1659
|
<cli> agents resume <id> [-w <ws>]
|
|
1611
1660
|
<cli> agents personas [--json]
|
|
1661
|
+
<cli> agents doctor [--json] # can each runtime run here, and why not
|
|
1612
1662
|
<cli> sessions attach <ws>:<id> # its terminal; ctrl-] detaches
|
|
1613
1663
|
```
|
|
1614
1664
|
|
|
1615
1665
|
- `start` runs the agent as session `<dist>-<ws>:<id>` in the workspace's
|
|
1616
1666
|
checkout dir (its `directory`, else `den-workspaces/<ws>`), capped by
|
|
1617
|
-
`agents.max_concurrent
|
|
1667
|
+
`agents.max_concurrent` (agents only: plain sessions never count). The id defaults to the persona's id; an id is
|
|
1618
1668
|
unique in the distribution, so a second workspace's planner gets
|
|
1619
1669
|
`planner-<ws>` unless you give `--id`.
|
|
1620
1670
|
- The **agent record**, `<stateDir>/agents/<id>.json` (0600), holds its id,
|
|
@@ -1647,7 +1697,14 @@ started. The workspace header counts them ("2 agents").
|
|
|
1647
1697
|
not running.
|
|
1648
1698
|
- **+ Start agent** picks a persona (Den's pack, then
|
|
1649
1699
|
`agents.persona_packs`) and a runtime (the persona's own unless you pick
|
|
1650
|
-
one; one that can't run here is listed, disabled), then Start.
|
|
1700
|
+
one; one that can't run here is listed, disabled), then Start. Under the
|
|
1701
|
+
runtime a line says what Start will run, with its version and path
|
|
1702
|
+
("Starts Claude Code 2.1.0 from ~/.local/bin/claude"). When that runtime
|
|
1703
|
+
can't run here, Start is disabled and the line says why and how to fix
|
|
1704
|
+
it: where it looked, then "Install it, or set
|
|
1705
|
+
`agents.runtimes.<id>.bin` to its path in <your user.yml>, or run
|
|
1706
|
+
`<cli> agents doctor`" (see [Runtimes](#runtimes)). Nothing fails only
|
|
1707
|
+
after Start.
|
|
1651
1708
|
- The page and the CLI call the same operations and read the same agent
|
|
1652
1709
|
records, so an agent started in one is listed in the other. A workspace
|
|
1653
1710
|
lists its own agents only.
|
|
@@ -1688,7 +1745,10 @@ agent of the workspaces in view, and their terminals in the browser.
|
|
|
1688
1745
|
running, and opening it again replays what you missed. **Stop** (an
|
|
1689
1746
|
agent only) is `agents stop`, behind the lock. **+ Start agent** (or
|
|
1690
1747
|
`/agents?start=1`) starts one and opens its pane; under All it asks for
|
|
1691
|
-
the workspace.
|
|
1748
|
+
the workspace. It says before Start whether the runtime can run, as the
|
|
1749
|
+
Workspaces page's form does. While the lock is closed, the dialog asks
|
|
1750
|
+
for the admin password at its top, and Start unlocks, then starts, without
|
|
1751
|
+
closing anything.
|
|
1692
1752
|
- ⌘K lists each as "Agents › <ws> / <name>", and "Start agent…".
|
|
1693
1753
|
- Several browsers and `<cli> sessions attach` can attach to one session
|
|
1694
1754
|
at once and each sees every byte; the terminal's size is the last
|
|
@@ -1714,7 +1774,8 @@ An agent's environment starts empty and gets, in order:
|
|
|
1714
1774
|
|
|
1715
1775
|
1. `PATH`, `HOME`, `USER`, `LOGNAME`, `SHELL`, `TERM` (default
|
|
1716
1776
|
`xterm-256color`), `COLORTERM`, `LANG`, `LC_*`, `TZ` and `TMPDIR` from
|
|
1717
|
-
yours
|
|
1777
|
+
yours (`PATH` with the login shell's and the runtimes' install
|
|
1778
|
+
directories added: [Runtimes](#runtimes));
|
|
1718
1779
|
2. the variables its runtime names (`envPassthrough`; den-claude:
|
|
1719
1780
|
`CLAUDE_CONFIG_DIR`);
|
|
1720
1781
|
3. the runtime's launch env;
|
|
@@ -1761,19 +1822,22 @@ An agent runs as your OS user, so it could still read `web.secret` itself;
|
|
|
1761
1822
|
the token keeps what Den does on its behalf attributable and bounded.
|
|
1762
1823
|
**The boundary is the server's token rules** (above), not a deny list.
|
|
1763
1824
|
|
|
1764
|
-
|
|
1765
|
-
(`--settings`) whose `permissions.deny` lists
|
|
1766
|
-
dir>/web.secret` and `<state dir>/sessions/**`, and
|
|
1767
|
-
Code's wildcard form for the path
|
|
1768
|
-
each of `cat`, `head`, `tail`,
|
|
1769
|
-
`base64` and `cp` followed by it),
|
|
1770
|
-
and under `$HOME`, with a path that
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1825
|
+
**The deny list is a convenience, not the boundary.** den-claude passes
|
|
1826
|
+
Claude Code a settings file (`--settings`) whose `permissions.deny` lists
|
|
1827
|
+
`Read` of `<state dir>/web.secret` and `<state dir>/sessions/**`, and
|
|
1828
|
+
`Bash` rules in Claude Code's wildcard form for the path
|
|
1829
|
+
(`Bash(*<state dir>/web.secret*)`, and each of `cat`, `head`, `tail`,
|
|
1830
|
+
`less`, `more`, `xxd`, `od`, `strings`, `base64` and `cp` followed by it),
|
|
1831
|
+
spelled as an absolute path, under `~` and under `$HOME`, with a path that
|
|
1832
|
+
has spaces also backslash-escaped. It binds cooperating agents only: an
|
|
1833
|
+
agent that ignores its permission config can read the files anyway. And
|
|
1834
|
+
it does not catch every spelling: a path relative to the agent's cwd
|
|
1835
|
+
(`cat ../.den/web.secret`, say), another program, a script or a copy under
|
|
1836
|
+
another name all get past it. The server's token rules (above)
|
|
1837
|
+
are the boundary: whatever an agent reads, its token reaches only its own
|
|
1838
|
+
session's routes, never a shell-grade route or the lock. OS-user isolation
|
|
1839
|
+
(a separate user, a container, a sandbox) is not in 0.4.x. `web logout
|
|
1840
|
+
--all` revokes every agent's token (above, under Signing in).
|
|
1777
1841
|
|
|
1778
1842
|
**What the scope does not do.** An agent's MCP scope binds cooperating
|
|
1779
1843
|
agents only. A runtime with a shell can still run `<cli>` itself, outside
|
|
@@ -1822,8 +1886,9 @@ are errors naming the file or the key.
|
|
|
1822
1886
|
|
|
1823
1887
|
`<cli> doctor` lists the runtime kind. Two are built in:
|
|
1824
1888
|
|
|
1825
|
-
- **`den-claude`** (the default)
|
|
1826
|
-
version from the install, without running
|
|
1889
|
+
- **`den-claude`** (the default) runs Claude Code's `claude`, found as
|
|
1890
|
+
below; `<cli> doctor` reads its version from the install, without running
|
|
1891
|
+
it. It runs `claude --session-id
|
|
1827
1892
|
<id> --mcp-config <file> --strict-mcp-config --settings <file> [--model <m>]
|
|
1828
1893
|
--append-system-prompt <role prompt>`, so Den's MCP server is the agent's
|
|
1829
1894
|
only one, and resumes with `claude --resume <id>`. Permission prompts stay
|
|
@@ -1836,6 +1901,64 @@ are errors naming the file or the key.
|
|
|
1836
1901
|
`{mcp_config}` is the path of the agent's MCP config, written only when the
|
|
1837
1902
|
template references it. It declares no resume.
|
|
1838
1903
|
|
|
1904
|
+
**Where a runtime's CLI is found.** As your terminal would find it, not
|
|
1905
|
+
only on the web server's own PATH (a launchd service's PATH leaves out
|
|
1906
|
+
`~/.local/bin`, where Claude Code's native installer puts `claude`). The
|
|
1907
|
+
first match wins:
|
|
1908
|
+
|
|
1909
|
+
1. **`agents.runtimes.<id>.bin`** in `config/local/user.yml`: the absolute
|
|
1910
|
+
path of the runtime's CLI. Paths are per machine, so it is read from
|
|
1911
|
+
user.yml only, never from the distribution's config. A bad value (not
|
|
1912
|
+
absolute, missing, not an executable file) makes the runtime
|
|
1913
|
+
unavailable with that reason; Den doesn't fall back to a search.
|
|
1914
|
+
|
|
1915
|
+
```yaml
|
|
1916
|
+
# config/local/user.yml (gitignored)
|
|
1917
|
+
agents:
|
|
1918
|
+
runtimes:
|
|
1919
|
+
den-claude:
|
|
1920
|
+
bin: /Users/me/.local/bin/claude
|
|
1921
|
+
```
|
|
1922
|
+
|
|
1923
|
+
2. **Your login shell's PATH**: the web server asks `$SHELL -lc` once at
|
|
1924
|
+
start, in the background, under a 3-second timeout. On a timeout or
|
|
1925
|
+
any failure it is left out.
|
|
1926
|
+
3. **The server's own PATH.**
|
|
1927
|
+
4. **The runtime's install directories.** For den-claude: `~/.local/bin`,
|
|
1928
|
+
`~/.claude/local`, `/opt/homebrew/bin`, `/usr/local/bin` and npm's
|
|
1929
|
+
global bin (`$npm_config_prefix/bin`, and node's own directory). Den
|
|
1930
|
+
never runs npm to find it.
|
|
1931
|
+
|
|
1932
|
+
Sessions and agents Den starts get the same directories on their PATH
|
|
1933
|
+
(and a configured CLI's own directory), so the runtime's own tools are
|
|
1934
|
+
found too. Two engine variables override the search for tests and
|
|
1935
|
+
diagnosis: `DEN_LOGIN_SHELL` (the shell to ask instead of `$SHELL`; `off`
|
|
1936
|
+
for none) and `DEN_RUNTIME_DIRS` (a PATH-style list that replaces every
|
|
1937
|
+
runtime's install directories; empty for none).
|
|
1938
|
+
|
|
1939
|
+
**`<cli> agents doctor`** prints, for each runtime, its status, the CLI it
|
|
1940
|
+
runs and where that came from (the `agents.runtimes` setting, the login
|
|
1941
|
+
shell's PATH, PATH, or an install directory), its version (`claude
|
|
1942
|
+
--version`, run once under a timeout), the directories it searched (names
|
|
1943
|
+
only, never an environment value), and the fix when it's missing. It
|
|
1944
|
+
resolves the login shell's PATH first, as the web server does. It exits 1
|
|
1945
|
+
when the default runtime is unavailable; `--json` prints what the Start
|
|
1946
|
+
dialog gets.
|
|
1947
|
+
|
|
1948
|
+
```
|
|
1949
|
+
den-claude (Claude Code) [default]
|
|
1950
|
+
status ✗ unavailable: claude not found (searched 5 directories)
|
|
1951
|
+
searched /usr/bin
|
|
1952
|
+
/bin
|
|
1953
|
+
…
|
|
1954
|
+
fix install Claude Code, or set agents.runtimes.den-claude.bin to its absolute path in /…/config/local/user.yml
|
|
1955
|
+
then run `<cli> agents doctor` again
|
|
1956
|
+
```
|
|
1957
|
+
|
|
1958
|
+
`GET /api/personas` gives each runtime its `version`, `path`, `source`
|
|
1959
|
+
(`setting`, `login-shell`, `well-known` or `PATH`), `searched` and
|
|
1960
|
+
`setting` (the user.yml key), and `userConfig`, the user.yml to edit.
|
|
1961
|
+
|
|
1839
1962
|
### Workspace scope and grants
|
|
1840
1963
|
|
|
1841
1964
|
An agent's MCP config, `<stateDir>/agent-files/<id>/mcp.json` (0600), runs
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Den reference
|
|
2
|
+
|
|
3
|
+
The technical reference that used to be the README. It will be split into separate pages (configuration, CLI, manifests, edge, secrets, tests, MCP) as part of DOC-2.
|
|
4
|
+
|
|
5
|
+
## Make it your own
|
|
6
|
+
|
|
7
|
+
1. **Create** a distribution named after your product:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npx @monster-orange/den create acme # → ./acme
|
|
11
|
+
cd acme && npm install
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
2. **Name** it: `den.yml` holds `product: { name: acme }`, so the CLI is
|
|
15
|
+
`./bin/acme`, env vars are `ACME_*`, state lives in `~/.acme`. Pass
|
|
16
|
+
`--cli`, `--env-prefix` or `--title` to `create`, or edit `den.yml`.
|
|
17
|
+
3. **Brand** it: `brand:` in `config/acme.yml` sets the name, tagline and
|
|
18
|
+
banner people see in the CLI and web UI.
|
|
19
|
+
4. **Point it at your repos**: set `github_org` in `config/acme.yml`, then
|
|
20
|
+
`./bin/acme register ../shop-api` (or add a manifest per repo under
|
|
21
|
+
`manifests/`).
|
|
22
|
+
5. **Run** it: `./bin/acme up`, `./bin/acme web`, `make test`.
|
|
23
|
+
|
|
24
|
+
Every field, the bin shim, the make fragment and upgrading are in
|
|
25
|
+
[docs/distributions.md](distributions.md);
|
|
26
|
+
[examples/distribution/](../examples/distribution) is what `create acme` writes.
|
|
27
|
+
|
|
28
|
+
## Distributions
|
|
29
|
+
|
|
30
|
+
You run Den through a small repo of your own (a _distribution_):
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
my-dev/
|
|
34
|
+
├── den.yml # who this distribution is (below)
|
|
35
|
+
├── package.json # pins an exact @monster-orange/den version
|
|
36
|
+
├── config/<name>.yml # your org config
|
|
37
|
+
├── config/environments/ # one <env>.json per environment
|
|
38
|
+
├── manifests/ # manifests for repos that don't carry their own
|
|
39
|
+
├── tests/pillars/<pillar>/ # your own suites (e.g. tests/pillars/perf/<module>/test.js)
|
|
40
|
+
├── bin/<cli> # shim: DEN_ROOT=<this dir> exec node_modules/@monster-orange/den/bin/den
|
|
41
|
+
└── Makefile # include node_modules/@monster-orange/den/make/den.mk
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`den.yml` names the product. Every runtime name derives from it:
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
product:
|
|
48
|
+
name:
|
|
49
|
+
acme # CLI "acme", env vars ACME_*, config/acme.yml,
|
|
50
|
+
# manifests .acme.yml, state ~/.acme,
|
|
51
|
+
# managed clones .acme-services/, workspaces acme-workspaces/
|
|
52
|
+
# Override any of them: cli, env_prefix, config, manifest, state_dir,
|
|
53
|
+
# services_dir, workspaces_dir
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Den's own checkout is a distribution named `den`, so in this repo the CLI is
|
|
57
|
+
`./bin/den`, the config is `config/den.yml`, and env vars are `DEN_*`.
|
|
58
|
+
|
|
59
|
+
## Repos opt in with a manifest
|
|
60
|
+
|
|
61
|
+
A repo joins by carrying a manifest at its root (`.<name>.yml`, e.g.
|
|
62
|
+
`.den.yml`) whose commands delegate to the repo's own targets. Den never
|
|
63
|
+
reaches inside a repo beyond that. When a repo has no manifest, the
|
|
64
|
+
distribution can hold one for it in `manifests/<repo>.<name>.yml`; the repo's
|
|
65
|
+
own manifest always wins.
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
# demo-api: a self-contained service. Nothing it calls, so it starts alone.
|
|
69
|
+
version: 1
|
|
70
|
+
name: demo-api
|
|
71
|
+
application: shop # the application this repo belongs to (den apps)
|
|
72
|
+
compose:
|
|
73
|
+
files: [docker-compose.yml]
|
|
74
|
+
routes:
|
|
75
|
+
api: api:8080 # https://api.demo-api.<edge domain>
|
|
76
|
+
services:
|
|
77
|
+
demo-api:
|
|
78
|
+
commands:
|
|
79
|
+
local:
|
|
80
|
+
setup: make setup
|
|
81
|
+
preflight: make test
|
|
82
|
+
# OWASP ZAP target for the security pillar (`make test-sec-api env=local`).
|
|
83
|
+
security:
|
|
84
|
+
api:
|
|
85
|
+
hosts:
|
|
86
|
+
local: ["https://api.demo-api.globex.localhost"]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
That is `examples/demo/demo-api/.den.yml`; the other demo repos are in
|
|
90
|
+
[`examples/demo/`](../examples/demo/).
|
|
91
|
+
|
|
92
|
+
## What it does
|
|
93
|
+
|
|
94
|
+
- **Workspaces.** A workspace is a set of repos. Each is either
|
|
95
|
+
_dev-managed_ (your checkout, your branch) or _managed_ (cloned by Den and
|
|
96
|
+
kept on a tracking branch).
|
|
97
|
+
- **Derived dependency graph.** Start and stop order comes from real signals in
|
|
98
|
+
each repo — SDK imports, `*_URL` env vars, IaC references, event rules,
|
|
99
|
+
readiness probes — tuned in config with `graph.aliases`, `graph.edges` and
|
|
100
|
+
an optional shared local-infra repo (`graph.infra`).
|
|
101
|
+
- **Local HTTPS edge.** One local proxy routes `https://<route>.<domain>` to
|
|
102
|
+
each service, with an optional project CA.
|
|
103
|
+
- **Test pillars.** `smoke`, `browser` (synthetic Playwright), `api`,
|
|
104
|
+
`journeys` (agent-driven), `perf` (k6), `security` (OWASP ZAP against hosts
|
|
105
|
+
you own) and `suites` (a repo's own commands).
|
|
106
|
+
- **Releases.** A release view over the repos and deploy workflows your config
|
|
107
|
+
lists.
|
|
108
|
+
- **Agents and sessions.** `sessions start` runs a terminal that outlives the
|
|
109
|
+
web server; `agents start` runs a persona (Den ships a default pack) in a
|
|
110
|
+
workspace's checkout, with an MCP server pinned to that workspace and a
|
|
111
|
+
token of its own. The web UI's Agents tab lists them and opens their
|
|
112
|
+
terminals.
|
|
113
|
+
- **Browser sign-in beyond loopback.** On localhost no route needs a sign-in.
|
|
114
|
+
Through the edge or on a non-loopback bind, every route needs one:
|
|
115
|
+
`<cli> web login` prints a one-time link (`web open` opens it).
|
|
116
|
+
- **Web UI and CLI** over the same state.
|
|
117
|
+
|
|
118
|
+
## Strategies
|
|
119
|
+
|
|
120
|
+
Concerns that vary by org are strategies selected in config, never hard-wired:
|
|
121
|
+
|
|
122
|
+
| Concern | Built-in choices | Chosen in |
|
|
123
|
+
| ---------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
124
|
+
| Secrets backend | `none`, `env`, `doppler`, `1password` | `secrets.provider` in the org config; per engineer in `config/local/user.yml`; per shell `<PREFIX>_SECRETS_PROVIDER` |
|
|
125
|
+
| Test-user tokens | `local-jwt`, `cognito`, `auth0`, `none` | `auth.provider` in `config/environments/<env>.json` |
|
|
126
|
+
|
|
127
|
+
A new secrets backend implements `SecretsProvider`
|
|
128
|
+
(`src/shared/secrets/types.ts`) and registers as a strategy
|
|
129
|
+
(`src/shared/secrets/strategies.ts`); a distribution needs no store at all
|
|
130
|
+
(`none`, see "Secrets" in `docs/distributions.md`). A new identity provider implements a token
|
|
131
|
+
provider in `src/shared/auth/token-providers/`.
|
|
132
|
+
|
|
133
|
+
## CLI
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
den up [repo] # start the repo's derived dependency closure (-x deps only, -n dry run)
|
|
137
|
+
den down [repo] # stop it cleanly (--with-deps: and what it sits on)
|
|
138
|
+
den restart [svc...] # restart services (-A <app> for a whole application)
|
|
139
|
+
den status # service health for the active workspace
|
|
140
|
+
den apps # Org > Application > Repo > Services
|
|
141
|
+
den graph [repo] # the derived dependency graph
|
|
142
|
+
den logs [repo] # tail a repo's local logs
|
|
143
|
+
den use-local [repo] # use your checkout · use-managed [repo] to switch back
|
|
144
|
+
den workspace list # workspaces (activate · deactivate · use · create · delete)
|
|
145
|
+
den run # run test suites (interactive, or with options)
|
|
146
|
+
den web # the web UI (open · login · sessions · logout · token)
|
|
147
|
+
den sessions list # terminals that outlive the web server (start · attach · kill)
|
|
148
|
+
den agents list -w ws # a workspace's agents (start · stop · resume · personas)
|
|
149
|
+
den doctor # the strategies in use and whether each can work (--probe)
|
|
150
|
+
den --help # everything else
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`up`, `down`, `restart` and `setup` take `-w <workspace>`. Without it, a
|
|
154
|
+
service you name is found in the focused workspace first, then across the
|
|
155
|
+
other active workspaces; when it is in several, the error lists the `-w`
|
|
156
|
+
command for each.
|
|
157
|
+
|
|
158
|
+
In a distribution the command is that distribution's `cli` name.
|
|
159
|
+
|
|
160
|
+
## Developing Den
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
npm ci
|
|
164
|
+
npm run typecheck
|
|
165
|
+
npm run lint
|
|
166
|
+
npm test
|
|
167
|
+
DEN_ORG_NAMES_FILE=<patterns file> node scripts/org-name-gate.mjs
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
CI runs the same gates. The org-name gate fails the build when any file names
|
|
171
|
+
an organisation, product, host or account; its patterns are kept out of this
|
|
172
|
+
repo (a CI secret) so the list itself is never published. See
|
|
173
|
+
[CONTRIBUTING.md](../CONTRIBUTING.md).
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
[MIT](../LICENSE)
|
package/package.json
CHANGED
|
@@ -28,27 +28,50 @@
|
|
|
28
28
|
* session Den starts (an agent is not a child of the session that
|
|
29
29
|
* started it).
|
|
30
30
|
*
|
|
31
|
-
* detect runs nothing
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* detect runs nothing (AG-0.1, runtime-search.ts): agents.runtimes.den-claude.bin
|
|
32
|
+
* in user.yml when set, else `claude` on the login shell's PATH, the
|
|
33
|
+
* caller's PATH, then its install directories (~/.local/bin, the native
|
|
34
|
+
* installer's; ~/.claude/local; /opt/homebrew/bin; /usr/local/bin; npm's
|
|
35
|
+
* global bin). Its version is read from where it is installed (a native
|
|
36
|
+
* install's versions/<version>, or the npm package's package.json); the
|
|
37
|
+
* Start dialog and `agents doctor` ask `claude --version` once as well.
|
|
34
38
|
*/
|
|
35
39
|
import { randomUUID } from "crypto";
|
|
36
40
|
import * as fs from "fs";
|
|
37
41
|
import * as path from "path";
|
|
42
|
+
import { configuredRuntimeBin } from "../den-config.js";
|
|
38
43
|
import { rolePrompt } from "./personas.js";
|
|
39
44
|
import {
|
|
40
45
|
LaunchError,
|
|
41
46
|
mcpConfigJson,
|
|
42
47
|
mcpConfigPath,
|
|
43
|
-
|
|
48
|
+
type DetectOptions,
|
|
44
49
|
type LaunchInput,
|
|
45
50
|
type LaunchOutput,
|
|
46
51
|
type Runtime,
|
|
47
52
|
type RuntimeDetect,
|
|
48
53
|
} from "./runtime.js";
|
|
54
|
+
import {
|
|
55
|
+
locateRuntime,
|
|
56
|
+
systemBinDirs,
|
|
57
|
+
type RuntimeFind,
|
|
58
|
+
} from "./runtime-search.js";
|
|
49
59
|
|
|
50
60
|
export const DEN_CLAUDE_ID = "den-claude";
|
|
51
61
|
const BIN = "claude";
|
|
62
|
+
|
|
63
|
+
/** Where Claude Code is found, and how its version is asked. */
|
|
64
|
+
export const DEN_CLAUDE_FIND: RuntimeFind = {
|
|
65
|
+
bins: [BIN],
|
|
66
|
+
dirs: (home, env) => [
|
|
67
|
+
// The native installer's, then the older local install's.
|
|
68
|
+
...(home
|
|
69
|
+
? [path.join(home, ".local", "bin"), path.join(home, ".claude", "local")]
|
|
70
|
+
: []),
|
|
71
|
+
...systemBinDirs(env),
|
|
72
|
+
],
|
|
73
|
+
versionArgs: ["--version"],
|
|
74
|
+
};
|
|
52
75
|
const PACKAGE = "@anthropic-ai/claude-code";
|
|
53
76
|
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
54
77
|
|
|
@@ -97,11 +120,29 @@ export function installedVersion(bin: string): string | undefined {
|
|
|
97
120
|
return undefined;
|
|
98
121
|
}
|
|
99
122
|
|
|
100
|
-
function detect(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
123
|
+
function detect(
|
|
124
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
125
|
+
opts: DetectOptions = {},
|
|
126
|
+
): RuntimeDetect {
|
|
127
|
+
const setting =
|
|
128
|
+
opts.bin === undefined ? configuredRuntimeBin(DEN_CLAUDE_ID) : opts.bin;
|
|
129
|
+
const found = locateRuntime(DEN_CLAUDE_ID, DEN_CLAUDE_FIND, env, setting);
|
|
130
|
+
if (!found.available || !found.path)
|
|
131
|
+
return {
|
|
132
|
+
available: false,
|
|
133
|
+
why: found.why ?? `${BIN} not found`,
|
|
134
|
+
// "setting": a bad agents.runtimes.<id>.bin is the reason, not the search.
|
|
135
|
+
...(found.source ? { source: found.source } : {}),
|
|
136
|
+
searched: found.searched,
|
|
137
|
+
};
|
|
138
|
+
const version = installedVersion(found.path);
|
|
139
|
+
return {
|
|
140
|
+
available: true,
|
|
141
|
+
...(version ? { version } : {}),
|
|
142
|
+
path: found.path,
|
|
143
|
+
...(found.source ? { source: found.source } : {}),
|
|
144
|
+
searched: found.searched,
|
|
145
|
+
};
|
|
105
146
|
}
|
|
106
147
|
|
|
107
148
|
/**
|
|
@@ -231,13 +272,17 @@ function common(input: LaunchInput): {
|
|
|
231
272
|
}
|
|
232
273
|
|
|
233
274
|
function binFor(input: LaunchInput): string {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
275
|
+
if (input.bin) return input.bin;
|
|
276
|
+
const d = detect(input.env);
|
|
277
|
+
if (!d.available || !d.path)
|
|
278
|
+
throw new LaunchError(`${DEN_CLAUDE_ID}: ${d.why ?? `${BIN} not found`}`);
|
|
279
|
+
return d.path;
|
|
237
280
|
}
|
|
238
281
|
|
|
239
282
|
export const denClaude: Runtime = {
|
|
240
283
|
id: DEN_CLAUDE_ID,
|
|
284
|
+
label: "Claude Code",
|
|
285
|
+
find: DEN_CLAUDE_FIND,
|
|
241
286
|
detect,
|
|
242
287
|
launch(input): LaunchOutput {
|
|
243
288
|
const bin = binFor(input);
|