@monster-orange/den 0.1.0 → 0.2.1

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/README.md +1 -1
  3. package/bin/den +8 -0
  4. package/docs/distributions.md +283 -12
  5. package/package.json +4 -2
  6. package/src/pillars/journeys/sandbox.ts +2 -5
  7. package/src/pillars/security/discover.ts +17 -1
  8. package/src/pillars/security/run.ts +31 -3
  9. package/src/shared/agent-actor.ts +77 -0
  10. package/src/shared/den-config.ts +122 -0
  11. package/src/shared/edge/status.ts +74 -0
  12. package/src/shared/environment-manager/health.ts +54 -8
  13. package/src/shared/environment-manager/load-applications.ts +31 -1
  14. package/src/shared/environment-manager/log-tail.ts +5 -4
  15. package/src/shared/environment-manager/opctl.ts +2 -1
  16. package/src/shared/environment-manager/service-container.ts +47 -0
  17. package/src/shared/environment-manager/service-runner.ts +19 -2
  18. package/src/shared/environment-manager/sync-targets.ts +66 -0
  19. package/src/shared/environment-manager/up-plan.ts +25 -1
  20. package/src/shared/manifest/merge-override.ts +75 -0
  21. package/src/shared/manifest/placement.ts +333 -0
  22. package/src/shared/manifest/read-manifest.ts +156 -38
  23. package/src/shared/manifest/schema.ts +14 -0
  24. package/src/shared/manifest/validate-layers.ts +229 -0
  25. package/src/shared/manifest/validate-manifest.ts +66 -6
  26. package/src/shared/paths.ts +12 -0
  27. package/src/shared/redact.ts +107 -0
  28. package/src/shared/stacks/manifest-commands.ts +30 -0
  29. package/src/shared/stacks/status.ts +99 -0
  30. package/src/shared/types/index.ts +23 -0
  31. package/src/ui/cli/applications-command.ts +34 -2
  32. package/src/ui/cli/edge-command.ts +9 -19
  33. package/src/ui/cli/graph-command.ts +50 -4
  34. package/src/ui/cli/index.ts +184 -55
  35. package/src/ui/cli/mcp-connect.ts +350 -0
  36. package/src/ui/cli/mcp-main.ts +13 -0
  37. package/src/ui/cli/mcp-stdout-guard.ts +10 -0
  38. package/src/ui/cli/named-target.ts +11 -0
  39. package/src/ui/cli/stack-command.ts +13 -40
  40. package/src/ui/cli/test-runner.ts +8 -1
  41. package/src/ui/mcp/api.ts +286 -0
  42. package/src/ui/mcp/connect.ts +304 -0
  43. package/src/ui/mcp/guards.ts +145 -0
  44. package/src/ui/mcp/server.ts +149 -0
  45. package/src/ui/mcp/tools.ts +644 -0
  46. package/src/ui/server/audit.ts +137 -0
  47. package/src/ui/server/changelog-parser.ts +11 -0
  48. package/src/ui/server/graph-prefs.ts +89 -0
  49. package/src/ui/server/health-row.ts +15 -0
  50. package/src/ui/server/index.ts +256 -61
  51. package/src/ui/server/lifecycle-exec.ts +188 -0
  52. package/src/ui/server/log-history.ts +4 -3
  53. package/src/ui/server/public/environments.html +4 -2
  54. package/src/ui/server/public/graph-model.js +416 -0
  55. package/src/ui/server/public/graph.css +980 -0
  56. package/src/ui/server/public/graph.js +986 -0
  57. package/src/ui/server/public/header.js +7 -5
  58. package/src/ui/server/public/local-dev.html +130 -2191
  59. package/src/ui/server/public/logs.html +11 -4
  60. package/src/ui/server/public/panel.js +790 -0
  61. package/src/ui/server/public/services.html +6 -1
  62. package/src/ui/server/readonly-routes.ts +209 -0
  63. package/src/ui/server/ui-assets.ts +73 -0
  64. package/src/ui/server/workspace-routes.ts +39 -8
package/CHANGELOG.md CHANGED
@@ -4,6 +4,210 @@ All notable changes to Den are documented here. Each release lists the
4
4
  config and manifest changes a distribution must make to move to it under
5
5
  **Migrations**.
6
6
 
