@monster-orange/den 0.4.4 → 0.4.6
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 +243 -0
- package/README.md +24 -9
- package/docs/distributions.md +320 -26
- package/docs/reference.md +8 -3
- package/package.json +4 -3
- package/skills/install-den/SKILL.md +114 -0
- package/src/shared/agent-setup.ts +9 -0
- package/src/shared/auth/sudo-vault.ts +115 -11
- package/src/shared/cli-command.ts +186 -0
- package/src/shared/confirm-code.ts +34 -0
- package/src/shared/constants.ts +7 -0
- package/src/shared/den-config.ts +7 -0
- package/src/shared/dev-metrics/emit.ts +59 -2
- package/src/shared/edge/config.ts +39 -0
- package/src/shared/edge/render.ts +92 -18
- package/src/shared/edge/runtime.ts +57 -1
- package/src/shared/edge/types.ts +11 -1
- package/src/shared/github-app.ts +94 -0
- package/src/shared/github-device.ts +286 -0
- package/src/shared/port-pick.ts +60 -0
- package/src/shared/secrets/onepassword.ts +27 -4
- package/src/shared/web-owner.ts +91 -0
- package/src/shared/web-session.ts +135 -1
- package/src/ui/cli/create-main.ts +1 -1
- package/src/ui/cli/create.ts +263 -30
- package/src/ui/cli/doctor-command.ts +67 -4
- package/src/ui/cli/doctor-onboard.ts +291 -0
- package/src/ui/cli/flag-parser.ts +5 -1
- package/src/ui/cli/index.ts +246 -4
- package/src/ui/cli/json-out.ts +49 -0
- package/src/ui/cli/mcp-connect.ts +179 -15
- package/src/ui/cli/repo-command.ts +490 -0
- package/src/ui/cli/repo-detect.ts +192 -0
- package/src/ui/cli/web-command.ts +513 -33
- package/src/ui/mcp/connect.ts +134 -0
- package/src/ui/server/device-flows.ts +274 -0
- package/src/ui/server/edge-exposure.ts +16 -4
- package/src/ui/server/github-routes.ts +321 -0
- package/src/ui/server/http-errors.ts +10 -0
- package/src/ui/server/index.ts +463 -27
- package/src/ui/server/login-waits.ts +203 -0
- package/src/ui/server/pair-requests.ts +237 -0
- package/src/ui/server/pair-routes.ts +196 -0
- package/src/ui/server/public/error-page.js +452 -40
- package/src/ui/server/public/error.html +59 -0
- package/src/ui/server/public/i18n/es.js +74 -8
- package/src/ui/server/public/i18n/ja.js +74 -8
- package/src/ui/server/public/local-dev.html +16 -4
- package/src/ui/server/public/qr.js +317 -0
- package/src/ui/server/public/settings-page.js +10 -3
- package/src/ui/server/public/settings-registry.js +392 -1
- package/src/ui/server/public/shell.js +123 -10
- package/src/ui/server/public/topbar.js +7 -3
- package/src/ui/server/public/workspace-picker.js +1 -1
- package/src/ui/server/request-guard.ts +103 -36
- package/src/ui/server/route-classes.ts +102 -2
- package/src/ui/server/ui-assets.ts +2 -0
package/docs/distributions.md
CHANGED
|
@@ -12,6 +12,7 @@ Den's own checkout is itself a distribution, named `den`.
|
|
|
12
12
|
- [The bin shim](#the-bin-shim)
|
|
13
13
|
- [make: den.mk](#make-denmk)
|
|
14
14
|
- [Two roots](#two-roots)
|
|
15
|
+
- [Dev metrics: local unless you opt in](#dev-metrics-local-unless-you-opt-in)
|
|
15
16
|
- [Pinning and upgrading](#pinning-and-upgrading)
|
|
16
17
|
- [Developing Den against a distribution](#developing-den-against-a-distribution)
|
|
17
18
|
|
|
@@ -31,8 +32,22 @@ From a Den checkout, `./bin/den create acme ../acme` does the same.
|
|
|
31
32
|
| `--title <title>` | `<Name>` Title Case | Brand name shown to people |
|
|
32
33
|
| `--cli <cli>` | `<name>` | The command people type |
|
|
33
34
|
| `--env-prefix <PREFIX>` | `<NAME>` | Env-var prefix (`ACME` → `ACME_HOME`) |
|
|
35
|
+
| `--yes`, `-y` | | Never prompt (create never does) |
|
|
36
|
+
| `--json` | | One JSON object on stdout |
|
|
37
|
+
| `--port <n>` | `7777`, else free | The web UI's port |
|
|
34
38
|
|
|
35
39
|
`create` refuses a directory that is not empty and never overwrites a file.
|
|
40
|
+
A directory that already holds this distribution (its `den.yml` names the
|
|
41
|
+
same product and command) is not an error: `create` says so and writes
|
|
42
|
+
nothing, so it is safe to re-run.
|
|
43
|
+
|
|
44
|
+
The web UI's port is 7777 unless that port is in use on this machine (another
|
|
45
|
+
Den already running, say). Then `create` picks the next free port above it,
|
|
46
|
+
never a port Den itself reserves (7778, 7791, 7799), writes it to the new
|
|
47
|
+
distribution's `config/local/user.yml` as `web: { port: N }`, and says so.
|
|
48
|
+
`--port <n>` sets it outright. The check only connects and binds; it sends
|
|
49
|
+
nothing to whatever holds the port. `DEN_WEB_PORT_START` moves the starting
|
|
50
|
+
port (tests use it so none touches 7777).
|
|
36
51
|
[`examples/distribution/`](../examples/distribution) is exactly the output of
|
|
37
52
|
`den create acme`; a unit test regenerates it, so the two never drift
|
|
38
53
|
(`npm run example:regen` after changing the scaffolder).
|
|
@@ -47,6 +62,85 @@ npm install
|
|
|
47
62
|
make help
|
|
48
63
|
```
|
|
49
64
|
|
|
65
|
+
## Set up by your agent
|
|
66
|
+
|
|
67
|
+
A developer pastes one line into their agent:
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
Set up Den for me by following https://raw.githubusercontent.com/Monster-Orange/den/main/skills/install-den/SKILL.md
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
That file is the install-den skill, also in the package at
|
|
74
|
+
`skills/install-den/SKILL.md`: plain Markdown any agent can follow, with
|
|
75
|
+
Claude skill front matter. `den --help` prints the line. The skill runs the
|
|
76
|
+
commands below. Each one takes `--json`, never prompts, and is safe to
|
|
77
|
+
re-run. With `--json`, stdout carries exactly one JSON object and the
|
|
78
|
+
command's log goes to stderr. When `ok` is false the object says why
|
|
79
|
+
(`error`) and carries either a command the agent can run (`fix`) or a
|
|
80
|
+
question for the person (`ask`, sometimes with `choices`).
|
|
81
|
+
|
|
82
|
+
| Step | Answers |
|
|
83
|
+
| ---------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
84
|
+
| `npx @monster-orange/den create <name> [dir] --yes --json` | `{ ok, dir, cli, existed, files, port, portPicked?, next }` |
|
|
85
|
+
| `<cli> repo add <path\|git-url> --json` | `{ ok, existed, repo, workspace, mode, path, manifest, services, next }` |
|
|
86
|
+
| `<cli> up <repo> --json` | `{ ok, workspace, services: [{ address, result, health, detail? }], next }` |
|
|
87
|
+
| `<cli> web up --no-open --json` | `{ ok, url, port, version? }` |
|
|
88
|
+
| `<cli> doctor --json` | `{ ok, checks: [{ id, status, detail, url?, port?, fix?, ask? }] }` |
|
|
89
|
+
| `<cli> mcp connect --runtime <runtime> --json` | `{ ok, server, clients: [{ id, ok, said, file?, changed? }], restart, web }` |
|
|
90
|
+
|
|
91
|
+
### repo add
|
|
92
|
+
|
|
93
|
+
`<cli> repo add <path|git-url> [--sidecar] [-w <workspace>] [--name <repo>]
|
|
94
|
+
[--run "<cmd>"] [--down "<cmd>"] [--health <url>] [--branch <branch>]
|
|
95
|
+
[--json]` adds a repo to a workspace without writing into the repo.
|
|
96
|
+
|
|
97
|
+
- A path is the person's own checkout, added dev-managed at that path. A git
|
|
98
|
+
URL is cloned where Den keeps its managed clones
|
|
99
|
+
(`.<name>-services/<repo>/<branch>`, the remote's default branch unless
|
|
100
|
+
`--branch` says otherwise) and added managed. A clone that is already
|
|
101
|
+
there is used as it is.
|
|
102
|
+
- A repo with its own manifest is used as it is. One without, or with
|
|
103
|
+
`--sidecar`, gets a side-car: a held manifest at
|
|
104
|
+
`manifests/<repo>.<name>.yml` ([below](#overriding-a-manifest)), keyed by
|
|
105
|
+
the repo's `origin` name or folder name. It is checked by the manifest
|
|
106
|
+
validator before it is written, and an existing one is never overwritten.
|
|
107
|
+
`--sidecar` on a repo with its own manifest is an error: Den always uses
|
|
108
|
+
the repo's own file (change it with an override instead).
|
|
109
|
+
- The side-car's commands are detected from the repo's files, in this order:
|
|
110
|
+
a Makefile with an `up` target (`make up`, plus `down`, `setup` and
|
|
111
|
+
`restart` when present); otherwise a compose file
|
|
112
|
+
(`docker compose -f <file> up -d` and `down`) or `package.json`'s `dev`,
|
|
113
|
+
else `start`, script. One candidate decides. Two, or none, is a question:
|
|
114
|
+
the answer has `ask` and `choices`, nothing is written, and a re-run names
|
|
115
|
+
the command with `--run "<cmd>"`. A Makefile `test` target or a
|
|
116
|
+
`package.json` `test` script becomes the repo's `tests.unit`.
|
|
117
|
+
`--health <url>` adds an HTTP health check.
|
|
118
|
+
- The workspace is `-w <workspace>` (made and activated if it doesn't exist),
|
|
119
|
+
else the focused one, else a new `default` workspace.
|
|
120
|
+
- The same repo again answers `ok` with `existed: true`. A different repo
|
|
121
|
+
under the same name is an error that offers `relocate` or `--name`.
|
|
122
|
+
|
|
123
|
+
### up, doctor and mcp connect
|
|
124
|
+
|
|
125
|
+
- `up --json` runs `up` as usual, then reports each service it touched:
|
|
126
|
+
`result` is `started`, `already-running`, `failed` or `skipped`, and
|
|
127
|
+
`health` is the service's health after it started (a service with a
|
|
128
|
+
health check is waited on, up to 30 seconds). A healthy service is
|
|
129
|
+
`already-running` on a re-run.
|
|
130
|
+
- `doctor --json` lists the onboarding checks first: `node` (22.13 or
|
|
131
|
+
later), `git`, `docker` (only when a registered repo has a compose file or
|
|
132
|
+
block, or a docker command), `web` (the web server answers at `url`) and
|
|
133
|
+
`mcp` (`<cli> mcp --call workspaces` gets an answer through it). Then one
|
|
134
|
+
`strategy:<kind>` check per row of the [human doctor](#doctor). `ok` is
|
|
135
|
+
false, and the exit code 1, when any check is `fail`.
|
|
136
|
+
- `mcp connect --runtime <runtime>` names the agent by its runtime: `claude`
|
|
137
|
+
(the `claude-code` client), `codex`, `gemini` or `grok`. Grok CLI keeps its
|
|
138
|
+
MCP servers in `~/.grok/config.toml` (or `.grok/config.toml` with
|
|
139
|
+
`--project`), one `[mcp_servers.<name>]` table each
|
|
140
|
+
([xAI's docs](https://docs.x.ai/build/features/mcp-servers)); Den writes
|
|
141
|
+
only its own table. A config file that already has the entry is left as
|
|
142
|
+
it is (`changed: false`).
|
|
143
|
+
|
|
50
144
|
## Layout
|
|
51
145
|
|
|
52
146
|
| Path | What |
|
|
@@ -379,9 +473,23 @@ a ref that is not set is **not configured**, and its feature says so
|
|
|
379
473
|
("not configured: set secrets.provider and secrets.refs.sudo in
|
|
380
474
|
config/acme.yml") instead of running a CLI. Typing the password always
|
|
381
475
|
works; only the saved password needs `refs.sudo`.
|
|
476
|
+
|
|
477
|
+
**Saving writes `refs.sudo` for you** (LK-4, 0.4.5). When `secrets.provider`
|
|
478
|
+
is `1password` or `doppler`, its CLI is available, and no `refs.sudo` is
|
|
479
|
+
set, the admin password prompt (shown when a command is about to run sudo)
|
|
480
|
+
still offers "Save to 1Password". Saving creates the item
|
|
481
|
+
(1Password: a Password item "<Product> admin password", field `password`,
|
|
482
|
+
in your default vault; Doppler: the key `<PREFIX>_LOCAL_ADMIN_PWD`) and
|
|
483
|
+
then writes that ref to `config/local/user.yml`, because the password is
|
|
484
|
+
the person's, not the distribution's. From then on it survives restarts.
|
|
485
|
+
A failed save writes nothing, and its error never carries the password.
|
|
486
|
+
`env` keeps nothing past the process, so it offers no save. A `refs.sudo`
|
|
487
|
+
in the distribution's config always wins over the one in user.yml.
|
|
488
|
+
|
|
382
489
|
`/api/sudo-password/vault-status` reports `backend`, `label`,
|
|
383
|
-
`configured`, `
|
|
384
|
-
|
|
490
|
+
`configured`, `canSave` (saving works with no YAML to edit),
|
|
491
|
+
`backendAvailable`, `reason` and `hasStored` (`opAvailable` is a
|
|
492
|
+
deprecated alias of `backendAvailable`, kept for one minor).
|
|
385
493
|
|
|
386
494
|
The admin password is for sudo, and nothing else (LK-0). It is no sign-in
|
|
387
495
|
and no access check: starting, stopping and resuming agents, sessions and
|
|
@@ -572,6 +680,10 @@ Acme doctor: strategies
|
|
|
572
680
|
`session-backend` is never a doctor failure: nothing needs a session until
|
|
573
681
|
one is started (see Sessions).
|
|
574
682
|
|
|
683
|
+
`<cli> doctor --json` adds the onboarding checks (Node, git, Docker, the web
|
|
684
|
+
server, Den's MCP) and answers with one JSON object: see
|
|
685
|
+
[Set up by your agent](#up-doctor-and-mcp-connect).
|
|
686
|
+
|
|
575
687
|
## den.yml: the product profile
|
|
576
688
|
|
|
577
689
|
Every runtime name derives from `product.name`. Set only what you want to
|
|
@@ -1209,11 +1321,13 @@ are the same directory.
|
|
|
1209
1321
|
|
|
1210
1322
|
On localhost nothing asks you to sign in: every page, the agents and the
|
|
1211
1323
|
terminal work as they are, and the admin password (R8) is the only prompt,
|
|
1212
|
-
asked only when a command Den runs needs sudo.
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1324
|
+
asked only when a command Den runs needs sudo. Through the edge, a browser
|
|
1325
|
+
the edge shows is on this machine asks for nothing either. Signing in is
|
|
1326
|
+
for everything else: the edge for a client it can't place here (0.4.5), a
|
|
1327
|
+
LAN address, or a bind that isn't loopback (`DEN_BIND_HOST`). From another
|
|
1328
|
+
device, sign in with GitHub as the Den's owner, approve the computer with
|
|
1329
|
+
the code its page shows, or open a one-time link (below, and
|
|
1330
|
+
[Signing in with GitHub](#signing-in-with-github)).
|
|
1217
1331
|
|
|
1218
1332
|
There is no password. Reading `web.secret` (32 random bytes, mode 0600 in
|
|
1219
1333
|
the state dir, made at the first `web up`) proves you are this computer's
|
|
@@ -1221,7 +1335,10 @@ user, and the CLI does that for you:
|
|
|
1221
1335
|
|
|
1222
1336
|
```bash
|
|
1223
1337
|
./bin/acme web open # start if needed, and sign this browser in (a new tab)
|
|
1224
|
-
./bin/acme web login #
|
|
1338
|
+
./bin/acme web login # a one-time sign-in link and its code; waits until it's used
|
|
1339
|
+
./bin/acme web login --no-wait # the link alone (asks for no code), and return
|
|
1340
|
+
./bin/acme web approve ABC-123 # approve the computer showing this code
|
|
1341
|
+
./bin/acme web approve --list # the computers waiting with a code
|
|
1225
1342
|
./bin/acme web sessions # the signed-in browsers: id, created, last seen, host, user agent
|
|
1226
1343
|
./bin/acme web logout <id> # sign out one browser (an id prefix from `web sessions`)
|
|
1227
1344
|
./bin/acme web logout # sign out every browser
|
|
@@ -1238,7 +1355,19 @@ user, and the CLI does that for you:
|
|
|
1238
1355
|
among them; its URL comes from the edge config, and a name the edge
|
|
1239
1356
|
doesn't route here is refused, saying what to add), and
|
|
1240
1357
|
`web login --origin http://<lan name>:<port>` for any other allowed name
|
|
1241
|
-
(the signed-out page there shows the exact command).
|
|
1358
|
+
(the signed-out page there shows the exact command, under "Other ways").
|
|
1359
|
+
- **`web login` waits** (0.4.5). Besides the link it prints an
|
|
1360
|
+
8-character code ("It asks for this code: ABCD-EFGH") and blocks until
|
|
1361
|
+
the link is used, then says who: "Signed in: Safari on macOS
|
|
1362
|
+
(100.64.0.7) at 10:42". Nobody within the minute: "Expired: nobody used
|
|
1363
|
+
the link", exit 1. The link's page asks for the code before it signs in,
|
|
1364
|
+
so the link alone is useless; once opened it waits 5 minutes for the
|
|
1365
|
+
code. A wrong code is refused; five burn the link ("Refused: too many
|
|
1366
|
+
wrong codes", exit 1), more than 20 wrong a minute (any link) are slowed,
|
|
1367
|
+
and the link still works once. The CLI asks the running server through
|
|
1368
|
+
its bearer and never logs the code. `--no-wait` prints the link alone and
|
|
1369
|
+
returns, as before (no code); with no server running `web login` does
|
|
1370
|
+
that too, and says so.
|
|
1242
1371
|
- **The cookie** (`den_session_<port>`) is HttpOnly, SameSite=Strict and
|
|
1243
1372
|
Path=/, and Secure when the request came over HTTPS through the edge. The
|
|
1244
1373
|
session lives server-side (`web-sessions.json`, 0600, in the state dir)
|
|
@@ -1256,36 +1385,111 @@ user, and the CLI does that for you:
|
|
|
1256
1385
|
The Host allowlist, the Origin check and the CSRF token are what stop
|
|
1257
1386
|
another site or a rebinding DNS name. From another machine, every page
|
|
1258
1387
|
and call needs a signed-in browser (or a bearer). A page there shows
|
|
1259
|
-
"Sign in to
|
|
1388
|
+
"Sign in to <name>" and the ways to sign in (below); it never asks for a
|
|
1260
1389
|
secret. `/api` answers 401 `signin_required`.
|
|
1261
|
-
- **The
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
`
|
|
1390
|
+
- **The signed-out page says where to act.** It names the machine Den runs
|
|
1391
|
+
on ("You're opening Den from another computer. Den runs on
|
|
1392
|
+
**studio**, so it needs to know this is you."), then:
|
|
1393
|
+
- **Sign in with GitHub**, only when a client id is set (below);
|
|
1394
|
+
- **Approve this computer**: a code (`XXX-XXX`) and the command that
|
|
1395
|
+
approves it, to run on that machine (`<cli> web approve <code>`), or
|
|
1396
|
+
Settings › Access › Waiting devices there. The page signs itself in
|
|
1397
|
+
when it's approved and goes to the page you asked for; it counts down
|
|
1398
|
+
the 10 minutes, and an expired code is a click away from a new one.
|
|
1399
|
+
From a computer with SSH access to it, the page gives the one-liner
|
|
1400
|
+
(`ssh <host> '<cli> web approve <code>'`) and, when the host is a name,
|
|
1401
|
+
an Open Terminal (`ssh://`) link;
|
|
1402
|
+
- **Other ways**, collapsed: the one-time link with
|
|
1403
|
+
`web login --origin`.
|
|
1404
|
+
|
|
1405
|
+
Every command the page prints names the machine it runs on and has a
|
|
1406
|
+
copy button. It is `<cli>` when the first `<cli>` on that machine's PATH
|
|
1407
|
+
is this distribution's, else the absolute path of its bin.
|
|
1408
|
+
|
|
1409
|
+
- **The machine's name** on the page is `web.machine_name` from
|
|
1410
|
+
`config/local/user.yml` (Settings › Access › Machine name), else the short
|
|
1411
|
+
host name (up to the first dot). It is never taken from the browser's
|
|
1412
|
+
Host. The ssh one-liner's target is the Host the browser used, without
|
|
1413
|
+
the port (the machine's host name if it had none).
|
|
1414
|
+
- **Codes.** About 30 bits (Crockford base32: no I, L, O or U; typed in any
|
|
1415
|
+
case, the dash optional); the page polls with a separate 128-bit id. A
|
|
1416
|
+
code expires after 10 minutes and works once; at most five wait at once
|
|
1417
|
+
(a sixth drops the oldest). They live in the server's memory only.
|
|
1418
|
+
Approving is rate-limited (10 a minute) and recorded in the activity log
|
|
1419
|
+
with who approved, never the code. The new session is "approved with a
|
|
1420
|
+
code". **Deny** (Settings › Access, or `web deny` through the API) tells
|
|
1421
|
+
the page it was refused.
|
|
1422
|
+
- **Seeing that you're signed in.** The chip at the foot of the rail says
|
|
1423
|
+
"Local" (this machine), "Signed in · this device" (a code or a one-time
|
|
1424
|
+
link) or "@<login>" (GitHub), and opens Settings › Access. The first page
|
|
1425
|
+
after a sign-in says "This device is signed in to Den on <machine>",
|
|
1426
|
+
once. Linked devices lists every session (GitHub, a code, a one-time
|
|
1427
|
+
link): how it signed in, when, from where (the address and the browser
|
|
1428
|
+
and OS), last seen, and **Revoke**; the one you're using says "This
|
|
1429
|
+
device".
|
|
1430
|
+
- **The edge counts as local only when it shows the client is.** A
|
|
1431
|
+
request for the edge hostname from the edge (an edge peer, below) is
|
|
1432
|
+
treated as loopback (no sign-in on any route, the agents and the
|
|
1433
|
+
terminal included) when the client the edge reports is this machine.
|
|
1434
|
+
Host, Origin, CSRF and the agent-token rules still apply. Who the client
|
|
1435
|
+
is comes from the right-most `X-Forwarded-For` entry, the one the edge
|
|
1436
|
+
itself appended (anything left of it came from the client, and is never
|
|
1437
|
+
believed):
|
|
1266
1438
|
- loopback, or one of this machine's own interface addresses (a LAN
|
|
1267
|
-
address, or a VPN's such as a Tailscale `100.x`): local;
|
|
1439
|
+
address, or a VPN's such as a Tailscale `100.x`): local, no sign-in;
|
|
1268
1440
|
- missing, or a container/VM network's address (the Docker ranges, or a
|
|
1269
1441
|
subnet of one of this machine's Docker/OrbStack/VM bridges): Den can't
|
|
1270
1442
|
tell (Docker Desktop and OrbStack on macOS hand the edge their
|
|
1271
1443
|
gateway's address, and a TCP passthrough in front of the edge hides the
|
|
1272
|
-
client), so
|
|
1273
|
-
- any other address: another machine, which
|
|
1274
|
-
|
|
1444
|
+
client), so it signs in: `web.edge_unattested`, below;
|
|
1445
|
+
- any other address: another machine, which signs in.
|
|
1446
|
+
|
|
1447
|
+
`web.edge_unattested: local` in `config/local/user.yml` counts a client
|
|
1448
|
+
Den can't place as local again (0.4.4's behaviour). It is read only from
|
|
1449
|
+
that file (a choice for that machine); the distribution config's key is
|
|
1450
|
+
ignored with a warning. The default is `signin`. A request straight to
|
|
1451
|
+
the server's port on localhost (loopback Host and peer) never signs in.
|
|
1275
1452
|
With `DEN_BIND_HOST` beyond loopback (a deliberate exposure, other than
|
|
1276
|
-
the Linux edge's Docker bridge address) the edge
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1453
|
+
the Linux edge's Docker bridge address) the edge always signs in.
|
|
1454
|
+
|
|
1455
|
+
- **The edge believes forwarded headers only from its own networks.** Den
|
|
1456
|
+
renders each entrypoint with `forwardedHeaders.trustedIPs` set to
|
|
1457
|
+
loopback and the Docker ranges (`127.0.0.1/32`, `::1/128`,
|
|
1458
|
+
`172.16.0.0/12`, `192.168.65.0/24`), never `forwardedHeaders.insecure`,
|
|
1459
|
+
so a client can't hand the edge an `X-Forwarded-For` of its own. An
|
|
1460
|
+
`edge.args` entry that sets `forwardedHeaders` or `proxyProtocol` is
|
|
1461
|
+
dropped, and `edge up` says so.
|
|
1462
|
+
- **A TCP passthrough in front of the edge** (a gateway that publishes
|
|
1463
|
+
`:443` and passes TLS through) hides the client: the edge sees the
|
|
1464
|
+
gateway's address. Have the gateway send the PROXY protocol, and list it
|
|
1465
|
+
in `edge.proxy_protocol.trusted_ips` (empty by default; IP addresses or
|
|
1466
|
+
CIDRs; `config/local/user.yml`'s list replaces the team's):
|
|
1467
|
+
|
|
1468
|
+
```yaml
|
|
1469
|
+
edge:
|
|
1470
|
+
proxy_protocol:
|
|
1471
|
+
trusted_ips: [172.18.0.5]
|
|
1472
|
+
```
|
|
1473
|
+
|
|
1474
|
+
Every entrypoint then takes the PROXY protocol from those addresses, and
|
|
1475
|
+
the client it carries is what Den sees. Configuring the gateway is the
|
|
1476
|
+
gateway's job.
|
|
1477
|
+
|
|
1478
|
+
- **`edge up` recreates the edge when its command changed.** It compares
|
|
1479
|
+
the running edge's command with the one it renders and says why ("acme-edge's
|
|
1480
|
+
settings changed (forwarded-header trust): recreating it once. Routes
|
|
1481
|
+
drop only while it restarts."). Upgrading to 0.4.5 does this once.
|
|
1280
1482
|
|
|
1281
1483
|
- **"Your edge is reachable from other machines".** When Den can tell, at
|
|
1282
1484
|
start, that the edge is reachable beyond this machine (an edge hostname
|
|
1283
1485
|
that resolves to an address that isn't loopback, such as a LAN or
|
|
1284
1486
|
Tailscale address, or the edge container publishing a port on every
|
|
1285
1487
|
interface, from `docker inspect`), it writes "Your edge is reachable from
|
|
1286
|
-
other machines
|
|
1287
|
-
|
|
1288
|
-
per browser).
|
|
1488
|
+
other machines. A browser it can't place as this one signs in first
|
|
1489
|
+
(Sign in with GitHub, or a pairing link)." once to `web.log`, and every
|
|
1490
|
+
page shows it as a banner (dismissed per browser). With
|
|
1491
|
+
`web.edge_unattested: local` it says instead that anyone it can't place
|
|
1492
|
+
can use terminals, and to remove the key to have them sign in.
|
|
1289
1493
|
- **localhost is the canonical name.** A cookie set on `localhost` isn't
|
|
1290
1494
|
sent to `127.0.0.1`, so the links Den prints use `localhost`.
|
|
1291
1495
|
- **Browsers.** Chromium, Firefox and Safari 16.4 or later: a sign-in link
|
|
@@ -1319,6 +1523,76 @@ logout --all` the old token is refused; the CLI reads the new one.
|
|
|
1319
1523
|
421 `misdirected`, so a LAN client can't send the edge's name straight to
|
|
1320
1524
|
the server's port.
|
|
1321
1525
|
|
|
1526
|
+
## Signing in with GitHub
|
|
1527
|
+
|
|
1528
|
+
A Den has one owner: a GitHub account. Once it is linked, that account
|
|
1529
|
+
signs in from any device (a phone, another laptop, a browser through the
|
|
1530
|
+
edge) with **Sign in with GitHub** on the signed-out page. Den stores only
|
|
1531
|
+
the account's id and login, never a token, and no other account gets in.
|
|
1532
|
+
|
|
1533
|
+
```bash
|
|
1534
|
+
./bin/acme web owner link # link the owner: GitHub's device flow, in the terminal
|
|
1535
|
+
./bin/acme web owner # show who it is (also: web owner show, --json)
|
|
1536
|
+
./bin/acme web owner unlink # forget the owner (signed-in browsers stay signed in)
|
|
1537
|
+
```
|
|
1538
|
+
|
|
1539
|
+
- **`web owner link`** prints `https://github.com/login/device` and a code.
|
|
1540
|
+
Enter the code there and approve; Den asks GitHub who approved, stores
|
|
1541
|
+
`owner: github:<id>` and the login in `web-owner.json` (0600, in the
|
|
1542
|
+
state dir) and throws the token away. A second link is refused until
|
|
1543
|
+
`web owner unlink`. Settings › Access does the same in the browser
|
|
1544
|
+
(Link your GitHub account, Unlink): shell-grade actions, for a signed-in
|
|
1545
|
+
browser and never an agent's token.
|
|
1546
|
+
- **Sign in with GitHub** on the signed-out page shows a code and
|
|
1547
|
+
`github.com/login/device`. The server runs the flow, and when the owner
|
|
1548
|
+
approves it sets the same never-expiring session cookie a one-time link
|
|
1549
|
+
sets, and the page reloads signed in. Another account sees "@<login>
|
|
1550
|
+
isn't allowed on this Den". The token is thrown away either way. At most
|
|
1551
|
+
three sign-ins wait for GitHub at once; a code expires as GitHub says
|
|
1552
|
+
(15 minutes). With no owner linked the page says to run
|
|
1553
|
+
`web owner link`.
|
|
1554
|
+
- **Settings › Access** lists the linked devices (the browser, how it
|
|
1555
|
+
signed in, the client address when the edge vouched for one, created and
|
|
1556
|
+
last seen; "This device" on yours), each with **Revoke**, which ends its session at once (its
|
|
1557
|
+
open terminals included). **Pair a device** makes a one-time link and a
|
|
1558
|
+
QR code for an edge hostname you pick (`web login --host <name>` in the
|
|
1559
|
+
browser): scan it on the other device within 60 seconds.
|
|
1560
|
+
- **`web.github_client_id`** is the GitHub App's client id (public; no
|
|
1561
|
+
secret). The default is Den's own App, **Monster Orange Den**
|
|
1562
|
+
(`Iv23liKL12gnN6WvRV5b`), so Sign in with GitHub works out of the box. A
|
|
1563
|
+
distribution that ships its own App sets its client id here (below);
|
|
1564
|
+
`config/local/user.yml`'s wins over the team's. An empty value
|
|
1565
|
+
(`github_client_id: ""`) turns Sign in with GitHub off: the page then
|
|
1566
|
+
offers the code and the one-time link only.
|
|
1567
|
+
- **Expiring tokens.** If the App hands out expiring tokens (GitHub's
|
|
1568
|
+
answer carries `expires_in` or a `refresh_token`), sign-in still
|
|
1569
|
+
completes, and Den says once, in the log and on stderr, to switch off
|
|
1570
|
+
token expiry in the App's settings ("Expire user authorization tokens");
|
|
1571
|
+
it never prints the token. `GET /api/web/owner` reports it as
|
|
1572
|
+
`tokenExpiry: true`.
|
|
1573
|
+
|
|
1574
|
+
### The GitHub App (once, by whoever ships the Den)
|
|
1575
|
+
|
|
1576
|
+
Den's default App is Monster Orange Den. A distribution that wants its own
|
|
1577
|
+
(its name on GitHub's approval page) needs a GitHub App with the device
|
|
1578
|
+
flow on. Create it once (organization or account › Settings › Developer settings › GitHub
|
|
1579
|
+
Apps › New GitHub App):
|
|
1580
|
+
|
|
1581
|
+
1. No callback URL; untick "Request user authorization during
|
|
1582
|
+
installation". Webhook off.
|
|
1583
|
+
2. **Enable Device Flow**: ticked. The device flow needs only the client
|
|
1584
|
+
id.
|
|
1585
|
+
3. **Expire user authorization tokens**: off. Den never asks for
|
|
1586
|
+
`offline_access`, and keeps no token either way.
|
|
1587
|
+
4. Account permissions: none. Den reads only `GET /user` (the id and the
|
|
1588
|
+
login), which needs none. Repository permissions are only for features
|
|
1589
|
+
that act on a repository (Den's default App has Issues for feedback).
|
|
1590
|
+
5. **Where can this GitHub App be installed**: Any account. GitHub lets only
|
|
1591
|
+
members of the owning organization authorize a private App, so anyone
|
|
1592
|
+
else couldn't sign in.
|
|
1593
|
+
6. Put the **Client ID** in `web.github_client_id`. Never generate or
|
|
1594
|
+
share a client secret or a private key for Den: it never uses one.
|
|
1595
|
+
|
|
1322
1596
|
## Running the web UI as a service (macOS)
|
|
1323
1597
|
|
|
1324
1598
|
Den ships a launchd installer that runs the web UI at login and restarts it if
|
|
@@ -2129,6 +2403,26 @@ with both workspaces named. Any key of `agents:` other than `max_concurrent`
|
|
|
2129
2403
|
and `persona_packs` is an agent id, and its only key is `grants`. A grant
|
|
2130
2404
|
naming a workspace that doesn't exist stops `start`, naming the key.
|
|
2131
2405
|
|
|
2406
|
+
## Dev metrics: local unless you opt in
|
|
2407
|
+
|
|
2408
|
+
Den records each command it runs (its name, surface, duration and exit
|
|
2409
|
+
code, never arguments or values) as one JSON line in
|
|
2410
|
+
`.dev-metrics/events.jsonl` under the distribution root. That file stays
|
|
2411
|
+
on the machine.
|
|
2412
|
+
|
|
2413
|
+
Nothing is sent anywhere unless the engineer opts in, per machine:
|
|
2414
|
+
|
|
2415
|
+
```yaml
|
|
2416
|
+
# config/local/user.yml (gitignored)
|
|
2417
|
+
metrics:
|
|
2418
|
+
share: true
|
|
2419
|
+
```
|
|
2420
|
+
|
|
2421
|
+
Only the boolean `true` counts. With it, and a Sentry DSN in
|
|
2422
|
+
`DEV_METRICS_SENTRY_DSN` (or a synced `dev-metrics.config`), each line is
|
|
2423
|
+
also sent, best effort. A DSN on its own never sends. `CI=true` and
|
|
2424
|
+
`DEV_METRICS_DISABLED=1` stop sending even when opted in.
|
|
2425
|
+
|
|
2132
2426
|
## Pinning and upgrading
|
|
2133
2427
|
|
|
2134
2428
|
`package.json` pins an exact Den version (`create` writes the version that
|
package/docs/reference.md
CHANGED
|
@@ -110,9 +110,14 @@ That is `examples/demo/demo-api/.den.yml`; the other demo repos are in
|
|
|
110
110
|
workspace's checkout, with an MCP server pinned to that workspace and a
|
|
111
111
|
token of its own. The web UI's Agents tab lists them and opens their
|
|
112
112
|
terminals.
|
|
113
|
-
- **Browser sign-in beyond loopback.** On localhost no route needs a sign-in
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
- **Browser sign-in beyond loopback.** On localhost no route needs a sign-in,
|
|
114
|
+
nor through the edge when it shows the browser is on this machine.
|
|
115
|
+
Anywhere else, every route needs one: Sign in with GitHub as the Den's
|
|
116
|
+
owner (`<cli> web owner link` links the account once), approving the
|
|
117
|
+
computer with the code its page shows (`<cli> web approve <code>`, or
|
|
118
|
+
Settings › Access › Waiting devices), or a one-time link (`<cli> web
|
|
119
|
+
login`, which waits until it's used; `web open` opens it; Settings ›
|
|
120
|
+
Access pairs a device with a QR code). The rail's chip shows who you are.
|
|
116
121
|
- **Web UI and CLI** over the same state.
|
|
117
122
|
|
|
118
123
|
## Strategies
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monster-orange/den",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Den: a local multi-repo dev orchestrator, HTTPS edge and E2E harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"README.md",
|
|
26
26
|
"CHANGELOG.md",
|
|
27
27
|
"docs/",
|
|
28
|
-
"!docs/images/"
|
|
28
|
+
"!docs/images/",
|
|
29
|
+
"skills/"
|
|
29
30
|
],
|
|
30
31
|
"scripts": {
|
|
31
32
|
"cli": "tsx src/ui/cli/index.ts menu",
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
"gate:org-names": "node scripts/org-name-gate.mjs",
|
|
45
46
|
"docs:shots": "node scripts/doc-shots.mjs",
|
|
46
47
|
"docs:links": "node scripts/docs-links.js",
|
|
47
|
-
"example:regen": "rm -rf examples/distribution && ./bin/den create acme examples/distribution",
|
|
48
|
+
"example:regen": "rm -rf examples/distribution && ./bin/den create acme examples/distribution --port 7777",
|
|
48
49
|
"dist-smoke": "bash scripts/dist-smoke.sh"
|
|
49
50
|
},
|
|
50
51
|
"keywords": [
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: install-den
|
|
3
|
+
description: Install and set up Den, Monster Orange's local dev orchestrator, for a developer in five minutes. Use when the person says "set up Den", "install Den", "get me started with Den", or pastes a link to this file.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Install Den
|
|
7
|
+
|
|
8
|
+
You are setting up Den on this person's machine. Den runs their repos locally, shows them in a browser, and gives you
|
|
9
|
+
(the agent) an MCP server to work with. Aim to be done in five minutes. Keep your messages short and plain.
|
|
10
|
+
|
|
11
|
+
## Hard rules
|
|
12
|
+
|
|
13
|
+
- **Never ask for a token, password or secret in chat.** If a step needs one, tell the person which command to run
|
|
14
|
+
themselves.
|
|
15
|
+
- **Never edit the person's repos.** Den's files go in the Den folder only. Read the repo; don't write to it.
|
|
16
|
+
- **Send nothing off this machine.** No uploads, no telemetry, no pasting their code anywhere. The only downloads are
|
|
17
|
+
Den from npm and a repo the person gave you as a git URL.
|
|
18
|
+
- **Sudo is the person's job.** Never run `sudo` yourself. When a Den step needs it, Den asks for the password in its
|
|
19
|
+
own prompt (in the terminal or the browser), and the person types it there.
|
|
20
|
+
- Every step below is safe to re-run. If you are unsure whether a step ran, run it again.
|
|
21
|
+
|
|
22
|
+
## 1. Check the prerequisites
|
|
23
|
+
|
|
24
|
+
Run these and read the results. Don't install anything without saying so first.
|
|
25
|
+
|
|
26
|
+
1. `node --version`: needs **22.13 or later**. If it's older or missing, say so and suggest their usual installer
|
|
27
|
+
(for example `brew install node`, or `nvm install 22`).
|
|
28
|
+
2. `npm --version`: needs 9 or later. It comes with Node.
|
|
29
|
+
3. `git --version`: needs to be there.
|
|
30
|
+
4. `docker info`: only matters if the repo runs in containers (it has a `docker-compose.yml` or `compose.yaml`).
|
|
31
|
+
If Docker is needed and not running, ask them to start it.
|
|
32
|
+
|
|
33
|
+
If anything is missing, stop here, tell them what and how to fix it, and wait.
|
|
34
|
+
|
|
35
|
+
## 2. Ask at most three questions
|
|
36
|
+
|
|
37
|
+
Ask them together, each with a default, so the person can just say "yes":
|
|
38
|
+
|
|
39
|
+
1. **Where should your Den live?** Default: `~/den`. It must be a new or empty folder.
|
|
40
|
+
2. **Which repo should we start with?** Default: the folder you're in now. A local path or a git URL.
|
|
41
|
+
3. **What should we call your Den?** Default: `den`. Lowercase letters, numbers and dashes. This also becomes the
|
|
42
|
+
command name, so `acme` gives you `./bin/acme`.
|
|
43
|
+
|
|
44
|
+
Below, `<dir>` is the Den folder, `<repo>` the repo path, and `<name>` the name.
|
|
45
|
+
|
|
46
|
+
## 3. Create the Den
|
|
47
|
+
|
|
48
|
+
Every Den command below takes `--json`. It then prints one JSON object on stdout and its log on stderr. Read `ok`
|
|
49
|
+
first. When `ok` is false, the object says why (`error`) and either a command to run (`fix`) or a question for the
|
|
50
|
+
person (`ask`, sometimes with `choices`).
|
|
51
|
+
|
|
52
|
+
1. `npx @monster-orange/den create <name> <dir> --yes --json`. It answers `{ok, dir, cli, existed, port, next}`. If
|
|
53
|
+
the folder already holds this Den, it answers `ok` with `existed: true` and changes nothing. `port` is the browser
|
|
54
|
+
UI's port. When another Den already uses the usual one on this machine, Den picks the next free port and says so
|
|
55
|
+
(`portPicked`). Remember the port for the hand-back.
|
|
56
|
+
2. `cd <dir> && npm install`
|
|
57
|
+
3. From here on, the Den command is `./bin/<name>`, run from `<dir>`.
|
|
58
|
+
|
|
59
|
+
## 4. Add the repo without changing it
|
|
60
|
+
|
|
61
|
+
1. `./bin/<name> repo add <repo> --json`. `<repo>` can be a path or a git URL. Den clones a URL into its own folder.
|
|
62
|
+
- If the repo has its own `.<name>.yml`, Den uses it.
|
|
63
|
+
- If it doesn't, Den works out how the repo starts and writes a side-car manifest into `<dir>/manifests/`. It never
|
|
64
|
+
writes into the repo. It looks for a Makefile `up` target, then a compose file, then the `dev` or `start` script
|
|
65
|
+
in `package.json`. `--sidecar` asks for this explicitly, and fails if the repo has its own manifest.
|
|
66
|
+
- If you can see which port the app listens on, add `--health http://localhost:<port>/`. Den then checks that it
|
|
67
|
+
answers.
|
|
68
|
+
2. If the answer has `ask` and `choices`, Den couldn't tell how the repo starts, and it wrote nothing. Ask the person
|
|
69
|
+
**one** question: the `ask` text, with the choices numbered. Then re-run with their answer:
|
|
70
|
+
`./bin/<name> repo add <repo> --run "<their command>" --json`.
|
|
71
|
+
3. The answer names the repo (`repo`), the workspace it joined (`workspace`) and where its manifest is (`manifest`).
|
|
72
|
+
A workspace is Den's name for a set of repos that run together. The first one is called `default`.
|
|
73
|
+
|
|
74
|
+
## 5. Start it and check it
|
|
75
|
+
|
|
76
|
+
1. `./bin/<name> up <repo> --json`. `<repo>` is the `repo` value from step 4. Each service comes back with a
|
|
77
|
+
`result` (`started` or `already-running`) and a `health` (`ok`, or `unknown` when it has no health check).
|
|
78
|
+
2. `./bin/<name> web up --no-open --json`. This starts the browser UI, and answers `{ok, url, port}`. The MCP server
|
|
79
|
+
reads through it.
|
|
80
|
+
3. `./bin/<name> doctor --json`. Each check comes back as `ok`, `warn` or `fail`. The checks are Node, git, Docker
|
|
81
|
+
(only if a repo needs it), the web server (with its `url` and `port`) and Den's MCP. Fix every `fail` before you hand back.
|
|
82
|
+
|
|
83
|
+
## 6. Connect yourself to Den's MCP
|
|
84
|
+
|
|
85
|
+
1. `./bin/<name> mcp connect --runtime <you> --json`. `<you>` is `claude`, `codex`, `gemini` or `grok`. For any other
|
|
86
|
+
agent, `./bin/<name> mcp config` prints the JSON to add by hand.
|
|
87
|
+
2. `./bin/<name> mcp clients` shows whether you are connected.
|
|
88
|
+
3. If the answer has `restart: true`, you load the new MCP server only when you start again. Tell the person to
|
|
89
|
+
restart you once.
|
|
90
|
+
|
|
91
|
+
## 7. Hand back
|
|
92
|
+
|
|
93
|
+
Tell the person, in a few short sentences. Explain every name you use the first time (for example, "a workspace is a
|
|
94
|
+
set of repos that run together").
|
|
95
|
+
|
|
96
|
+
- **Open:** the `url` from `web up` or from the doctor's `web` check, for example `http://localhost:<port>`. Never
|
|
97
|
+
assume a port: use the one Den gave you.
|
|
98
|
+
- **What's running:** each service from `up`, and whether it's healthy.
|
|
99
|
+
- **Your Den:** where it lives (`<dir>`) and its command (`./bin/<name>`).
|
|
100
|
+
- **Decisions, if any:** as numbered yes/no questions, for example "1. Restart me now so I can use Den's tools?"
|
|
101
|
+
- **Three next things we can do together:**
|
|
102
|
+
1. Add another repo.
|
|
103
|
+
2. Look at a service's logs, or run its tests.
|
|
104
|
+
3. Start an agent session on this repo from the browser.
|
|
105
|
+
|
|
106
|
+
## When something fails
|
|
107
|
+
|
|
108
|
+
1. Read the JSON. If it has `fix`, run that command, then re-run the step that failed.
|
|
109
|
+
2. If it has `ask`, the person has to decide or act (sudo, a password, starting Docker, signing in). Ask them the
|
|
110
|
+
question, show any `choices` numbered, and wait.
|
|
111
|
+
3. If there is neither, ask the person **one** clear question, with your best guess as the default.
|
|
112
|
+
4. Never work around a failure by editing their repo, turning off a check, or skipping a step without saying so.
|
|
113
|
+
5. If you're stuck after two tries, stop. Tell them what you ran, what it said, and where Den writes its logs
|
|
114
|
+
(`~/.<name>/`), so they can get help.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one line a developer pastes into their agent to have it set Den up
|
|
3
|
+
* (ON-5): the agent follows the install-den skill published at this URL
|
|
4
|
+
* (skills/install-den/SKILL.md in the package).
|
|
5
|
+
*/
|
|
6
|
+
export const AGENT_SETUP_URL =
|
|
7
|
+
"https://raw.githubusercontent.com/Monster-Orange/den/main/skills/install-den/SKILL.md";
|
|
8
|
+
|
|
9
|
+
export const AGENT_SETUP_LINE = `Set up Den for me by following ${AGENT_SETUP_URL}`;
|