@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.
- package/CHANGELOG.md +204 -0
- package/README.md +1 -1
- package/bin/den +8 -0
- package/docs/distributions.md +283 -12
- package/package.json +4 -2
- package/src/pillars/journeys/sandbox.ts +2 -5
- package/src/pillars/security/discover.ts +17 -1
- package/src/pillars/security/run.ts +31 -3
- package/src/shared/agent-actor.ts +77 -0
- package/src/shared/den-config.ts +122 -0
- package/src/shared/edge/status.ts +74 -0
- package/src/shared/environment-manager/health.ts +54 -8
- package/src/shared/environment-manager/load-applications.ts +31 -1
- package/src/shared/environment-manager/log-tail.ts +5 -4
- package/src/shared/environment-manager/opctl.ts +2 -1
- package/src/shared/environment-manager/service-container.ts +47 -0
- package/src/shared/environment-manager/service-runner.ts +19 -2
- package/src/shared/environment-manager/sync-targets.ts +66 -0
- package/src/shared/environment-manager/up-plan.ts +25 -1
- package/src/shared/manifest/merge-override.ts +75 -0
- package/src/shared/manifest/placement.ts +333 -0
- package/src/shared/manifest/read-manifest.ts +156 -38
- package/src/shared/manifest/schema.ts +14 -0
- package/src/shared/manifest/validate-layers.ts +229 -0
- package/src/shared/manifest/validate-manifest.ts +66 -6
- package/src/shared/paths.ts +12 -0
- package/src/shared/redact.ts +107 -0
- package/src/shared/stacks/manifest-commands.ts +30 -0
- package/src/shared/stacks/status.ts +99 -0
- package/src/shared/types/index.ts +23 -0
- package/src/ui/cli/applications-command.ts +34 -2
- package/src/ui/cli/edge-command.ts +9 -19
- package/src/ui/cli/graph-command.ts +50 -4
- package/src/ui/cli/index.ts +184 -55
- package/src/ui/cli/mcp-connect.ts +350 -0
- package/src/ui/cli/mcp-main.ts +13 -0
- package/src/ui/cli/mcp-stdout-guard.ts +10 -0
- package/src/ui/cli/named-target.ts +11 -0
- package/src/ui/cli/stack-command.ts +13 -40
- package/src/ui/cli/test-runner.ts +8 -1
- package/src/ui/mcp/api.ts +286 -0
- package/src/ui/mcp/connect.ts +304 -0
- package/src/ui/mcp/guards.ts +145 -0
- package/src/ui/mcp/server.ts +149 -0
- package/src/ui/mcp/tools.ts +644 -0
- package/src/ui/server/audit.ts +137 -0
- package/src/ui/server/changelog-parser.ts +11 -0
- package/src/ui/server/graph-prefs.ts +89 -0
- package/src/ui/server/health-row.ts +15 -0
- package/src/ui/server/index.ts +256 -61
- package/src/ui/server/lifecycle-exec.ts +188 -0
- package/src/ui/server/log-history.ts +4 -3
- package/src/ui/server/public/environments.html +4 -2
- package/src/ui/server/public/graph-model.js +416 -0
- package/src/ui/server/public/graph.css +980 -0
- package/src/ui/server/public/graph.js +986 -0
- package/src/ui/server/public/header.js +7 -5
- package/src/ui/server/public/local-dev.html +130 -2191
- package/src/ui/server/public/logs.html +11 -4
- package/src/ui/server/public/panel.js +790 -0
- package/src/ui/server/public/services.html +6 -1
- package/src/ui/server/readonly-routes.ts +209 -0
- package/src/ui/server/ui-assets.ts +73 -0
- 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.
|
|
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
|
package/docs/distributions.md
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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).
|
|
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;
|