7
+ ## [0.2.1] - 2026-11-04
8
+
9
+ ### Fixed
10
+
11
+ - **MCP `service_restart` on a service with only `up` and `down`.**
12
+ `/api/service/exec` now runs `up`, `down` and `restart` (env local) through
13
+ the service runner, as `<cli> up|down|restart` do, instead of looking up
14
+ and executing the manifest's command itself. So, for the web UI's panel and
15
+ for MCP alike: `restart` with no declared `restart` is `down` then `up`;
16
+ `up` on a service that is already healthy starts nothing and says
17
+ `already up (healthy)`; `up` attaches to a running shared instance; `down`
18
+ honours workspace ownership. The panel offers Restart whenever a service has
19
+ `up` and `down`.
20
+ - **A detached start says when it is ready.** `/api/service/exec` sends a
21
+ `ready` event once a detached `up` or `restart` is up: its health check
22
+ passes (`checked: true`; for a restart, only after the old instance went
23
+ down or 5s passed), or, with no health check, its launcher is still running
24
+ after 5s (`checked: false`). MCP `service_up` and `service_restart` return
25
+ at `ready` (`"ready": true` in the result) instead of waiting for a
26
+ launcher that runs in the foreground (a dev server) to exit, which ran
27
+ into the 180s timeout and reported a false failure. A restart of a running
28
+ shared instance now says `restarting the running instance`, not
29
+ `already running`, and the log path is printed once. An MCP action's
30
+ `ended` reads `ready` or `handed-off` for a detached start, never
31
+ `stopped`.
32
+ - **MCP actions report failure.** `service_up|down|restart`, `run_tests` and
33
+ `workspace_sync` return `isError` with `"failed": true` in the result when
34
+ the command exits non-zero, a detached service's launcher exits non-zero,
35
+ or the call times out. They used to come back ok with `exitCode: 1` inside.
36
+ - **`<cli> up|down|restart <remote service>` exits 1.** Naming a remote
37
+ service (or its repo) directly is a refused command. A remote service
38
+ reached through a workspace, `-a` or `-A <app>` run is still said and
39
+ skipped, exit 0.
40
+ - **Refusals name the command.** `runs in dev (AWS); nothing to restart here`
41
+ (and `stop`, `start`, `run "<cmd>" on`), not `start` for everything.
42
+
43
+ ### Added
44
+
45
+ - **`<cli> mcp connect`: register the MCP server with your agents.**
46
+ `mcp clients` lists the agents installed here (Claude Code, Codex, Cursor,
47
+ VS Code, Claude Desktop, Windsurf, Gemini CLI) and whether each is
48
+ connected. `mcp connect <client>...|--all` registers the distribution's
49
+ server at user scope through each client's own CLI or config file, and
50
+ `--project [dir]` writes a project's config instead. `--dry-run` shows the
51
+ change, `mcp disconnect` removes it, and `mcp config` prints JSON for any
52
+ other client. The registration pins `PATH` to the running node so GUI
53
+ agents can launch it. See "Connecting your agents" in
54
+ `docs/distributions.md`.
55
+ - **Refused attempts are in the activity trail.** `/api/activity` entries may
56
+ carry `outcome: "refused"` and a `reason`: a refused or unresolvable
57
+ `/api/service/exec`, and any MCP action a guard refused before it reached
58
+ the web server. Agents report the latter to `POST /api/activity` (agent
59
+ audit header required; it appends one sanitised entry and nothing else).
60
+ The panel's Activity tab shows them as refused. An attempt that named only
61
+ the repo shows for the repo and for each of its services.
62
+ - **`target.expect` in placement.** `/api/services` and `/api/health` rows'
63
+ `target` now carry `expect` (as declared) when the manifest's target has its
64
+ own `url`.
65
+
66
+ ### Migrations
67
+
68
+ - None required. Two additive API changes a contract probe may see:
69
+ `target.expect` on `/api/services` and `/api/health` rows, and
70
+ `outcome`/`reason` on `/api/activity` entries.
71
+ - Scripts that relied on `<cli> restart|down <remote service>` exiting 0
72
+ should expect 1.
73
+
74
+ ## [0.2.0] - 2026-11-04
75
+
76
+ ### Added
77
+
78
+ - **Manifest overrides.** An optional `manifests/<repo>.override.yml` in the
79
+ distribution is deep-merged onto the repo's base manifest (its own, or the
80
+ held one): maps merge by key, lists and scalars replace, and an explicit
81
+ `null` deletes a key. An override may only name services and keys the base
82
+ has, and an override with no base is an error. See "Overriding a manifest"
83
+ in `docs/distributions.md`.
84
+ - **Manifest layers are visible.** `/api/services` returns `manifestLayers`
85
+ per service (`["repo"]`, `["held", "override"]`, …) and the Services cards
86
+ show them; `GET /api/manifest/validate?path=<repo dir>` returns each layer's
87
+ result.
88
+ - **`validate` checks each layer.** For a repo folder it validates the base
89
+ and the override separately, each error naming its file, then the merged
90
+ result.
91
+ - **`<cli> mcp`: an MCP server over stdio** for agents, a thin client of the
92
+ running web server (it never starts it, and says how when it is down). Read
93
+ tools are always on, including the new `stacks` and `edge_status`; the
94
+ actions `service_up`, `service_down`, `service_restart`, `workspace_sync`
95
+ and `run_tests` are opt-in per org with `mcp.actions`. Guards: lifecycle
96
+ commands and env=local only, locally run services only, `run_tests` limited
97
+ to `mcp.run_tests.envs` (default `[local]`, never production), security runs
98
+ must pass the owned-host plan and `full` is local only, log tools read
99
+ local sources unless `mcp.logs.remote: true`, and all text returned is
100
+ secret-redacted. No deploy tool. Pinned `@modelcontextprotocol/sdk` 1.32.1.
101
+ See "MCP server" in `docs/distributions.md`.
102
+ - **Agent audit trail.** Every service exec, test run and sync is recorded
103
+ with who asked: an agent's actions carry actor `agent`, the MCP client's
104
+ self-reported name and version, and the MCP pid and session id, and show as
105
+ `[agent]` lines in the web server log on the Logs page.
106
+ - **Read-only status endpoints:** `/api/stacks` (what `stack ls/health` show),
107
+ `/api/edge` (what `edge status` shows) and `/api/security/plan` (the
108
+ security CLI's own dry run). `stack ls` and `edge status` now print from the
109
+ same code.
110
+ - **Graph: cards and a details panel.** The Local Dev graph is laid out by
111
+ ELK (layered, compound org or application › repo › service, orthogonal
112
+ edges; elkjs vendored and served by Den, so it works offline). Cards are
113
+ full size (detailed or compact) on a canvas you pan and zoom; Fit zooms out
114
+ to 35%. A card shows status, name, repo, placement and health, and its main
115
+ actions on hover. Clicking a card or a repo opens a side panel: one primary
116
+ action (Start when down, Open when up, else Logs), two common ones, and More
117
+ (manifest commands, tools, and careful actions that confirm on a second
118
+ click); tabs Overview, Logs (live tail), Config and Activity (the audit
119
+ trail, yours and agents'). ⤢ pops the panel out as a dialog. Layout
120
+ direction, grouping, card density and panel width are remembered per user
121
+ (`/api/settings` `graph`). Find highlights and centres on matches. The
122
+ panel is built from registries (actions, tabs, sections) a distribution or
123
+ later release can add to.
124
+ - `GET /api/activity`: the audit trail, filtered by service or repo.
125
+ - **One secret redactor** (`src/shared/redact.ts`), shared by the journeys and
126
+ security pillars and the MCP server.
127
+ - **Placement (R13): what runs here and what runs in the cloud.** A
128
+ manifest's service may say `runs: { local: remote, dev: remote }` (per env,
129
+ `local` or `remote`) and `target: { env, account?, url?, expect? }`.
130
+ Without `runs`, a service is local when it or any service of its repo has
131
+ local commands (or the repo is a compose stack), remote when it has only an
132
+ HTTP health URL and nothing in its repo runs here, and otherwise has no
133
+ placement. `/api/services`
134
+ and `/api/health` rows carry `runs` and `target`; `/api/applications`
135
+ carries `placements` per service address, and per application `placement`
136
+ (`local | remote | hybrid | none`) and a `summary`
137
+ (`shop-frontend local · shop-core remote (dev)`). `<cli> status`,
138
+ `services` and `apps` suffix remote services `☁ remote (dev)`, and
139
+ `<cli> graph` marks remote and hybrid nodes. See "Placement" in
140
+ `docs/distributions.md`.
141
+ - **Remote health.** A remote service is checked by the health declared for
142
+ its `target.env`; only when there is none does Den GET `target.url`, up when
143
+ the status is one of `target.expect` (a code or a list, 100–599; default
144
+ any 2xx). Its health row's label names the target (`dev · AWS`).
145
+ - **`production_envs`** in the org config: the envs that count as production
146
+ (default `[prod, production]`).
147
+
148
+ ### Changed
149
+
150
+ - **`/api/health` rows: `url` is only ever a URL.** A shell health check's
151
+ command (a compose stack's `<cli> stack health …`, or any declared shell
152
+ check) is now in a new `check` field and `url` is null; the UI shows the
153
+ same tooltips as before. Clients that read the command from `url` should
154
+ read `check`.
155
+ - `/api/server-info` returns Den's `version`.
156
+ - **Nothing runs a command on a remote service.** `/api/service/exec`,
157
+ `<cli> up|down|restart`, workspace up and down, and `<cli> up`'s plan refuse
158
+ it (`runs in dev (AWS); nothing to start here`; a `down` succeeds with
159
+ nothing to stop). The MCP guard `runsLocally()` reads `runs`. Security
160
+ `auto_targets` excludes remote services explicitly, a remote service is
161
+ never scanned as if it were local, and one whose `target.env` counts as
162
+ production is never scanned at all.
163
+ - **Overrides may add placement.** `runs` and `target` are the one thing a
164
+ `manifests/<repo>.override.yml` may add to a service its base has.
165
+ - `validate` checks env names: a `runs` key or `target.env` that is not one of
166
+ the org's `environments` is an error.
167
+ - `<cli> status` keeps a hybrid repo's remote service as its own row instead
168
+ of rolling it into the entrypoint.
169
+
170
+ ### Fixed
171
+
172
+ - **Logs of compose-stack services.** The live tail (`/api/service/taillog`,
173
+ so the UI's log view and `logs_tail`), log history and search
174
+ (`logs_search`) and `<cli> logs` now find a stack service's container,
175
+ `<stack>-<workspace>-<service>-1`, through the stacks naming helpers. They
176
+ looked only for opctl containers, so they fell back to the startup log
177
+ ("container log unavailable") or found nothing.
178
+ - **`<cli> mcp` against an older web server.** A tool whose route the web
179
+ server predates (`stacks`, `edge_status`, a security `run_tests`) now says
180
+ `web server is Den <x>; <tool> needs >= <y>: run <cli> web restart`
181
+ instead of a bare 404; other 404s name the tool and the route.
182
+ - Sync never touches a dev-managed checkout: a managed entry whose path is,
183
+ holds or sits inside one is refused with the reason, and naming a
184
+ dev-managed repo says so instead of doing nothing.
185
+ - The What's new dialog showed every changelog bullet cut off mid-sentence.
186
+ - With nothing run yet, the empty output pane no longer takes 200px from the
187
+ Local Dev page.
188
+ - A security run's mode chosen on the Tests page (`api`, `full`) now reaches
189
+ the scan; it always ran `baseline` before.
190
+
191
+ ### Security
192
+
193
+ - Dependency advisories: `npm audit fix` plus `@cursor/sdk` 1.0.23 → 1.0.40
194
+ (drops its `undici` 5 / `zod` 3 tree) clear every critical and high
195
+ advisory. Left: a `minimatch` ReDoS under `openapi-typescript` (only fix is
196
+ a downgrade to 6.x; it matches local config globs only) and dev-only
197
+ tooling (jest, eslint) moderates.
198
+
199
+ ### Migrations
200
+
201
+ - None required: with no override files, manifests resolve as in 0.1.0.
202
+ - To let agents act through `<cli> mcp`, add `mcp.actions` to the org config.
203
+ - Placement needs no change: without `runs`, services keep working as
204
+ before. A service with an HTTP health URL, no commands, and nothing else
205
+ in its repo that runs here now shows as remote; add `runs: { local: local }`
206
+ if it is really on this machine. To
207
+ mark more envs as production than `prod` and `production`, set
208
+ `production_envs`.
209
+
210
+
7
211
  ## [0.1.0] - 2026-11-04
8
212
 
9
213
  The first Den release: one engine for local development, tests and releases
package/README.md CHANGED
@@ -9,7 +9,7 @@ Den knows nothing about any particular organisation. Everything specific to
9
9
  yours — product name, repos, environments, hosts, secrets backend, test
10
10
  accounts — lives in **your distribution**, not in Den.
11
11
 
12
- > **Status: 0.1.0**, the first release: `npm install @monster-orange/den`. Den is pre-1.0; each release lists the changes a distribution makes under **Migrations** in [CHANGELOG.md](CHANGELOG.md).
12
+ > **Status: 0.2.0**: `npm install @monster-orange/den`. Den is pre-1.0; each release lists the changes a distribution makes under **Migrations** in [CHANGELOG.md](CHANGELOG.md).
13
13
 
14
14
  ## Make it your own
15
15
 
package/bin/den CHANGED
@@ -40,6 +40,14 @@ if [ "${1:-}" = "create" ]; then
40
40
  entry="src/ui/cli/create-main.ts"
41
41
  built="dist/src/ui/cli/create-main.js"
42
42
  fi
43
+ # `mcp` speaks the MCP protocol on stdout: its own entry point, so nothing
44
+ # the full CLI prints can reach the protocol stream. Its setup subcommands
45
+ # (connect, disconnect, clients, config) are ordinary CLI commands.
46
+ if [ "${1:-}" = "mcp" ] && ! case "${2:-}" in connect|disconnect|clients|config) true ;; *) false ;; esac; then
47
+ shift
48
+ entry="src/ui/cli/mcp-main.ts"
49
+ built="dist/src/ui/cli/mcp-main.js"
50
+ fi
43
51
 
