@monster-orange/den 0.4.3 → 0.4.4
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 +179 -0
- package/README.md +9 -9
- package/docs/distributions.md +215 -65
- package/package.json +1 -1
- package/src/shared/agents/catalog-runtime.ts +178 -0
- package/src/shared/agents/catalog.ts +343 -0
- package/src/shared/agents/den-claude.ts +14 -18
- package/src/shared/agents/runtime-search.ts +142 -15
- package/src/shared/agents/runtime.ts +18 -1
- package/src/shared/agents/runtimes.ts +23 -1
- package/src/shared/auth/sudo-vault.ts +16 -0
- package/src/shared/den-config.ts +57 -3
- package/src/shared/edge/status.ts +134 -0
- package/src/shared/environment-manager/health.ts +82 -8
- package/src/shared/environment-manager/workspaces.ts +54 -0
- package/src/shared/health/not-checked.ts +42 -0
- package/src/shared/health/rollup.ts +4 -0
- package/src/shared/process/spawn-streaming.ts +8 -3
- package/src/shared/sessions/client.ts +5 -1
- package/src/shared/sessions/host.ts +29 -1
- package/src/shared/sessions/protocol.ts +6 -1
- package/src/shared/sessions/types.ts +6 -0
- package/src/shared/stacks/status.ts +24 -0
- package/src/shared/strategies/registry.ts +5 -0
- package/src/shared/types/index.ts +6 -0
- package/src/ui/cli/agent-ops.ts +210 -19
- package/src/ui/cli/agents-command.ts +50 -21
- package/src/ui/cli/edge-command.ts +16 -3
- package/src/ui/cli/health-display.ts +4 -1
- package/src/ui/mcp/scope.ts +23 -3
- package/src/ui/mcp/server.ts +31 -7
- package/src/ui/mcp/tools.ts +327 -20
- package/src/ui/mcp/views.ts +450 -0
- package/src/ui/server/agent-routes.ts +118 -28
- package/src/ui/server/index.ts +77 -6
- package/src/ui/server/public/agent-start.js +46 -7
- package/src/ui/server/public/agents-page.js +232 -97
- package/src/ui/server/public/agents-section.js +3 -5
- package/src/ui/server/public/agents.html +12 -28
- package/src/ui/server/public/graph-model.js +2 -0
- package/src/ui/server/public/header.js +2 -3
- package/src/ui/server/public/health-rollup.js +20 -8
- package/src/ui/server/public/i18n/es.js +15 -13
- package/src/ui/server/public/i18n/ja.js +14 -13
- package/src/ui/server/public/index.html +17 -4
- package/src/ui/server/public/local-dev.html +28 -17
- package/src/ui/server/public/services.html +21 -5
- package/src/ui/server/public/settings-page.js +3 -2
- package/src/ui/server/public/settings-registry.js +225 -0
- package/src/ui/server/public/settings.html +90 -1
- package/src/ui/server/public/shell.js +28 -23
- package/src/ui/server/public/topbar.js +166 -206
- package/src/ui/server/readonly-routes.ts +29 -5
- package/src/ui/server/request-guard.ts +1 -1
- package/src/ui/server/route-classes.ts +14 -2
- package/src/ui/server/session-routes.ts +9 -1
- package/src/ui/server/sudo-store.ts +170 -6
- package/src/ui/server/workspace-routes.ts +10 -3
package/docs/distributions.md
CHANGED
|
@@ -352,7 +352,7 @@ where it is, under `secrets.refs`:
|
|
|
352
352
|
|
|
353
353
|
| Ref | Feature |
|
|
354
354
|
| -------------------- | ------------------------------------------------------------ |
|
|
355
|
-
| `sudo` | The saved
|
|
355
|
+
| `sudo` | The saved admin password, for sudo (auto-load, "Save to …") |
|
|
356
356
|
| `journeys_agent_key` | The journeys agent's API key, when `CURSOR_API_KEY` is unset |
|
|
357
357
|
|
|
358
358
|
```yaml
|
|
@@ -377,19 +377,37 @@ secrets:
|
|
|
377
377
|
and `env` need a `key` (or an env-shaped `field`). There are no defaults:
|
|
378
378
|
a ref that is not set is **not configured**, and its feature says so
|
|
379
379
|
("not configured: set secrets.provider and secrets.refs.sudo in
|
|
380
|
-
config/acme.yml") instead of running a CLI.
|
|
380
|
+
config/acme.yml") instead of running a CLI. Typing the password always
|
|
381
381
|
works; only the saved password needs `refs.sudo`.
|
|
382
382
|
`/api/sudo-password/vault-status` reports `backend`, `label`,
|
|
383
383
|
`configured`, `backendAvailable`, `reason` and `hasStored` (`opAvailable`
|
|
384
384
|
is a deprecated alias of `backendAvailable`, kept for one minor).
|
|
385
385
|
|
|
386
|
-
The
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
386
|
+
The admin password is for sudo, and nothing else (LK-0). It is no sign-in
|
|
387
|
+
and no access check: starting, stopping and resuming agents, sessions and
|
|
388
|
+
terminals, and a service whose commands run no sudo, never ask for it.
|
|
389
|
+
Den asks only when a command it runs is about to run sudo without it:
|
|
390
|
+
every service command (up, down, restart, setup, sync) gets a `sudo` shim
|
|
391
|
+
first on its `PATH`. With a password held, the shim gives it to sudo. With
|
|
392
|
+
none, sudo that needs no password (NOPASSWD, a warm cache) runs as it
|
|
393
|
+
would, and anything else never prompts: the shim prints
|
|
394
|
+
`<cli>: needs-sudo: sudo <command>` and fails, and the run's event stream
|
|
395
|
+
carries `{ "type": "needs-sudo", "command": "sudo <command>", "step":
|
|
396
|
+
"<service>" }` (sudo's own "a password is required" counts too, without
|
|
397
|
+
the command). Only then does the page ask, saying "`sudo <command>` needs
|
|
398
|
+
your admin password", with the 🔑 chip in the top bar while it waits. It
|
|
399
|
+
asks the server first (another tab or device may have set it), then, when
|
|
400
|
+
a password is stored, loads it from the vault (a page view alone never
|
|
401
|
+
wakes the vault), and only then shows the password field, always on top,
|
|
402
|
+
even over a dialog. After the password, the action runs again.
|
|
403
|
+
|
|
404
|
+
Settings › Command line › Admin password says whether it is saved ("Saved
|
|
405
|
+
in 1Password", "Saved for this session" or "Not saved"), with **Forget**:
|
|
406
|
+
`POST /api/sudo-password/forget` drops the password the server holds and,
|
|
407
|
+
when this server saved it to the vault or loaded it from there, clears the
|
|
408
|
+
vault's copy. `GET /api/sudo-password/status` answers `set`,
|
|
409
|
+
`passwordless`, `saved` (`vault`, `session` or null) and `vault` (`{
|
|
410
|
+
configured, label }`, read from the config alone).
|
|
393
411
|
|
|
394
412
|
A `1password` distribution with no refs gets one warning, in the web
|
|
395
413
|
server's startup log and in `<cli> validate`, with the YAML to add.
|
|
@@ -1190,8 +1208,8 @@ are the same directory.
|
|
|
1190
1208
|
## Signing in
|
|
1191
1209
|
|
|
1192
1210
|
On localhost nothing asks you to sign in: every page, the agents and the
|
|
1193
|
-
terminal work as they are, and the
|
|
1194
|
-
|
|
1211
|
+
terminal work as they are, and the admin password (R8) is the only prompt,
|
|
1212
|
+
asked only when a command Den runs needs sudo. The edge counts as local too (0.4.3): opening
|
|
1195
1213
|
`https://acme.acme.localhost` from this machine asks for nothing.
|
|
1196
1214
|
Signing in is for when the server is reached from another machine: the
|
|
1197
1215
|
edge for a client the edge says is elsewhere, a LAN address, or a bind
|
|
@@ -1448,19 +1466,40 @@ port.
|
|
|
1448
1466
|
`--workspace <ws>` and `--actor agent:<id>` (both or neither) scope the server
|
|
1449
1467
|
to one workspace for one agent:
|
|
1450
1468
|
|
|
1451
|
-
- **Pinned.** A tool that takes a `workspace` gets `<ws
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1469
|
+
- **Pinned.** A tool that takes a `workspace` gets `<ws>`. The server
|
|
1470
|
+
registers each workspace tool with "This workspace only" in its
|
|
1471
|
+
description and without the `workspace` parameter (with grants it stays,
|
|
1472
|
+
a name whose description lists `<ws>` and the granted ones; any other
|
|
1473
|
+
name gets the refusal below, not a schema error). A call naming another
|
|
1474
|
+
workspace is refused
|
|
1475
|
+
(`Refused: <tool>: this MCP server is scoped to workspace "<ws>"
|
|
1476
|
+
(agent:<id>; no grants); "<other>" is another workspace`; with grants,
|
|
1477
|
+
`grants: <a>, <b>`) before anything reaches the web server. The refusal
|
|
1478
|
+
says how to get more: a person adds `<other>` to `agents.<id>.grants` in
|
|
1479
|
+
the org config and restarts the agent; an agent can't grant itself one. A
|
|
1480
|
+
refused call is recorded in the activity trail, read tools included.
|
|
1481
|
+
- **Says so.** Every scoped answer (`status`, `services`, `applications`,
|
|
1482
|
+
`graph`, `stacks`, `workspaces`) carries a `scope` block: the workspace
|
|
1483
|
+
shown, how many other workspaces there are and how much it leaves out,
|
|
1484
|
+
for example `hidden: { repos: 6 }`, and a note pointing at `org_summary`.
|
|
1485
|
+
A scoped `graph` is the workspace's own repos and the edges out of them
|
|
1486
|
+
(`map: "workspace"`, with `outside` naming the repos they depend on
|
|
1487
|
+
elsewhere). Unscoped with no `workspace` it is the org-wide map
|
|
1488
|
+
(`map: "org-wide"`: every active workspace's graph merged, `workspaces`
|
|
1489
|
+
naming them); with one, that workspace's (`map: "workspace"`).
|
|
1456
1490
|
- **Filtered.** `workspaces` lists `<ws>` only. `logs_search` and `logs_tail`
|
|
1457
1491
|
take only `<ws>`'s services, `manifest_validate` takes only an absolute path
|
|
1458
1492
|
inside one of its repos (or the content), and `workspace_sync` syncs `<ws>`.
|
|
1459
1493
|
Tests run against the focused workspace, so `tests_available` and
|
|
1460
1494
|
`run_tests` are refused unless `<ws>` is the focused one.
|
|
1461
1495
|
- **Not workspace data**, served as unscoped: `environments`,
|
|
1462
|
-
`security_latest`, `release_status` and `
|
|
1463
|
-
distribution's, not a workspace's).
|
|
1496
|
+
`security_latest`, `release_status`, `edge_status` and `org_summary` (the
|
|
1497
|
+
org's and the distribution's, not a workspace's).
|
|
1498
|
+
- **The org at a glance.** `org_summary` gives every agent every workspace
|
|
1499
|
+
and repo with status counts, the graph, the edge and the environments
|
|
1500
|
+
(see [Tools](#tools)). Detail and actions stay scoped: for workspaces
|
|
1501
|
+
other than `<ws>` it carries names and counts only, never logs, env
|
|
1502
|
+
values, commands, URLs, paths or check output.
|
|
1464
1503
|
- **Stamped.** Every web API call carries `x-den-mcp-agent: agent:<id>` and
|
|
1465
1504
|
`x-den-mcp-workspace: <ws>` beside the usual audit headers. The activity
|
|
1466
1505
|
entry gains `agent` and `workspace`, and the log line reads `· by
|
|
@@ -1479,7 +1518,9 @@ agent:<id> <client> <version> (workspace <ws>, mcp pid <pid>, session
|
|
|
1479
1518
|
- `<ws>` must exist in this distribution; until it does, every tool says so.
|
|
1480
1519
|
The id is letters, digits, dots, dashes and underscores (at most 64). A bad
|
|
1481
1520
|
or half-given scope is a usage error, exit 2. `--list` and `--call` take
|
|
1482
|
-
the scope too
|
|
1521
|
+
the scope too: they list and validate against the pinned tools, as the
|
|
1522
|
+
server registers them, and `--call` names its scope on stderr
|
|
1523
|
+
(`acme mcp --call: scoped to <ws> as agent:<id>`).
|
|
1483
1524
|
|
|
1484
1525
|
**It binds cooperating agents only.** The scope lives in the MCP server's
|
|
1485
1526
|
process. The web server's HTTP API does not check it yet, so an agent that
|
|
@@ -1525,22 +1566,60 @@ guard off.
|
|
|
1525
1566
|
|
|
1526
1567
|
Read tools (always registered):
|
|
1527
1568
|
|
|
1528
|
-
| Tool | Web API | What
|
|
1529
|
-
| ------------------- | ------------------------------------------ |
|
|
1530
|
-
| `status` | `/api/health` | Service health
|
|
1531
|
-
| `services` | `/api/services` | Services, their mode (managed or dev-managed) and commands
|
|
1532
|
-
| `applications` | `/api/applications` | Org > Application > Repo > Services
|
|
1533
|
-
| `graph` | `/api/graph` | The derived dependency graph and startup order
|
|
1534
|
-
| `workspaces` | `/api/workspaces` | Workspaces, active and focused
|
|
1535
|
-
| `environments` | `/api/environments` | The enabled environment names (never their values)
|
|
1536
|
-
| `logs_search` | `/api/logs/search` | Search services' full log history
|
|
1537
|
-
| `logs_tail` | `/api/service/taillog` | The last lines of a service's log (bounded, no follow)
|
|
1538
|
-
| `tests_available` | `/api/modules`, `/api/test-sources` | A pillar's modules and the repos they come from
|
|
1539
|
-
| `security_latest` | `/api/security/latest` | The newest security scan per mode
|
|
1540
|
-
| `release_status` | `/api/release/runs`, `/api/release/status` | Recent CD runs, and an env's deployed-version table
|
|
1541
|
-
| `manifest_validate` | `/api/manifest/validate` | Validate a manifest by path or content
|
|
1542
|
-
| `stacks` | `/api/stacks` | What `stack ls/ps/health` show: project, routes, health
|
|
1543
|
-
| `edge_status` | `/api/edge` | The edge: up or down, its routes and cert names
|
|
1569
|
+
| Tool | Web API | What |
|
|
1570
|
+
| ------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1571
|
+
| `status` | `/api/health` | Service health: `counts`, then each service's `state` (`up`, `down`, `redirect`, `pending`, `unknown`, `not-checked`) and, without a plain verdict, `why`; `url` (HTTP check) or `check` (shell, or `container <project>/<service>`); `ok` true up, false down, null not known |
|
|
1572
|
+
| `services` | `/api/services` | Services, their mode (managed or dev-managed) and commands |
|
|
1573
|
+
| `applications` | `/api/applications` | Org > Application > Repo > Services |
|
|
1574
|
+
| `graph` | `/api/graph` | The derived dependency graph (`map: "org-wide"`, every active workspace's; with `workspace`, that one's and its startup order; scoped, the workspace's own) |
|
|
1575
|
+
| `workspaces` | `/api/workspaces` | Workspaces, active and focused |
|
|
1576
|
+
| `environments` | `/api/environments` | The enabled environment names (never their values) |
|
|
1577
|
+
| `logs_search` | `/api/logs/search` | Search services' full log history |
|
|
1578
|
+
| `logs_tail` | `/api/service/taillog` | The last lines of a service's log (bounded, no follow) |
|
|
1579
|
+
| `tests_available` | `/api/modules`, `/api/test-sources` | A pillar's modules and the repos they come from |
|
|
1580
|
+
| `security_latest` | `/api/security/latest` | The newest security scan per mode |
|
|
1581
|
+
| `release_status` | `/api/release/runs`, `/api/release/status` | Recent CD runs, and an env's deployed-version table |
|
|
1582
|
+
| `manifest_validate` | `/api/manifest/validate` | Validate a manifest by path or content |
|
|
1583
|
+
| `stacks` | `/api/stacks` | What `stack ls/ps/health` show: project, routes, health; an empty list says `why` |
|
|
1584
|
+
| `edge_status` | `/api/edge` | The edge: up or down, its routes and cert names; `https` (TLS strategy, who supplies the cert, whether HTTPS is serving) |
|
|
1585
|
+
| `org_summary` | several (read only) | Every workspace and repo with status counts, the graph, the edge and the environments; scoped, `yours` adds your workspace's services |
|
|
1586
|
+
|
|
1587
|
+
**Health states.** A service with no verdict says why in `why`: no health
|
|
1588
|
+
check in the manifest for the env, remote with no check or `target.url`
|
|
1589
|
+
for its env, not deployed in the env (no commands and no check there), no
|
|
1590
|
+
manifest, or checked but no answer yet. A redirect (`302 → /login`) is
|
|
1591
|
+
`redirect` with its status and target (the query is dropped), not down:
|
|
1592
|
+
something answered. It is never green either, so an unexpected redirect
|
|
1593
|
+
(an ALB answering for a stopped service) is not taken for up; list the code
|
|
1594
|
+
in `target.expect` to count it as up. The web health row and `<cli> status`
|
|
1595
|
+
use the same rule: the web row shows a redirect muted (with `status` and `redirect`
|
|
1596
|
+
in `/api/health`), and `<cli> status` as `redirect 302 → /login`.
|
|
1597
|
+
|
|
1598
|
+
**`org_summary`** reads `/api/workspaces`, `/api/health` (local, every
|
|
1599
|
+
active workspace), `/api/graph`, `/api/edge` and `/api/environments`:
|
|
1600
|
+
|
|
1601
|
+
- `workspaces`: each workspace's name, whether it is active, focused or
|
|
1602
|
+
yours, its state (`running`: every checked service answers; `partial`:
|
|
1603
|
+
some are down; `stopped`: all checked ones are down; `unknown`: nothing
|
|
1604
|
+
checked; `inactive`: not polled), its counts (up, down, redirect,
|
|
1605
|
+
pending, not checked) and its repos, each with its rolled-up state.
|
|
1606
|
+
- `graph`: the org-wide map, every active workspace's repos merged (each
|
|
1607
|
+
workspace's graph from the web server; `workspaces` names them, and
|
|
1608
|
+
`unavailable` any whose graph failed), repo names and edge kinds only (no
|
|
1609
|
+
evidence, paths or URLs).
|
|
1610
|
+
- `edge`: up, state, domains, the route count and `https`.
|
|
1611
|
+
- `environments`: the enabled names, never their values.
|
|
1612
|
+
- scoped, `yours`: your workspace's services as `status` gives them.
|
|
1613
|
+
|
|
1614
|
+
**`edge_status.https`** says which TLS strategy is in use (`mkcert`,
|
|
1615
|
+
`files`, `none`), who supplies the cert (`den`, or `distribution` under
|
|
1616
|
+
`tls: files` and the legacy `issue_cert: false`) and whether HTTPS is
|
|
1617
|
+
serving: a TLS handshake on the edge's published https port (`serving`
|
|
1618
|
+
true or false, null when a proxy in front owns the port), with `detail`.
|
|
1619
|
+
`certIssuedByDen` replaces `certIssued`, which is kept for 0.4.4 with a
|
|
1620
|
+
note in `deprecated`: it said whether Den minted the cert, so it was
|
|
1621
|
+
`false` under `tls: files` while HTTPS worked. `edge status` prints the
|
|
1622
|
+
same `https` line.
|
|
1544
1623
|
|
|
1545
1624
|
Action tools (only those named in `mcp.actions`): `service_up`,
|
|
1546
1625
|
`service_down`, `service_restart` (through `/api/service/exec`),
|
|
@@ -1658,7 +1737,7 @@ server scoped to its workspace.
|
|
|
1658
1737
|
<cli> agents stop <id> [-w <ws>]
|
|
1659
1738
|
<cli> agents resume <id> [-w <ws>]
|
|
1660
1739
|
<cli> agents personas [--json]
|
|
1661
|
-
<cli> agents doctor [--json]
|
|
1740
|
+
<cli> agents doctor [-w <ws>] [--json] # can each runtime run here, and why not
|
|
1662
1741
|
<cli> sessions attach <ws>:<id> # its terminal; ctrl-] detaches
|
|
1663
1742
|
```
|
|
1664
1743
|
|
|
@@ -1708,9 +1787,10 @@ started. The workspace header counts them ("2 agents").
|
|
|
1708
1787
|
- The page and the CLI call the same operations and read the same agent
|
|
1709
1788
|
records, so an agent started in one is listed in the other. A workspace
|
|
1710
1789
|
lists its own agents only.
|
|
1711
|
-
- Start, Stop and Resume
|
|
1712
|
-
|
|
1713
|
-
for an agent (agents don't
|
|
1790
|
+
- Start, Stop and Resume never ask for the admin password (it is for
|
|
1791
|
+
sudo, and no agent action runs sudo), are capped by
|
|
1792
|
+
`agents.max_concurrent`, and are refused for an agent (agents don't
|
|
1793
|
+
start agents from the page). Each one is in the
|
|
1714
1794
|
server log's activity, with you as its actor. The agent's environment is
|
|
1715
1795
|
the one below, built from the web server's environment.
|
|
1716
1796
|
- **Open in Agents →** (the section header) opens the Agents tab for the
|
|
@@ -1720,7 +1800,7 @@ The routes: `GET /api/workspaces/<ws>/agents`, `POST
|
|
|
1720
1800
|
/api/workspaces/<ws>/agents` (`{ persona, runtime?, model?, id? }`), `POST
|
|
1721
1801
|
/api/workspaces/<ws>/agents/<id>/stop` and `…/resume`, and `GET
|
|
1722
1802
|
/api/personas` (personas and runtimes). Errors are the usual
|
|
1723
|
-
`{ ok: false, error, code, requestId }`: `
|
|
1803
|
+
`{ ok: false, error, code, requestId }`: `cap` (429),
|
|
1724
1804
|
`conflict` (409, e.g. already running, or no resume), `not_found` (404),
|
|
1725
1805
|
`exited_at_once` (502, with the session's last lines).
|
|
1726
1806
|
|
|
@@ -1743,12 +1823,10 @@ agent of the workspaces in view, and their terminals in the browser.
|
|
|
1743
1823
|
reload or Back reopens them.
|
|
1744
1824
|
- **Close** (×) detaches, as leaving the page does: the session keeps
|
|
1745
1825
|
running, and opening it again replays what you missed. **Stop** (an
|
|
1746
|
-
agent only) is `agents stop
|
|
1826
|
+
agent only) is `agents stop`. **+ Start agent** (or
|
|
1747
1827
|
`/agents?start=1`) starts one and opens its pane; under All it asks for
|
|
1748
1828
|
the workspace. It says before Start whether the runtime can run, as the
|
|
1749
|
-
Workspaces page's form does.
|
|
1750
|
-
for the admin password at its top, and Start unlocks, then starts, without
|
|
1751
|
-
closing anything.
|
|
1829
|
+
Workspaces page's form does. It never asks for the admin password.
|
|
1752
1830
|
- ⌘K lists each as "Agents › <ws> / <name>", and "Start agent…".
|
|
1753
1831
|
- Several browsers and `<cli> sessions attach` can attach to one session
|
|
1754
1832
|
at once and each sees every byte; the terminal's size is the last
|
|
@@ -1813,8 +1891,9 @@ header (agent headers without the token are recorded as an unverified
|
|
|
1813
1891
|
claim).
|
|
1814
1892
|
|
|
1815
1893
|
An agent's token reaches only its own session's routes, and never a
|
|
1816
|
-
shell-grade route, `/api/web/*`, sign-in or sign-out, or the
|
|
1817
|
-
|
|
1894
|
+
shell-grade route, `/api/web/*`, sign-in or sign-out, or the admin
|
|
1895
|
+
password's routes: it can't set the admin password, use it for sudo or
|
|
1896
|
+
start another agent (403
|
|
1818
1897
|
`agent_forbidden`). A person's credential never reaches a session:
|
|
1819
1898
|
`DEN_PERSON_TOKEN`, a person token's value and a path to `web.secret` are
|
|
1820
1899
|
always removed, and `sessions start --keep-env` of one is refused and logged.
|
|
@@ -1835,7 +1914,8 @@ it does not catch every spelling: a path relative to the agent's cwd
|
|
|
1835
1914
|
(`cat ../.den/web.secret`, say), another program, a script or a copy under
|
|
1836
1915
|
another name all get past it. The server's token rules (above)
|
|
1837
1916
|
are the boundary: whatever an agent reads, its token reaches only its own
|
|
1838
|
-
session's routes, never a shell-grade route or the
|
|
1917
|
+
session's routes, never a shell-grade route or the admin password's.
|
|
1918
|
+
OS-user isolation
|
|
1839
1919
|
(a separate user, a container, a sandbox) is not in 0.4.x. `web logout
|
|
1840
1920
|
--all` revokes every agent's token (above, under Signing in).
|
|
1841
1921
|
|
|
@@ -1884,7 +1964,8 @@ are errors naming the file or the key.
|
|
|
1884
1964
|
|
|
1885
1965
|
### Runtimes
|
|
1886
1966
|
|
|
1887
|
-
`<cli> doctor` lists the runtime kind.
|
|
1967
|
+
`<cli> doctor` lists the runtime kind. Den knows the runtimes in its
|
|
1968
|
+
**catalog** (below); two are special:
|
|
1888
1969
|
|
|
1889
1970
|
- **`den-claude`** (the default) runs Claude Code's `claude`, found as
|
|
1890
1971
|
below; `<cli> doctor` reads its version from the install, without running
|
|
@@ -1899,14 +1980,64 @@ are errors naming the file or the key.
|
|
|
1899
1980
|
- **`generic`** runs the persona's `argv` template. `{model}` is the agent's
|
|
1900
1981
|
model (an error when the template uses it and there is none);
|
|
1901
1982
|
`{mcp_config}` is the path of the agent's MCP config, written only when the
|
|
1902
|
-
template references it. It declares no resume.
|
|
1983
|
+
template references it. It declares no resume. It is startable only for
|
|
1984
|
+
a persona that has an `argv`.
|
|
1985
|
+
|
|
1986
|
+
**The runtime catalog.** Den ships, as data, every vendor CLI it knows how
|
|
1987
|
+
to find. Each entry was checked against the vendor's own docs or installer:
|
|
1988
|
+
|
|
1989
|
+
| id | Vendor | CLI | Found in (after PATH) | Source |
|
|
1990
|
+
| ------------ | --------- | --------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
1991
|
+
| `den-claude` | Anthropic | Claude Code, `claude` | `~/.local/bin`, `~/.claude/local`, system dirs | [setup](https://code.claude.com/docs/en/setup) |
|
|
1992
|
+
| `codex` | OpenAI | Codex CLI, `codex` | `$CODEX_INSTALL_DIR`, `~/.local/bin`, system dirs | [openai/codex](https://github.com/openai/codex), [installer](https://chatgpt.com/codex/install.sh) |
|
|
1993
|
+
| `gemini` | Google | Gemini CLI, `gemini` | system dirs | [google-gemini/gemini-cli](https://github.com/google-gemini/gemini-cli) |
|
|
1994
|
+
| `grok` | xAI | Grok Build, `grok` | `$GROK_BIN_DIR`, `~/.grok/bin`, `~/.local/bin`, `/usr/local/bin` | [Grok Build](https://docs.x.ai/build/overview), [installer](https://x.ai/cli/install.sh) |
|
|
1995
|
+
|
|
1996
|
+
"System dirs" are `/opt/homebrew/bin`, `/usr/local/bin` and npm's global
|
|
1997
|
+
bin. Each one's version is `<bin> --version`. Every vendor here publishes
|
|
1998
|
+
an official CLI, so none is left out; a vendor without one is left out
|
|
1999
|
+
rather than guessed at (community CLIs, such as third-party "grok" CLIs,
|
|
2000
|
+
are not xAI's and are not in the catalog).
|
|
2001
|
+
|
|
2002
|
+
Claude Code runs as `den-claude` (above). Every other installed runtime
|
|
2003
|
+
runs its own interactive CLI (`codex`, `gemini`, `grok`) in the
|
|
2004
|
+
workspace's directory; Den's tools (MCP) and permission modes are coming
|
|
2005
|
+
for them, and lists tag them **Den tools: coming**. A runtime is offered
|
|
2006
|
+
as startable only when it can start here: installed (or, for `generic`,
|
|
2007
|
+
with the persona's `argv`).
|
|
2008
|
+
|
|
2009
|
+
A distribution adds entries with `agents.runtime_catalog` in its config,
|
|
2010
|
+
and a person with the same key in `config/local/user.yml`. Both go
|
|
2011
|
+
through the validator the built-in entries pass; an entry can't reuse a
|
|
2012
|
+
catalog id (set a path instead, below):
|
|
2013
|
+
|
|
2014
|
+
```yaml
|
|
2015
|
+
agents:
|
|
2016
|
+
runtime_catalog:
|
|
2017
|
+
- id: aider # lowercase, starts with a letter
|
|
2018
|
+
vendor: Aider
|
|
2019
|
+
name: Aider
|
|
2020
|
+
bins: [aider] # executable names, no paths
|
|
2021
|
+
version:
|
|
2022
|
+
args: [--version]
|
|
2023
|
+
identity: # at least one: how a path is known to be it
|
|
2024
|
+
output: "aider" # a pattern its version output matches
|
|
2025
|
+
# package: aider-chat (the npm package it resolves into)
|
|
2026
|
+
# realpath_under: [~/.aider] (where it really lives)
|
|
2027
|
+
dirs: [~/.local/bin, $AIDER_DIR, "{system}"]
|
|
2028
|
+
launch:
|
|
2029
|
+
argv: ["{bin}"] # "{bin}" first: the path found
|
|
2030
|
+
```
|
|
1903
2031
|
|
|
1904
2032
|
**Where a runtime's CLI is found.** As your terminal would find it, not
|
|
1905
2033
|
only on the web server's own PATH (a launchd service's PATH leaves out
|
|
1906
2034
|
`~/.local/bin`, where Claude Code's native installer puts `claude`). The
|
|
1907
2035
|
first match wins:
|
|
1908
2036
|
|
|
1909
|
-
1.
|
|
2037
|
+
1. **A workspace's own path**, `<ws>.agents.runtimes.<id>.bin` in
|
|
2038
|
+
`config/local/workspaces.yml`, for that workspace's agents. Checked the
|
|
2039
|
+
same way as the next one.
|
|
2040
|
+
2. **`agents.runtimes.<id>.bin`** in `config/local/user.yml`: the absolute
|
|
1910
2041
|
path of the runtime's CLI. Paths are per machine, so it is read from
|
|
1911
2042
|
user.yml only, never from the distribution's config. A bad value (not
|
|
1912
2043
|
absolute, missing, not an executable file) makes the runtime
|
|
@@ -1920,14 +2051,28 @@ first match wins:
|
|
|
1920
2051
|
bin: /Users/me/.local/bin/claude
|
|
1921
2052
|
```
|
|
1922
2053
|
|
|
1923
|
-
|
|
2054
|
+
3. **Your login shell's PATH**: the web server asks `$SHELL -lc` once at
|
|
1924
2055
|
start, in the background, under a 3-second timeout. On a timeout or
|
|
1925
2056
|
any failure it is left out.
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
`~/.claude/local`, `/opt/homebrew/bin`,
|
|
1929
|
-
global bin
|
|
1930
|
-
never runs npm to find it.
|
|
2057
|
+
4. **The server's own PATH.**
|
|
2058
|
+
5. **The runtime's install directories** (its catalog entry's; for
|
|
2059
|
+
den-claude: `~/.local/bin`, `~/.claude/local`, `/opt/homebrew/bin`,
|
|
2060
|
+
`/usr/local/bin` and npm's global bin, `$npm_config_prefix/bin` and
|
|
2061
|
+
node's own directory). Den never runs npm to find it.
|
|
2062
|
+
6. **Your interactive shell's PATH**, only while a catalog runtime is
|
|
2063
|
+
still not found: `$SHELL -ilc` once (stdin closed, 3-second timeout,
|
|
2064
|
+
the answer kept), for a PATH set only in an interactive startup file
|
|
2065
|
+
such as `~/.zshrc`. Its source is `interactive-shell`.
|
|
2066
|
+
|
|
2067
|
+
Paths are this machine's only: Den reads them from `config/local` files,
|
|
2068
|
+
never from the distribution's config. **Settings › Agents › Runtimes**
|
|
2069
|
+
shows each runtime's status, version, the path in use and its source.
|
|
2070
|
+
**Set path…** runs the runtime's version command and refuses a path that
|
|
2071
|
+
isn't that runtime (its output, npm package or install location must name
|
|
2072
|
+
it); **Clear** removes it. The **Paths for** selector switches between
|
|
2073
|
+
every workspace (`user.yml`) and one workspace (`workspaces.yml`). Only a
|
|
2074
|
+
person sets a path, never an agent (`POST /api/agents/runtimes/<id>/path`,
|
|
2075
|
+
audited).
|
|
1931
2076
|
|
|
1932
2077
|
Sessions and agents Den starts get the same directories on their PATH
|
|
1933
2078
|
(and a configured CLI's own directory), so the runtime's own tools are
|
|
@@ -1936,17 +2081,19 @@ diagnosis: `DEN_LOGIN_SHELL` (the shell to ask instead of `$SHELL`; `off`
|
|
|
1936
2081
|
for none) and `DEN_RUNTIME_DIRS` (a PATH-style list that replaces every
|
|
1937
2082
|
runtime's install directories; empty for none).
|
|
1938
2083
|
|
|
1939
|
-
**`<cli> agents doctor`** prints, for each runtime
|
|
1940
|
-
runs and where that came from (the
|
|
1941
|
-
shell's PATH, PATH,
|
|
2084
|
+
**`<cli> agents doctor`** prints, for each runtime (installed first), its
|
|
2085
|
+
status, the CLI it runs and where that came from (the workspace's or the
|
|
2086
|
+
user.yml `agents.runtimes` setting, the login shell's PATH, PATH, an
|
|
2087
|
+
install directory, or the interactive shell's PATH), its version (`claude
|
|
1942
2088
|
--version`, run once under a timeout), the directories it searched (names
|
|
1943
2089
|
only, never an environment value), and the fix when it's missing. It
|
|
1944
2090
|
resolves the login shell's PATH first, as the web server does. It exits 1
|
|
1945
|
-
when the default runtime is unavailable;
|
|
1946
|
-
dialog gets.
|
|
2091
|
+
when the default runtime is unavailable; `-w <ws>` uses that workspace's
|
|
2092
|
+
paths; `--json` prints what the Start dialog gets. A search list the same
|
|
2093
|
+
as the one above it prints once.
|
|
1947
2094
|
|
|
1948
2095
|
```
|
|
1949
|
-
den-claude (Claude Code) [default]
|
|
2096
|
+
den-claude (Claude Code, Anthropic) [default]
|
|
1950
2097
|
status ✗ unavailable: claude not found (searched 5 directories)
|
|
1951
2098
|
searched /usr/bin
|
|
1952
2099
|
/bin
|
|
@@ -1955,9 +2102,12 @@ den-claude (Claude Code) [default]
|
|
|
1955
2102
|
then run `<cli> agents doctor` again
|
|
1956
2103
|
```
|
|
1957
2104
|
|
|
1958
|
-
`GET /api/personas`
|
|
1959
|
-
|
|
1960
|
-
`
|
|
2105
|
+
`GET /api/personas` and `GET /api/agents/runtimes` (each with an optional
|
|
2106
|
+
`?workspace=<ws>`) give each runtime its `vendor`, `denTools`, `version`,
|
|
2107
|
+
`path`, `source` (`workspace`, `setting`, `login-shell`, `PATH`,
|
|
2108
|
+
`well-known` or `interactive-shell`), `searched`, `setting` (the user.yml
|
|
2109
|
+
key) and `workspaceSetting` (the workspaces.yml key), and `userConfig`,
|
|
2110
|
+
the user.yml to edit.
|
|
1961
2111
|
|
|
1962
2112
|
### Workspace scope and grants
|
|
1963
2113
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A runtime from a catalog entry (AG-0.5, catalog.ts): found by AG-0.1's
|
|
3
|
+
* search with the entry's binary names and install directories, and
|
|
4
|
+
* launched as its interactive CLI in the agent's directory (the entry's
|
|
5
|
+
* launch argv, "{bin}" the path found).
|
|
6
|
+
*
|
|
7
|
+
* These don't get Den's tools yet: no MCP config, no permission mode
|
|
8
|
+
* (AG-12 and AG-13 wire them per runtime). Lists tag them "Den tools:
|
|
9
|
+
* coming" (denTools: false). Claude Code is den-claude, which takes its
|
|
10
|
+
* search from the catalog and keeps its own launch.
|
|
11
|
+
*
|
|
12
|
+
* checkRuntimeBin is what "Set path…" runs: the entry's version command,
|
|
13
|
+
* and the entry's identity (its output names it, it resolves into the
|
|
14
|
+
* vendor's npm package, or under the vendor's install directory), so a
|
|
15
|
+
* path that isn't that runtime is refused.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from "fs";
|
|
18
|
+
import * as path from "path";
|
|
19
|
+
import { configuredRuntimeBin } from "../den-config.js";
|
|
20
|
+
import { entryDirs, type CatalogEntry } from "./catalog.js";
|
|
21
|
+
import {
|
|
22
|
+
LaunchError,
|
|
23
|
+
type DetectOptions,
|
|
24
|
+
type LaunchOutput,
|
|
25
|
+
type Runtime,
|
|
26
|
+
type RuntimeDetect,
|
|
27
|
+
} from "./runtime.js";
|
|
28
|
+
import {
|
|
29
|
+
locateRuntime,
|
|
30
|
+
parseVersion,
|
|
31
|
+
systemBinDirs,
|
|
32
|
+
versionOutput,
|
|
33
|
+
type RuntimeFind,
|
|
34
|
+
} from "./runtime-search.js";
|
|
35
|
+
|
|
36
|
+
/** An entry as AG-0.1's search takes it. */
|
|
37
|
+
export function catalogFind(e: CatalogEntry): RuntimeFind {
|
|
38
|
+
return {
|
|
39
|
+
bins: e.bins,
|
|
40
|
+
dirs: (home, env) => entryDirs(e, home, env, systemBinDirs),
|
|
41
|
+
versionArgs: e.version.args,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Find an entry's CLI: a setting (workspace or global), else the search. */
|
|
46
|
+
export function detectEntry(
|
|
47
|
+
e: CatalogEntry,
|
|
48
|
+
find: RuntimeFind,
|
|
49
|
+
env: NodeJS.ProcessEnv,
|
|
50
|
+
opts: DetectOptions,
|
|
51
|
+
): RuntimeDetect {
|
|
52
|
+
const setting =
|
|
53
|
+
opts.bin === undefined ? configuredRuntimeBin(e.id) : opts.bin;
|
|
54
|
+
const found = locateRuntime(e.id, find, env, setting);
|
|
55
|
+
if (!found.available || !found.path)
|
|
56
|
+
return {
|
|
57
|
+
available: false,
|
|
58
|
+
why: found.why ?? `${e.bins.join(" or ")} not found`,
|
|
59
|
+
...(found.source ? { source: found.source } : {}),
|
|
60
|
+
searched: found.searched,
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
available: true,
|
|
64
|
+
path: found.path,
|
|
65
|
+
...(found.source ? { source: found.source } : {}),
|
|
66
|
+
searched: found.searched,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The launch argv: the entry's template, "{bin}" the path found. */
|
|
71
|
+
export function launchArgv(e: CatalogEntry, bin: string): string[] {
|
|
72
|
+
return e.launch.argv.map((a) => a.split("{bin}").join(bin));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A runtime for a catalog entry other than Claude Code (den-claude). */
|
|
76
|
+
export function catalogRuntime(e: CatalogEntry): Runtime {
|
|
77
|
+
const find = catalogFind(e);
|
|
78
|
+
const detect = (
|
|
79
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
80
|
+
opts: DetectOptions = {},
|
|
81
|
+
) => detectEntry(e, find, env, opts);
|
|
82
|
+
return {
|
|
83
|
+
id: e.id,
|
|
84
|
+
label: e.name,
|
|
85
|
+
vendor: e.vendor,
|
|
86
|
+
catalog: e,
|
|
87
|
+
find,
|
|
88
|
+
denTools: false,
|
|
89
|
+
detect,
|
|
90
|
+
launch(input): LaunchOutput {
|
|
91
|
+
let bin = input.bin;
|
|
92
|
+
if (!bin) {
|
|
93
|
+
const d = detect(input.env);
|
|
94
|
+
if (!d.available || !d.path)
|
|
95
|
+
throw new LaunchError(`${e.id}: ${d.why ?? "not found"}`);
|
|
96
|
+
bin = d.path;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
argv: launchArgv(e, bin),
|
|
100
|
+
env: {},
|
|
101
|
+
warnings: [
|
|
102
|
+
`${e.name} runs as its own CLI in ${input.worktree}: Den's tools (MCP) and permission modes are coming for it`,
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
approvals: "terminal",
|
|
107
|
+
scrubEnv: [],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The npm package a file resolves into (the nearest package.json's name). */
|
|
112
|
+
function packageOf(real: string): string | undefined {
|
|
113
|
+
for (let dir = path.dirname(real), i = 0; i < 8; i++) {
|
|
114
|
+
try {
|
|
115
|
+
const pkg = JSON.parse(
|
|
116
|
+
fs.readFileSync(path.join(dir, "package.json"), "utf-8"),
|
|
117
|
+
) as { name?: unknown };
|
|
118
|
+
if (typeof pkg.name === "string") return pkg.name;
|
|
119
|
+
} catch {
|
|
120
|
+
/* not here */
|
|
121
|
+
}
|
|
122
|
+
const up = path.dirname(dir);
|
|
123
|
+
if (up === dir) break;
|
|
124
|
+
dir = up;
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type BinCheck =
|
|
130
|
+
| { ok: true; path: string; version: string }
|
|
131
|
+
| { ok: false; why: string };
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Whether `bin` is entry `e`'s runtime: an absolute path to an executable
|
|
135
|
+
* file whose version command exits 0 and prints a version, and whose
|
|
136
|
+
* output, npm package or real location names it. Runs only that command
|
|
137
|
+
* (stdin closed, under a timeout). Never throws.
|
|
138
|
+
*/
|
|
139
|
+
export async function checkRuntimeBin(
|
|
140
|
+
e: CatalogEntry,
|
|
141
|
+
bin: string,
|
|
142
|
+
env: NodeJS.ProcessEnv,
|
|
143
|
+
): Promise<BinCheck> {
|
|
144
|
+
const p = bin.trim();
|
|
145
|
+
const shown = p.replace(/[^\x20-\x7e]/g, "?").slice(0, 200);
|
|
146
|
+
if (!path.isAbsolute(p))
|
|
147
|
+
return { ok: false, why: `"${shown}" is not an absolute path` };
|
|
148
|
+
let real: string;
|
|
149
|
+
try {
|
|
150
|
+
const st = fs.statSync(p);
|
|
151
|
+
fs.accessSync(p, fs.constants.X_OK);
|
|
152
|
+
if (!st.isFile()) throw new Error("not a file");
|
|
153
|
+
real = fs.realpathSync(p);
|
|
154
|
+
} catch {
|
|
155
|
+
return { ok: false, why: `${shown} is not an executable file` };
|
|
156
|
+
}
|
|
157
|
+
const cmd = `${path.basename(p)} ${e.version.args.join(" ")}`;
|
|
158
|
+
const r = await versionOutput(p, e.version.args, env);
|
|
159
|
+
if (!r.ok) return { ok: false, why: `\`${cmd}\` failed: not ${e.name}` };
|
|
160
|
+
const version = parseVersion(r.out);
|
|
161
|
+
if (!version)
|
|
162
|
+
return { ok: false, why: `\`${cmd}\` printed no version: not ${e.name}` };
|
|
163
|
+
const idn = e.version.identity;
|
|
164
|
+
const home = env.HOME && path.isAbsolute(env.HOME) ? env.HOME : undefined;
|
|
165
|
+
const under = (idn.realpath_under ?? []).flatMap((d) =>
|
|
166
|
+
d.startsWith("~/") ? (home ? [path.join(home, d.slice(2))] : []) : [d],
|
|
167
|
+
);
|
|
168
|
+
const named =
|
|
169
|
+
(idn.output !== undefined && new RegExp(idn.output, "i").test(r.out)) ||
|
|
170
|
+
(idn.package !== undefined && packageOf(real) === idn.package) ||
|
|
171
|
+
under.some((d) => real === d || real.startsWith(d + path.sep));
|
|
172
|
+
if (!named)
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
why: `${shown} is not ${e.name}: its version output, package and location don't name it`,
|
|
176
|
+
};
|
|
177
|
+
return { ok: true, path: p, version };
|
|
178
|
+
}
|