44
52
  # Escape hatch: force the compiled build (e.g. to test the prod artifact).
45
53
  if [ "${DEN_USE_DIST:-}" = "1" ] && [ -f "$DEN_ENGINE_ROOT/$built" ]; then
@@ -49,18 +49,152 @@ make help
49
49
 
50
50
  ## Layout
51
51
 
52
- | Path | What |
53
- | ------------------------- | ------------------------------------------------------------ |
54
- | `den.yml` | The product profile ([below](#denyml-the-product-profile)) |
55
- | `package.json` | Pins an exact `@monster-orange/den` version |
56
- | `bin/<cli>` | The shim people run ([below](#the-bin-shim)) |
57
- | `Makefile` | Includes Den's targets ([below](#make-denmk)) |
58
- | `config/<name>.yml` | Org config: brand, `github_org`, `secrets`, `edge`, … |
59
- | `config/environments/` | One `<env>.json` per environment (base URLs, auth, timeouts) |
60
- | `config/local/` | Per-engineer overrides (`user.yml`); gitignored |
61
- | `manifests/` | Manifests for repos that do not carry their own |
62
- | `tests/pillars/<pillar>/` | Your own suites, e.g. `tests/pillars/perf/<module>/test.js` |
63
- | `reports/`, `results/` | Test output; gitignored |
52
+ | Path | What |
53
+ | ------------------------- | ------------------------------------------------------------------------------------------------ |
54
+ | `den.yml` | The product profile ([below](#denyml-the-product-profile)) |
55
+ | `package.json` | Pins an exact `@monster-orange/den` version |
56
+ | `bin/<cli>` | The shim people run ([below](#the-bin-shim)) |
57
+ | `Makefile` | Includes Den's targets ([below](#make-denmk)) |
58
+ | `config/<name>.yml` | Org config: brand, `github_org`, `secrets`, `edge`, … |
59
+ | `config/environments/` | One `<env>.json` per environment (base URLs, auth, timeouts) |
60
+ | `config/local/` | Per-engineer overrides (`user.yml`); gitignored |
61
+ | `manifests/` | Manifests for repos that do not carry their own, and overrides ([below](#overriding-a-manifest)) |
62
+ | `tests/pillars/<pillar>/` | Your own suites, e.g. `tests/pillars/perf/<module>/test.js` |
63
+ | `reports/`, `results/` | Test output; gitignored |
64
+
65
+ ## Overriding a manifest
66
+
67
+ A repo's manifest comes from one base: the repo's own file (`.acme.yml`, or
68
+ its `.json` twin) or, when the repo has none, the one the distribution holds
69
+ at `manifests/<repo>.acme.yml`. On top of that base, an optional
70
+ `manifests/<repo>.override.yml` changes what this distribution needs to
71
+ differ, without a commit to the repo. The override file is named with
72
+ `.override` plus the manifest's extension, and `<repo>` is matched exactly as
73
+ for a held manifest: the checkout's `origin` repo name, then the managed-clone
74
+ name, then the folder name.
75
+
76
+ ```yaml
77
+ # manifests/shop.override.yml, merged onto shop's own .acme.yml
78
+ services:
79
+ web:
80
+ commands:
81
+ local:
82
+ up: make up-fast # replaces up; the base's other commands stay
83
+ depends_on: [] # lists replace whole: web no longer waits on api
84
+ urls: null # null deletes the key
85
+ ```
86
+
87
+ Merge rules:
88
+
89
+ - Maps merge by key, recursively.
90
+ - Lists and scalars replace the base value whole.
91
+ - An explicit `null` deletes the key from the result.
92
+
93
+ An override only changes or deletes what the base has. A service or key the
94
+ base lacks is an error, even inside a map whose keys are free-form (a new
95
+ command, a new environment): put new entries in the base. An override with no
96
+ base is an error, never a manifest on its own.
97
+
98
+ `<cli> validate <repo-dir>` checks each layer against the schema on its own,
99
+ naming the file each error is in, then checks the merged result:
100
+
101
+ ```
102
+ ✓ /src/shop/.acme.yml (repo, v1, no issues)
103
+ ✗ /dist/manifests/shop.override.yml (override, v1, 1 error)
104
+ ERROR services.api.commands.local.down unknown key "down": not in the base manifest
105
+ ```
106
+
107
+ The web UI's Services cards show the layers each manifest was built from
108
+ (`manifest: repo + override`), and `/api/services` returns them per service as
109
+ `manifestLayers`, e.g. `["held", "override"]`. `DEN_MANIFESTS_DIR` points the
110
+ held and override lookup at another directory (tests use it).
111
+
112
+ ## Placement
113
+
114
+ Each service says where it runs, per environment: `local` (on this machine,
115
+ started by Den) or `remote` (deployed elsewhere). A remote service still
116
+ shows in the graph, the health row and the applications, marked ☁, but Den
117
+ never starts, stops, scans or execs it. The common case is a hybrid
118
+ application: its frontend runs here and calls the team's dev deployment of
119
+ its backend.
120
+
121
+ ```yaml
122
+ # .acme.yml of shop-core
123
+ services:
124
+ core:
125
+ runs: { local: remote, dev: remote } # per env: local | remote
126
+ target:
127
+ env: dev # one of the org's environments
128
+ account: AWS # a label for where it is deployed
129
+ url: https://api.dev.acme.example/admin/manifest
130
+ expect: 401 # the API answers 401 when it is up (behind a JWT authorizer)
131
+ ```
132
+
133
+ When `runs` says nothing for the env being viewed:
134
+
135
+ - `local` if the service has commands there. Locally that also covers every
136
+ service of a repo that runs here: a compose stack, or a repo where any
137
+ service has `commands.local` (one repo-level `up` starts the others). A
138
+ remote service inside such a repo says `runs: { local: remote }`;
139
+ - `remote` if it has an HTTP health URL there and nothing in its repo runs
140
+ there;
141
+ - otherwise `none`: Den knows no runtime for it, and shows no placement.
142
+
143
+ `target` names where a remote service runs: `env`, an optional `account`
144
+ label, an optional `url`, and `expect`, a status code or a list of codes that
145
+ count as up when Den probes `url` (default: any 2xx). Codes are integers
146
+ 100–599. A service with `runs.local: remote` must have a `target`.
147
+
148
+ **Health.** A remote service is checked by the health declared for its
149
+ `target.env` (`health.dev` above), the same check Den runs when you view that
150
+ env. Only when there is none does Den GET `target.url` and compare the status
151
+ with `expect`. The health row's label names the target, `dev · AWS`, followed
152
+ by the status or response time.
153
+
154
+ **Validation.** `<cli> validate` rejects an env name in `runs`, or a
155
+ `target.env`, that is not one of the org's `environments`, an unknown key in
156
+ `target`, and `expect` codes outside 100–599. Placement is the one thing an
157
+ [override](#overriding-a-manifest) may add to a service its base has, so a
158
+ distribution can place a repo's service remote without a commit to the repo:
159
+
160
+ ```yaml
161
+ # manifests/shop-core.override.yml
162
+ services:
163
+ core:
164
+ runs: { local: remote }
165
+ target: { env: dev, account: AWS, url: https://api.dev.acme.example/health }
166
+ ```
167
+
168
+ **Where it shows.** `/api/services` and `/api/health` rows carry `runs`
169
+ (`local`, `remote` or `none`, for the env asked about) and `target`
170
+ (`{ env, account?, url? }`, or null unless remote). `/api/applications`
171
+ carries each service's placement in `placements` (keyed by address), and per
172
+ application `placement` (`local`, `remote`, `hybrid` or `none`) with a
173
+ `summary` such as `shop-frontend local · shop-core remote (dev)`.
174
+ `<cli> status` and `<cli> services` suffix remote services with
175
+ `☁ remote (dev)`; `<cli> apps` marks hybrid and remote applications and
176
+ repos; `<cli> graph` marks remote nodes (dashed in `--format dot`) and hybrid
177
+ ones (`⇄ hybrid`).
178
+
179
+ **Guards.**
180
+
181
+ - Service exec (`/api/service/exec`, `<cli> up|down|restart`, workspace-wide
182
+ up and down, `<cli> up`'s plan) refuses every command on a remote service:
183
+ `shop/core runs in dev (AWS); nothing to start here`. A `down` stops
184
+ nothing and succeeds, so a workspace down is not failed by it.
185
+ - MCP: `runsLocally()` reads the served `runs`, so agents cannot start, stop
186
+ or restart a remote service.
187
+ - Security: `auto_targets` never adds a remote service, even one with local
188
+ commands. A declared target of a remote service is not scanned in `local`,
189
+ only in other envs.
190
+ - **Production.** A remote service whose `target.env` counts as production is
191
+ never scanned, in any env, and never acted on. Production is an attribute of
192
+ an env in the org config, never a name check:
193
+
194
+ ```yaml
195
+ # config/acme.yml
196
+ production_envs: [prod] # default: [prod, production]
197
+ ```
64
198
 
65
199
  ## den.yml: the product profile
66
200
 
@@ -215,6 +349,143 @@ The service runs the engine's server with the `node` on your `PATH` at install
215
349
  time, from the distribution root; re-run `install` after changing node versions
216
350
  or upgrading Den. Keep the label stable: it is how launchd knows the service.
217
351
 
352
+ ## MCP server
353
+
354
+ `./bin/acme mcp` runs an [MCP](https://modelcontextprotocol.io) server over
355
+ stdio, so an agent (an MCP client such as an editor or a coding agent) can
356
+ read and, where the org allows it, act on the same local environment the web
357
+ UI shows. It is a thin client of the running web server
358
+ (`http://localhost:<web.port>`): agents and the UI share one state and one
359
+ set of guards. It never starts the web server; if it is not running, every
360
+ tool answers with the command that starts it (`./bin/acme web up`). stdout
361
+ carries only the protocol; diagnostics go to stderr.
362
+
363
+ The web server keeps running the Den it started with until it is restarted,
364
+ so after an upgrade it can be older than the MCP server. When a tool's route
365
+ answers 404, the MCP server reads the web server's version from
366
+ `/api/server-info` and, if it predates the route, says so:
367
+ `web server is Den 0.1.0; stacks needs >= 0.2.0: run acme web restart`. Any
368
+ other 404 is reported as it is.
369
+
370
+ ### Connecting your agents
371
+
372
+ `<cli> mcp connect` registers the server with the agents you use, so nobody
373
+ edits an MCP config by hand:
374
+
375
+ ```sh
376
+ ./bin/acme mcp clients # which agents are installed, which are connected
377
+ ./bin/acme mcp connect --all # every installed agent, user scope
378
+ ./bin/acme mcp connect claude-code # one (or several) by id
379
+ ./bin/acme mcp connect cursor --project ../shop # a project's config instead
380
+ ./bin/acme mcp connect --all --dry-run # show what it would write or run
381
+ ./bin/acme mcp disconnect cursor
382
+ ./bin/acme mcp config # JSON for any other MCP client
383
+ ```
384
+
385
+ | Client | User scope | Project scope (`--project [dir]`) |
386
+ | ---------------- | ------------------------------------- | --------------------------------- |
387
+ | `claude-code` | `claude mcp add --scope user` | `<dir>/.mcp.json` |
388
+ | `codex` | `codex mcp add` | – |
389
+ | `cursor` | `~/.cursor/mcp.json` | `<dir>/.cursor/mcp.json` |
390
+ | `vscode` | `code --add-mcp` | `<dir>/.vscode/mcp.json` |
391
+ | `claude-desktop` | its `claude_desktop_config.json` | – |
392
+ | `windsurf` | `~/.codeium/windsurf/mcp_config.json` | – |
393
+ | `gemini` | `~/.gemini/settings.json` | `<dir>/.gemini/settings.json` |
394
+
395
+ The server is registered under the CLI's name (`acme`) as the distribution's
396
+ `bin/<cli>` by absolute path, with `PATH` set to the directory of the `node`
397
+ that ran `connect` plus the system dirs. GUI agents start without your login
398
+ shell's PATH (so without nvm's node) and would otherwise fail to launch it.
399
+ After upgrading or switching node, run `connect` again. For a JSON config,
400
+ `connect` changes only its own entry, keeps the previous file as `.bak`, and
401
+ refuses to rewrite a file it cannot parse. Connecting again replaces the entry.
402
+ Restart the agent, or start a new session, to load it.
403
+
404
+ Project-scope files hold absolute paths on your machine: commit them only if
405
+ everyone's checkout is in the same place.
406
+
407
+ `--port <n>` (on `mcp` or `mcp connect`) points it at a web server on another
408
+ port.
409
+
410
+ ### Config
411
+
412
+ Without an `mcp:` block in the org config (`config/<name>.yml`) only the read
413
+ tools are registered. Actions are opt-in, by name:
414
+
415
+ ```yaml
416
+ mcp:
417
+ actions:
418
+ [service_up, service_down, service_restart, workspace_sync, run_tests]
419
+ run_tests:
420
+ envs: [local] # the default; production is refused even if listed
421
+ logs:
422
+ remote: false # the default; true lets the log tools read non-local sources
423
+ ```
424
+
425
+ An unknown action or environment name is an error, so a typo never turns a
426
+ guard off.
427
+
428
+ ### Tools
429
+
430
+ Read tools (always registered):
431
+
432
+ | Tool | Web API | What |
433
+ | ------------------- | ------------------------------------------ | ---------------------------------------------------------- |
434
+ | `status` | `/api/health` | Service health; `url` (HTTP check) or `check` (shell) |
435
+ | `services` | `/api/services` | Services, their mode (managed or dev-managed) and commands |
436
+ | `applications` | `/api/applications` | Org > Application > Repo > Services |
437
+ | `graph` | `/api/graph` | The derived dependency graph and startup order |
438
+ | `workspaces` | `/api/workspaces` | Workspaces, active and focused |
439
+ | `environments` | `/api/environments` | The enabled environment names (never their values) |
440
+ | `logs_search` | `/api/logs/search` | Search services' full log history |
441
+ | `logs_tail` | `/api/service/taillog` | The last lines of a service's log (bounded, no follow) |
442
+ | `tests_available` | `/api/modules`, `/api/test-sources` | A pillar's modules and the repos they come from |
443
+ | `security_latest` | `/api/security/latest` | The newest security scan per mode |
444
+ | `release_status` | `/api/release/runs`, `/api/release/status` | Recent CD runs, and an env's deployed-version table |
445
+ | `manifest_validate` | `/api/manifest/validate` | Validate a manifest by path or content |
446
+ | `stacks` | `/api/stacks` | What `stack ls/ps/health` show: project, routes, health |
447
+ | `edge_status` | `/api/edge` | The edge: up or down, its routes and cert names |
448
+
449
+ Action tools (only those named in `mcp.actions`): `service_up`,
450
+ `service_down`, `service_restart` (through `/api/service/exec`),
451
+ `workspace_sync` (`/api/workspace/sync`) and `run_tests` (`/api/run`). There
452
+ is no deploy tool.
453
+
454
+ ### Guards
455
+
456
+ - **Lifecycle only, local only.** The service tools run a manifest's `up`,
457
+ `down` or `restart` and nothing else, with `env=local`, and only for a
458
+ service run locally: its placement is `local` and it has a local `up`
459
+ command (see [Placement](#placement)).
460
+ - **Sync touches managed clones only.** It never pulls, resets or switches
461
+ branches in a dev-managed checkout (your own). The web server enforces
462
+ this for the UI's Sync too: a managed entry whose path is, holds or sits
463
+ inside a dev-managed checkout is refused, out loud.
464
+ - **Tests:** `run_tests` targets only `mcp.run_tests.envs` (default
465
+ `[local]`) and never production. A security run must first pass the
466
+ security CLI's own plan (`/api/security/plan`, a dry run): every target
467
+ host in `security.owned_hosts` and the mode within the env's ceiling.
468
+ `full` (active attacks) is local only.
469
+ - **Logs** read local sources only, unless `mcp.logs.remote: true`. All text
470
+ the tools return is scrubbed of secret-shaped values: bearer tokens, JWTs,
471
+ AWS keys, credentials in URLs and DSNs, `key=value` secrets, private-key
472
+ blocks.
473
+ - **Never exposed:** sudo passwords, AWS login, opening a terminal or editor,
474
+ workspace-wide exec, any other service command, settings writes, workspace
475
+ create, rename or mode changes, the server log, and anything that returns
476
+ environment values. The MCP server's HTTP client cannot reach any other
477
+ route.
478
+
479
+ ### Audit
480
+
481
+ Every action is recorded with actor `agent`, the MCP client's name and
482
+ version (as the client reports them at initialize: a label, never used to
483
+ authorise anything), and the MCP server's pid and session id. They travel as
484
+ `x-den-*` headers on each web API call. The web server writes each action
485
+ (and each UI one) as a lifecycle line in its log; on the Logs page, the
486
+ `acme/server` stream shows an agent's actions as `[agent]`, ending
487
+ `· by agent <client> <version> (mcp pid <pid>, session <id>)`.
488
+
218
489
  ## Pinning and upgrading
219
490
 
220
491
  `package.json` pins an exact Den version (`create` writes the version that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monster-orange/den",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Den: a local multi-repo dev orchestrator, HTTPS edge and E2E harness",
5
5
  "type": "module",
6
6
  "bin": {
@@ -53,7 +53,8 @@
53
53
  "author": "Monster Orange",
54
54
  "license": "MIT",
55
55
  "dependencies": {
56
- "@cursor/sdk": "1.0.23",
56
+ "@cursor/sdk": "1.0.40",
57
+ "@modelcontextprotocol/sdk": "1.32.1",
57
58
  "@playwright/test": "^1.61.1",
58
59
  "@slack/webhook": "^7.0.2",
59
60
  "ajv": "^8.12.0",
@@ -62,6 +63,7 @@
62
63
  "chalk": "^5.3.0",
63
64
  "cli-table3": "^0.6.3",
64
65
  "dotenv": "^16.4.1",
66
+ "elkjs": "0.12.0",
65
67
  "inquirer": "^9.2.14",
66
68
  "jsonwebtoken": "^9.0.2",
67
69
  "openapi-typescript": "^7.13.0",
@@ -48,6 +48,7 @@ import type { JourneyPersonaRole } from "./spec/schema.js";
48
48
  import { product } from "../../shared/product.js";
49
49
  import { enginePath } from "../../shared/engine.js";
50
50
  import { getDistRoot } from "../../shared/paths.js";
51
+ import { containsValue } from "../../shared/redact.js";
51
52
  // ── Layer 1: captureSecrets ──────────────────────────────────────────────────
52
53
 
53
54
  /** 1Password field label holding the Cursor API key (= env var name). */
@@ -375,11 +376,7 @@ export class TranscriptScrubber {
375
376
 
376
377
  /** Throws SecretLeakError when `text` contains any live secret value. */
377
378
  assertClean(text: string): void {
378
- for (const secret of this.secrets) {
379
- if (text.includes(secret)) {
380
- throw new SecretLeakError();
381
- }
382
- }
379
+ if (containsValue(text, this.secrets)) throw new SecretLeakError();
383
380
  }
384
381
  }
385
382
 
@@ -11,6 +11,10 @@
11
11
  import { discoverRepos } from "../smoke/discover.js";
12
12
  import { effectiveLocalCommands } from "../../shared/stacks/manifest-commands.js";
13
13
  import type { WorkspaceService } from "../../shared/manifest/workspace-manifests.js";
14
+ import {
15
+ servicePlacement,
16
+ type Placement,
17
+ } from "../../shared/manifest/placement.js";
14
18
  import type {
15
19
  DiscoveredRepo,
16
20
  SecurityServiceConfig,
@@ -27,6 +31,12 @@ export interface DiscoveredSecurityTarget {
27
31
  config: SecurityServiceConfig;
28
32
  /** Derived by `security.auto_targets` from a health URL, not declared. */
29
33
  auto?: true;
34
+ /**
35
+ * Where the service runs from this machine (R13). A remote one is never
36
+ * scanned as if it were local, and never at all when its target env counts
37
+ * as production (see run.ts).
38
+ */
39
+ placement?: Placement;
30
40
  }
31
41
 
32
42
  /** Repo-qualified name used by --service and the module picker. */
@@ -61,6 +71,7 @@ export function discoverSecurityTargets(
61
71
  repoPath: repo.repoPath,
62
72
  serviceName,
63
73
  config,
74
+ placement: servicePlacement(repo.manifest, serviceName, "local"),
64
75
  });
65
76
  }
66
77
  }
@@ -73,7 +84,8 @@ export function discoverSecurityTargets(
73
84
  * local commands or compose) with an HTTP health URL, scanned at that URL's
74
85
  * origin in each env that has one. Never a service its manifest marks
75
86
  * `security.skip`, one already declared under `security:`, or one with no
76
- * local runtime (a remote or shell-checked service). Host policy is applied
87
+ * local runtime (a remote or shell-checked service). A service whose
88
+ * placement is remote is excluded explicitly, whatever commands it has. Host policy is applied
77
89
  * later as for any target: every URL must be on the owned-host allowlist.
78
90
  */
79
91
  export function autoSecurityTargets(
@@ -85,6 +97,10 @@ export function autoSecurityTargets(
85
97
  for (const s of services) {
86
98
  const id = securityTargetId({ repoName: s.repo, serviceName: s.service });
87
99
  if (taken.has(id) || s.config?.security?.skip) continue;
100
+ // Explicitly: a service placed remote (R13) is never an automatic
101
+ // target, even when it also has local commands.
102
+ if (servicePlacement(s.manifest, s.service, "local").runs !== "local")
103
+ continue;
88
104
  const runsLocally =
89
105
  Object.keys(effectiveLocalCommands(s.manifest, s.service, s.repo, "auto"))
90
106
  .length > 0;