@monster-orange/den 0.2.6 → 0.2.8
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 +225 -0
- package/bin/den +9 -0
- package/docs/distributions.md +205 -6
- package/package.json +1 -1
- package/service/install.sh +41 -2
- package/src/shared/agent-actor.ts +38 -3
- package/src/shared/config-error.ts +33 -0
- package/src/shared/den-config.ts +84 -5
- package/src/shared/environment-manager/catalog.ts +61 -3
- package/src/shared/environment-manager/default-workspace.ts +53 -14
- package/src/shared/environment-manager/git-remote.ts +49 -0
- package/src/shared/environment-manager/health.ts +51 -8
- package/src/shared/environment-manager/service-runner.ts +51 -5
- package/src/shared/environment-manager/workspace-manager.ts +69 -26
- package/src/shared/environment-manager/workspace-org.ts +279 -0
- package/src/shared/manifest/service-links.ts +20 -8
- package/src/shared/org-sources.ts +309 -0
- package/src/shared/types/index.ts +19 -2
- package/src/ui/cli/clone-plan.ts +374 -0
- package/src/ui/cli/flag-parser.ts +57 -3
- package/src/ui/cli/index.ts +346 -48
- package/src/ui/cli/org-command.ts +172 -0
- package/src/ui/cli/results-viewer.ts +5 -5
- package/src/ui/cli/table.ts +20 -0
- package/src/ui/cli/up-args.ts +15 -0
- package/src/ui/cli/validate-all.ts +171 -0
- package/src/ui/mcp/scope.ts +277 -0
- package/src/ui/mcp/server.ts +94 -9
- package/src/ui/mcp/tools.ts +101 -17
- package/src/ui/server/audit.ts +90 -3
- package/src/ui/server/daemon-reaper.ts +22 -4
- package/src/ui/server/github-service.ts +6 -1
- package/src/ui/server/index.ts +96 -5
- package/src/ui/server/public/panel.js +1 -1
- package/src/ui/server/readonly-routes.ts +12 -3
- package/src/ui/server/release-service.ts +40 -5
- package/src/ui/server/workspace-routes.ts +13 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,231 @@ 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.8] - 2026-11-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **A scoped MCP server for cooperating agents** (0.2.8-dev.0, AG-2a-core).
|
|
12
|
+
`<cli> mcp --workspace <ws> --actor agent:<id>` pins one workspace: a tool
|
|
13
|
+
that takes a workspace gets `<ws>`, and a call naming another is refused
|
|
14
|
+
before it reaches the web server and recorded in the activity trail.
|
|
15
|
+
Lists are filtered to `<ws>`: `workspaces` shows it alone, the log tools
|
|
16
|
+
and `manifest_validate` take only its services and repos, `workspace_sync`
|
|
17
|
+
syncs it, and the test tools need it to be the focused workspace. Every
|
|
18
|
+
web API call is stamped with the actor (`x-den-mcp-agent`,
|
|
19
|
+
`x-den-mcp-workspace`), so each exec, run and sync in the activity trail
|
|
20
|
+
and the server log names `agent:<id>` and its workspace. Both flags or
|
|
21
|
+
neither; a bad value is a usage error (exit 2). `--list` and `--call` take
|
|
22
|
+
them too. Without them the server is unchanged. **Limit:** it binds
|
|
23
|
+
cooperating agents only. The scope lives in the MCP process, and the web
|
|
24
|
+
server's HTTP API does not check it yet (WS-3a, 0.4); `mcp connect` still
|
|
25
|
+
registers the unscoped server.
|
|
26
|
+
- **`<cli> setup --plan -w <ws>` and `--all-active`** (0.2.8-dev.0). `-w` is
|
|
27
|
+
repeatable and plans those workspaces, active or not; `--all-active` plans
|
|
28
|
+
every active one; on a fresh machine `-w` narrows the blueprint's plan. A
|
|
29
|
+
closing hint names the repos only workspaces outside the plan hold. `-w`
|
|
30
|
+
and `--all-active` go with `--plan` only.
|
|
31
|
+
- **Orgs are Den's own; sources are bindings** (0.2.8-dev.1, ORG-1d). An
|
|
32
|
+
org is any key of `orgs:`. `orgs.<id>.sources: [{host, owner|group}]`
|
|
33
|
+
claims git-host owners for it (an org can span two GitHub owners), and
|
|
34
|
+
`orgs.<id>.repos: [...]` lists repos outright (one owner can be split
|
|
35
|
+
across orgs). Catalog entries take `remote: <git url>` for any host;
|
|
36
|
+
`gh: owner/repo` stays the GitHub shorthand. A repo's org is the org
|
|
37
|
+
listing it, else the one whose source claims its owner, else its GitHub
|
|
38
|
+
owner; inference, `/api/orgs` and `org list` use it, and a claimed owner
|
|
39
|
+
is not listed as its own org. A repo clones from its source (GitHub with
|
|
40
|
+
`gh` as credential helper), or with no source from its `remote:` through
|
|
41
|
+
plain git and the user's own credential helper (SEC-0); `setup --plan`
|
|
42
|
+
shows that URL. Discovery (`register`) and the Release page skip an org
|
|
43
|
+
with no GitHub source. A bad value names its key; a repo or owner claimed
|
|
44
|
+
by two orgs is an error naming both. See docs/distributions.md, Orgs.
|
|
45
|
+
- **`<cli> register <path> -w <ws>`** (0.2.8-dev.0) writes that workspace,
|
|
46
|
+
active or not, and leaves focus alone. It used to fail with "-w/--workspace
|
|
47
|
+
is not valid with register".
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **`setup --plan` and `up --plan` show a dev-managed repo's real branch**
|
|
52
|
+
(0.2.8-dev.0): the one its checkout has out (read from `.git/HEAD`, no git
|
|
53
|
+
process), or `(branch unknown)`, where they printed `main` for every
|
|
54
|
+
checkout.
|
|
55
|
+
- **`up --plan` exits 1 when nothing resolves** (0.2.8-dev.0): a name no
|
|
56
|
+
workspace has, or a workspace with nothing to start. It exited 0.
|
|
57
|
+
- **`register` with no terminal exits 2** (0.2.8-dev.0) with one usage line
|
|
58
|
+
that names `-w`: `register needs a terminal (stdin is not a TTY); without
|
|
59
|
+
one: <cli> register <path> [--name <repo>] [--gh <owner/repo>] [-w
|
|
60
|
+
<workspace>]`. It went on to prompt with nothing to answer.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- **A config that fails validation stops every command** (0.2.8-dev.2).
|
|
65
|
+
A repo or owner claimed by two orgs (or any other bad ORG-1d value) is
|
|
66
|
+
checked before any command that touches orgs, the catalog, clones or
|
|
67
|
+
releases runs, and it exits 1 naming both keys. `sync` over existing
|
|
68
|
+
clones exited 0 and pulled, because it reads only workspaces.yml and the
|
|
69
|
+
catalog; `register <path> -w <ws>` wrote workspaces.yml and the catalog,
|
|
70
|
+
`workspace create` wrote workspaces.yml, and `workspace use` /
|
|
71
|
+
`add-service` exited 0. Now each stops before any git call or write. `web`, `mcp`, `validate`, `create`,
|
|
72
|
+
`config`, `local-dev` and `view-results` still run.
|
|
73
|
+
- **The web API answers a config that fails validation as JSON**
|
|
74
|
+
(0.2.8-dev.2). Every API route that reads the config answers 422
|
|
75
|
+
`{ ok: false, error: "<the message naming the keys>", code:
|
|
76
|
+
"config_invalid" }`, where `/api/orgs`, `/api/services` and others
|
|
77
|
+
answered a text/plain 500 "internal server error" and some routes turned
|
|
78
|
+
it into a 400 or an empty list. The error names config keys, never
|
|
79
|
+
values. `/api/server-info`, `/api/server-log`, `/api/changelog` and
|
|
80
|
+
`/api/web/restart` still answer, and pages still load.
|
|
81
|
+
- **A scoped MCP `manifest_validate` refused on another workspace's path
|
|
82
|
+
shows in that repo's activity** (0.2.8-dev.2). The refusal is recorded
|
|
83
|
+
under the catalog repo whose checkout holds the path (in any workspace),
|
|
84
|
+
as `logs_tail` refusals are since 0.2.8-dev.1. The entry is addressed by
|
|
85
|
+
the repo, not the path; its detail is the refusal message, which quotes
|
|
86
|
+
the path.
|
|
87
|
+
- **A scoped MCP refusal of a non-lifecycle tool shows on its service**
|
|
88
|
+
(0.2.8-dev.1). A refused `logs_tail`, `logs_search` or other non-lifecycle tool
|
|
89
|
+
was stored with an empty detail and no service, so the service panel's
|
|
90
|
+
activity (`GET /api/activity?address=<repo>/<svc>`) never showed it. The
|
|
91
|
+
entry's detail is now the refusal, and it carries `addresses`: the repo
|
|
92
|
+
or `repo/service` the call named, or every target of a `logs_search`
|
|
93
|
+
(one entry, listed under each).
|
|
94
|
+
- **`up --plan -w <ws>` with nothing to start is a usage error**
|
|
95
|
+
(0.2.8-dev.1): one line, exit 2, naming `up -a -w <ws>` (the whole
|
|
96
|
+
workspace) and `up <repo> -w <ws>`. It printed "No services specified and
|
|
97
|
+
no invocation target resolvable".
|
|
98
|
+
- **Test workers inherit no credentials** (0.2.8-dev.0). `tests/setup.ts`
|
|
99
|
+
strips credential-shaped variables (by name, family or token shape) from
|
|
100
|
+
the environment before any suite runs, so a failing assertion on a
|
|
101
|
+
spawn env can no longer print a developer's tokens.
|
|
102
|
+
|
|
103
|
+
### Migrations
|
|
104
|
+
|
|
105
|
+
- None. A script that ran `up --plan` and ignored its exit code may now see
|
|
106
|
+
1 when the plan is empty (2 for `up --plan -w <ws>` with no repo or
|
|
107
|
+
`-a`).
|
|
108
|
+
- ORG-1d (`orgs.<id>.sources`, `orgs.<id>.repos`, catalog `remote:`) is
|
|
109
|
+
optional: no change is needed, and a config without those keys behaves
|
|
110
|
+
exactly as on 0.2.7. A den.yml that uses them needs Den 0.2.8 or later.
|
|
111
|
+
- A client of the web API that reads a config error should check for
|
|
112
|
+
status 422 and `code: "config_invalid"` (0.2.8-dev.2); it was a
|
|
113
|
+
text/plain 500.
|
|
114
|
+
|
|
115
|
+
## [0.2.7] - 2026-11-05
|
|
116
|
+
|
|
117
|
+
### Added
|
|
118
|
+
|
|
119
|
+
- **A workspace belongs to an org** (0.2.7-dev.2, ORG-1a). Its org is
|
|
120
|
+
`org:` on the workspace in workspaces.yml, else `org:` on the blueprint
|
|
121
|
+
workspace (`defaults.workspaces.<n>.org`, the one new blueprint key), else
|
|
122
|
+
the one catalog owner of its repos (case-insensitive; `gh: ""` ignored),
|
|
123
|
+
else `github_org`, the default org. It is computed on read, never written
|
|
124
|
+
to workspaces.yml. A config with no `org:` resolves to `github_org` as
|
|
125
|
+
before, unless all of a workspace's repos have one other owner in the
|
|
126
|
+
catalog.
|
|
127
|
+
- **`<cli> org list`** (0.2.7-dev.2, ORG-1a): each org's label, its
|
|
128
|
+
workspaces on this machine, which is the default, and the org warnings.
|
|
129
|
+
`<cli> workspace list` gains an Org column; `--org <id>` filters it.
|
|
130
|
+
- **Org warnings, never errors** (0.2.7-dev.2, ORG-1a, C1): a workspace
|
|
131
|
+
whose repos span several owners, an `org:` naming an owner Den does not
|
|
132
|
+
know, a repo in another owner than its workspace's org, a workspaces.yml
|
|
133
|
+
`org:` that overrides the blueprint's. Shown by `org list` and
|
|
134
|
+
`workspace list`.
|
|
135
|
+
- **`<cli> setup --plan`** (0.2.7-dev.2, ORG-1a): each repo's org and clone
|
|
136
|
+
URL, and whether it is cloned, then stop. On a fresh machine it plans the
|
|
137
|
+
blueprint's workspaces; once seeded, the focused workspace. `<cli> up
|
|
138
|
+
--plan` prints the same lines for its repos. Nothing is cloned, seeded or
|
|
139
|
+
written.
|
|
140
|
+
- **`GET /api/orgs` entries gain `default` and `workspaces`**, and
|
|
141
|
+
`/api/<cli>/config` gains `default_org` (0.2.7-dev.2, ORG-1a). Every
|
|
142
|
+
existing field, entry and order is unchanged; `?repos=1` still maps each
|
|
143
|
+
repo to its owner string.
|
|
144
|
+
- **`<cli> register --org <id>`** (0.2.7-dev.2, ORG-1a) lists another org's
|
|
145
|
+
repos for a managed clone.
|
|
146
|
+
|
|
147
|
+
- **Bare `<cli> validate` checks every manifest** (0.2.7-dev.1, from a
|
|
148
|
+
consumer). In the distribution root, with no argument: each workspace repo
|
|
149
|
+
once per checkout (own or held, plus any override, then the merge), the
|
|
150
|
+
held manifest when there is no checkout, and held manifests no workspace
|
|
151
|
+
uses. One line per repo, details on failure, exit 1 if any fails, nothing
|
|
152
|
+
cloned. `--held` checks `manifests/` alone, independent of the machine. A
|
|
153
|
+
path argument works as before.
|
|
154
|
+
|
|
155
|
+
### Changed
|
|
156
|
+
|
|
157
|
+
- **A repo is released from its own org** (0.2.7-dev.2, REL-1). With no
|
|
158
|
+
`release.org`, a repo with no `gh:` in `release.repos` released from
|
|
159
|
+
`github_org`, so a client org's repo targeted the wrong owner. Its owner
|
|
160
|
+
is now an explicit `release.org` (unchanged: it still wins for every
|
|
161
|
+
repo), else the repo's non-empty catalog owner, else `github_org`.
|
|
162
|
+
- **`register` lists the focused workspace's org** (0.2.7-dev.2, ORG-1a),
|
|
163
|
+
not `github_org` alone, and no longer falls back to the owner of Den's own
|
|
164
|
+
git remote (SEC-0): with no org it says how to set one.
|
|
165
|
+
|
|
166
|
+
### Fixed
|
|
167
|
+
|
|
168
|
+
- **A second root's server leaves the first root's services alone**
|
|
169
|
+
(0.2.7-dev.3, dev.4). Every root of a product shares one daemons file.
|
|
170
|
+
A server's two startup sweeps (the stale-daemon prune and the health
|
|
171
|
+
reaper) dropped any record whose address did not resolve in its own
|
|
172
|
+
workspaces, so starting a scratch root's server silently forgot another
|
|
173
|
+
root's running dev server; an interactive Ctrl-C stopped every service in
|
|
174
|
+
the file, and deactivating a workspace released any record with the same
|
|
175
|
+
workspace name. Records are now stamped with the root that started them
|
|
176
|
+
and a server lists, stops, releases and sweeps only its own (other roots'
|
|
177
|
+
records stay in the file untouched). An older unstamped record is never
|
|
178
|
+
dropped just for not resolving; it still goes when its containers are
|
|
179
|
+
verifiably gone or two health checks fail. The reaper now logs what it
|
|
180
|
+
removes.
|
|
181
|
+
- **A service the edge has no route for is no longer healthy** (0.2.7-dev.2).
|
|
182
|
+
A health URL served through the edge answered the edge's own
|
|
183
|
+
`404 page not found` when the service had no route, and Den counted any
|
|
184
|
+
404 as up. That exact 404 is now down with "no route (edge 404)", over
|
|
185
|
+
HTTP and HTTPS; a 404 from the service itself is still up.
|
|
186
|
+
- **A client workspace no longer clones from the default org** (0.2.7-dev.2,
|
|
187
|
+
ORG-1a). Seeding, `up` and the web sync gave a repo with no clone URL
|
|
188
|
+
`<github_org>/<repo>`; it is now `<org>/<repo>` with its workspace's org.
|
|
189
|
+
- **`<cli> setup` clones a catalog entry with `gh: ""`** from its
|
|
190
|
+
workspace's org (0.2.7-dev.2), as `up` does, instead of failing on an
|
|
191
|
+
empty slug.
|
|
192
|
+
- **A seed without a terminal no longer resets a workspace that has
|
|
193
|
+
entries** (SEED-0). `<cli> init` and `<cli> workspace seed` from a script
|
|
194
|
+
or `make` replaced every entry of the default (or blueprint) workspaces with
|
|
195
|
+
managed clones, your own dev-managed checkouts included, without asking.
|
|
196
|
+
Without a TTY they now refuse (exit 1, nothing written) and name
|
|
197
|
+
`--reset`; on a TTY they still ask, default No. `init --reset` and
|
|
198
|
+
`workspace seed --reset` reset as before.
|
|
199
|
+
- **A failed seed leaves the workspaces as they were** (0.2.7-dev.1, from a
|
|
200
|
+
consumer). Seeding wrote workspaces.yml, then filled the catalog, which
|
|
201
|
+
fails with no `github_org`: the workspace was reset and nothing could be
|
|
202
|
+
cloned. The catalog comes first now, and the org is needed only for a repo
|
|
203
|
+
with no clone URL in the catalog.
|
|
204
|
+
- **A seed refusal names the command that was run** (`init --reset` or
|
|
205
|
+
`workspace seed --reset`).
|
|
206
|
+
- **A `DEN_ROOT` with no den.yml is a one-line error** (exit 2), not a stack
|
|
207
|
+
trace from deep in module loading.
|
|
208
|
+
- **Piped output carries no ANSI from tables** (`status`, `services`, test
|
|
209
|
+
results). cli-table3 coloured borders and head cells itself, ignoring
|
|
210
|
+
NO_COLOR and a pipe.
|
|
211
|
+
- **`<cli> mcp --call` validates its arguments** against the tool's schema,
|
|
212
|
+
strictly: an unknown or misspelt key, or a bad value, exits 2 with the
|
|
213
|
+
problem and the keys the tool takes, and calls nothing.
|
|
214
|
+
- **The web UI service survives a removed nvm version.** Install prefers a
|
|
215
|
+
stable node of the same major version over a version manager's (or uses
|
|
216
|
+
`DEN_SERVICE_NODE`), and warns when it must keep one. `service status`
|
|
217
|
+
exits 1, naming it, when the installed node no longer exists.
|
|
218
|
+
|
|
219
|
+
- **`<cli> init` in the distribution root seeds even through a symlinked
|
|
220
|
+
path** (`/var` → `/private/var`, a symlinked home). It compared paths
|
|
221
|
+
without resolving links and ran the manifest scaffolder instead.
|
|
222
|
+
|
|
223
|
+
### Migrations
|
|
224
|
+
|
|
225
|
+
- A script that relied on a non-interactive seed resetting a changed
|
|
226
|
+
workspace must pass `--reset`.
|
|
227
|
+
- Optional (ORG-1a): to put a blueprint workspace in an org other than the
|
|
228
|
+
one its catalog owners give, add `org: <id>` to it under
|
|
229
|
+
`defaults.workspaces`. Pin Den 0.2.7 or later first: an older Den rejects
|
|
230
|
+
the key. Nothing else needs changing.
|
|
231
|
+
|
|
7
232
|
## [0.2.6] - 2026-11-04
|
|
8
233
|
|
|
9
234
|
### Fixed
|
package/bin/den
CHANGED
|
@@ -49,6 +49,15 @@ if [ "${1:-}" = "mcp" ] && ! case "${2:-}" in connect|disconnect|clients|config)
|
|
|
49
49
|
built="dist/src/ui/cli/mcp-main.js"
|
|
50
50
|
fi
|
|
51
51
|
|
|
52
|
+
# A DEN_ROOT that names no distribution is a usage error, not a crash: say
|
|
53
|
+
# so here, before any module resolves paths from it at import time.
|
|
54
|
+
# (`create` needs no distribution.)
|
|
55
|
+
if [ -n "${DEN_ROOT:-}" ] && [ "$entry" != "src/ui/cli/create-main.ts" ] && [ ! -f "$DEN_ROOT/den.yml" ]; then
|
|
56
|
+
echo "den: DEN_ROOT is set to \"$DEN_ROOT\" but that directory has no den.yml." >&2
|
|
57
|
+
echo " Point it at your distribution's checkout, or unset it." >&2
|
|
58
|
+
exit 2
|
|
59
|
+
fi
|
|
60
|
+
|
|
52
61
|
# Escape hatch: force the compiled build (e.g. to test the prod artifact).
|
|
53
62
|
if [ "${DEN_USE_DIST:-}" = "1" ] && [ -f "$DEN_ENGINE_ROOT/$built" ]; then
|
|
54
63
|
exec node "$DEN_ENGINE_ROOT/$built" "$@"
|
package/docs/distributions.md
CHANGED
|
@@ -104,6 +104,23 @@ naming the file each error is in, then checks the merged result:
|
|
|
104
104
|
ERROR services.api.commands.local.down unknown key "down": not in the base manifest
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
Run with no argument in the distribution root, `<cli> validate` checks every
|
|
108
|
+
manifest the distribution uses, and never clones:
|
|
109
|
+
|
|
110
|
+
- each repo of every workspace, once per checkout path, with the
|
|
111
|
+
workspaces that use it and where its base comes from (`own`, `held`,
|
|
112
|
+
`+ override`);
|
|
113
|
+
- a repo with no checkout on disk falls back to its held manifest, marked
|
|
114
|
+
`held only, no checkout`. With no held manifest either it is a warning, not
|
|
115
|
+
a failure;
|
|
116
|
+
- each held manifest in `manifests/` that no workspace uses.
|
|
117
|
+
|
|
118
|
+
It prints one line per repo, details only for a layer with errors or
|
|
119
|
+
warnings, and exits 1 if any fails. `<cli> validate --held` checks only
|
|
120
|
+
`manifests/`, as if nothing were checked out, so the result does not depend on
|
|
121
|
+
the machine (use it in a CI or upgrade gate). An override whose base is the
|
|
122
|
+
repo's own manifest is noted there, not checked.
|
|
123
|
+
|
|
107
124
|
The web UI's Services cards show the layers each manifest was built from
|
|
108
125
|
(`manifest: repo + override`), and `/api/services` returns them per service as
|
|
109
126
|
`manifestLayers`, e.g. `["held", "override"]`. `DEN_MANIFESTS_DIR` points the
|
|
@@ -435,8 +452,8 @@ defaults:
|
|
|
435
452
|
focus: true
|
|
436
453
|
```
|
|
437
454
|
|
|
438
|
-
Each entry takes `repos`, `branch` and `
|
|
439
|
-
`defaults.workspace`, plus `focus`. Seeding creates every named workspace
|
|
455
|
+
Each entry takes `repos`, `branch`, `autostart_off` and `org`, the same
|
|
456
|
+
fields as `defaults.workspace`, plus `focus`. Seeding creates every named workspace
|
|
440
457
|
with its repos, activates them all, and focuses the one with `focus: true`
|
|
441
458
|
(else the first). Set `defaults.workspace` or `defaults.workspaces`, not both:
|
|
442
459
|
both at once is an error, as is more than one `focus: true`.
|
|
@@ -452,6 +469,141 @@ Once seeded, a repo that later joins `defaults.workspace.repos` is added to
|
|
|
452
469
|
you removed from it or that another workspace holds. With
|
|
453
470
|
`defaults.workspaces` nothing is topped up.
|
|
454
471
|
|
|
472
|
+
To see what a setup would clone before it clones anything, run
|
|
473
|
+
`acme setup --plan`: each repo with its org and clone URL, and whether it
|
|
474
|
+
is cloned yet. On a fresh machine it plans the blueprint (every workspace
|
|
475
|
+
`acme workspace seed` would create); once seeded, the focused workspace.
|
|
476
|
+
`-w <workspace>` (repeatable) plans those workspaces instead, active or not,
|
|
477
|
+
and `--all-active` every active one; on a fresh machine `-w` narrows the
|
|
478
|
+
blueprint's plan. Repos that only workspaces outside the plan hold are named
|
|
479
|
+
in a closing hint. A dev-managed repo shows the branch its checkout has out
|
|
480
|
+
(read from its `.git/HEAD`), not a configured one. `acme up --plan` lists
|
|
481
|
+
the same lines for the repos it would start, and exits 1 when nothing
|
|
482
|
+
resolves (a name no workspace has, an empty workspace). Both are read-only:
|
|
483
|
+
nothing is cloned, seeded or written.
|
|
484
|
+
|
|
485
|
+
## Orgs
|
|
486
|
+
|
|
487
|
+
A distribution can hold work for more than one org: each workspace belongs
|
|
488
|
+
to one. An org's id is a key of `orgs:` or a GitHub owner, and today an org
|
|
489
|
+
is the GitHub owner of the same name. `github_org` is the default org. A
|
|
490
|
+
distribution that never mentions orgs has one org, the default, and every
|
|
491
|
+
workspace is in it.
|
|
492
|
+
|
|
493
|
+
```yaml
|
|
494
|
+
github_org: acme
|
|
495
|
+
orgs:
|
|
496
|
+
acme: { label: Acme }
|
|
497
|
+
globex: { label: Globex, color: "#0af" }
|
|
498
|
+
defaults:
|
|
499
|
+
workspaces:
|
|
500
|
+
storefront:
|
|
501
|
+
repos: [shop-web, shop]
|
|
502
|
+
client:
|
|
503
|
+
org: globex # optional: the workspace's org
|
|
504
|
+
repos: [globex-web, globex-api]
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
A workspace's org is the first of:
|
|
508
|
+
|
|
509
|
+
1. `org:` on the workspace in `config/local/workspaces.yml` (this machine)
|
|
510
|
+
2. `org:` on the blueprint workspace of the same name (committed)
|
|
511
|
+
3. the one owner of its repos in the catalog (`gh: <owner>/<repo>`; owners
|
|
512
|
+
compare case-insensitively, and a `gh: ""` counts as unknown)
|
|
513
|
+
4. `github_org`
|
|
514
|
+
|
|
515
|
+
It is worked out each time it is read and never written back. A repo with
|
|
516
|
+
no clone URL is cloned from `<org>/<repo>` with its workspace's org: seeding
|
|
517
|
+
the blueprint above gives `globex-web` `gh: globex/globex-web`, not
|
|
518
|
+
`acme/...`. `acme register` picks a managed clone from the focused
|
|
519
|
+
workspace's org, or from `--org <id>`.
|
|
520
|
+
|
|
521
|
+
`acme org list` shows each org's label, its workspaces and which is the
|
|
522
|
+
default. `acme workspace list` has an Org column, and `--org <id>` lists one
|
|
523
|
+
org's workspaces. A workspace whose repos belong to several owners, an
|
|
524
|
+
`org:` naming an owner Den does not know, or a repo in another owner than
|
|
525
|
+
its workspace's org is a warning in both lists, never an error.
|
|
526
|
+
`GET /api/orgs` adds `default` and `workspaces` to each entry, and
|
|
527
|
+
`/api/<cli>/config` adds `default_org`.
|
|
528
|
+
|
|
529
|
+
`org:` in the blueprint needs Den 0.2.7 or later: an older Den rejects the
|
|
530
|
+
unknown key. Pin 0.2.7 before you commit it.
|
|
531
|
+
|
|
532
|
+
### Sources and repos (0.2.8)
|
|
533
|
+
|
|
534
|
+
An org is Den's own: its id is any key of `orgs:`, and it needs no git
|
|
535
|
+
host. Two optional keys bind it to where its repos live:
|
|
536
|
+
|
|
537
|
+
```yaml
|
|
538
|
+
github_org: acme
|
|
539
|
+
orgs:
|
|
540
|
+
acme-group: # one org, two GitHub owners
|
|
541
|
+
label: Acme Group
|
|
542
|
+
sources:
|
|
543
|
+
- { host: github, owner: acme }
|
|
544
|
+
- { host: github, owner: acme-labs }
|
|
545
|
+
globex-billing: # one owner split across two orgs
|
|
546
|
+
repos: [globex-invoices]
|
|
547
|
+
globex:
|
|
548
|
+
sources: [{ host: github, owner: globex }]
|
|
549
|
+
initech: # no git host at all
|
|
550
|
+
sources: []
|
|
551
|
+
repos: [widget]
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
- `orgs.<id>.sources` is a list of `{host, owner}` (or `{host, group}`). A
|
|
555
|
+
source claims that owner's repos for the org. `host: github` is built
|
|
556
|
+
in; any other host id (`gitlab`, …) claims repos whose `remote:` is on
|
|
557
|
+
`<host>.com` or a `<host>.` hostname, and clones them from that remote.
|
|
558
|
+
- `orgs.<id>.repos` lists repos that belong to the org whatever their
|
|
559
|
+
owner.
|
|
560
|
+
- An entry with neither key is what it was before 0.2.8: the GitHub owner
|
|
561
|
+
of its own name.
|
|
562
|
+
|
|
563
|
+
The catalog (`config/local/service-catalog.yml`) takes a `remote:` for any
|
|
564
|
+
host, beside `gh: owner/repo`, which stays as the GitHub shorthand:
|
|
565
|
+
|
|
566
|
+
```yaml
|
|
567
|
+
widget: { remote: git@git.initech.example:tools/widget.git }
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
A repo's org is the org whose `repos` lists it, else the org with a source
|
|
571
|
+
that claims its owner, else its GitHub owner as before. That org is what
|
|
572
|
+
`/api/orgs?repos=1` maps the repo to and what infers a workspace's org (step
|
|
573
|
+
3 above). A claimed owner is not listed as an org of its own, and a
|
|
574
|
+
`github_org` that names one makes the claiming org the default. Service rows
|
|
575
|
+
(`/api/services`, `/api/health`) keep `org` as the repo's GitHub owner.
|
|
576
|
+
|
|
577
|
+
A repo clones from its source: a GitHub source gives
|
|
578
|
+
`https://github.com/<owner>/<repo>.git` with your `gh` as credential helper.
|
|
579
|
+
A repo with no source that claims it clones and pulls from its `remote:`
|
|
580
|
+
through plain git, with your own git credential helper; Den never hands it
|
|
581
|
+
`gh` or any credential of its own. A remote must be `https://`, `ssh://`,
|
|
582
|
+
`git://`, `file://`, `user@host:path` or an absolute path. `setup --plan`
|
|
583
|
+
shows it as the clone URL, with any user info masked.
|
|
584
|
+
|
|
585
|
+
An org with no GitHub source has no repo discovery (`register` lists
|
|
586
|
+
nothing for it) and its repos are left off the Release page.
|
|
587
|
+
|
|
588
|
+
These keys are checked when den.yml is read, and a bad value is an error
|
|
589
|
+
that names its key (`orgs.initech.sources[0].owner must be …`). A repo
|
|
590
|
+
listed by two orgs, or an owner claimed by two orgs' sources (or by a
|
|
591
|
+
source and an org with no `sources` of that name), is an error naming both
|
|
592
|
+
keys. Such an error stops every command that touches orgs, the catalog,
|
|
593
|
+
clones or releases (exit 1, before any git call or write), and every web
|
|
594
|
+
API route that reads the config answers it as 422 `{ "ok": false, "error":
|
|
595
|
+
"<message>", "code": "config_invalid" }`. Without the new keys nothing
|
|
596
|
+
changes: `/api/orgs` is byte for byte what 0.2.7 answered. They need Den
|
|
597
|
+
0.2.8 or later.
|
|
598
|
+
|
|
599
|
+
## Releases
|
|
600
|
+
|
|
601
|
+
The Release page dispatches each repo's CD workflow on GitHub. A repo's
|
|
602
|
+
owner there is its own `gh: owner/repo` in `release.repos`, else
|
|
603
|
+
`release.org` when it is set, else the repo's owner in the catalog, else
|
|
604
|
+
`github_org`. Leave `release.org` unset when the repos belong to different
|
|
605
|
+
orgs: each then releases from its own.
|
|
606
|
+
|
|
455
607
|
## The bin shim
|
|
456
608
|
|
|
457
609
|
`bin/<cli>` is three lines of sh: it names the distribution root and runs
|
|
@@ -580,10 +732,17 @@ node_modules/@monster-orange/den/service/install.sh print # the plist only
|
|
|
580
732
|
| `DEN_ROOT` | required in a distribution |
|
|
581
733
|
| `DEN_SERVICE_LABEL` | `local.<distribution dir name>.web` |
|
|
582
734
|
| `DEN_SERVICE_LOGS` | `~/Library/Logs/<distribution dir name>` |
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
735
|
+
| `DEN_SERVICE_NODE` | see below |
|
|
736
|
+
|
|
737
|
+
The service runs the engine's server, from the distribution root, with the
|
|
738
|
+
`node` on your `PATH` at install time. A version manager's node (nvm, fnm,
|
|
739
|
+
volta, asdf, mise) lives under a versioned path that disappears when that
|
|
740
|
+
version is removed, so install prefers `/opt/homebrew/bin/node` or
|
|
741
|
+
`/usr/local/bin/node` when one has the same major version, and otherwise
|
|
742
|
+
keeps the version manager's and warns. `DEN_SERVICE_NODE` names the node
|
|
743
|
+
outright. `service status` exits 1 when the installed node no longer exists.
|
|
744
|
+
Re-run `install` after changing node versions or upgrading Den. Keep the
|
|
745
|
+
label stable: it is how launchd knows the service.
|
|
587
746
|
|
|
588
747
|
## MCP server
|
|
589
748
|
|
|
@@ -650,6 +809,46 @@ everyone's checkout is in the same place.
|
|
|
650
809
|
`--port <n>` (on `mcp` or `mcp connect`) points it at a web server on another
|
|
651
810
|
port.
|
|
652
811
|
|
|
812
|
+
### Scoped to one workspace
|
|
813
|
+
|
|
814
|
+
```sh
|
|
815
|
+
./bin/acme mcp --workspace smb --actor agent:planner
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
`--workspace <ws>` and `--actor agent:<id>` (both or neither) scope the server
|
|
819
|
+
to one workspace for one agent:
|
|
820
|
+
|
|
821
|
+
- **Pinned.** A tool that takes a `workspace` gets `<ws>` when none is given.
|
|
822
|
+
A call naming another workspace is refused (`Refused: <tool>: this MCP
|
|
823
|
+
server is scoped to workspace "<ws>" (agent:<id>); "<other>" is another
|
|
824
|
+
workspace`) before anything reaches the web server. A refused call is
|
|
825
|
+
recorded in the activity trail, read tools included.
|
|
826
|
+
- **Filtered.** `workspaces` lists `<ws>` only. `logs_search` and `logs_tail`
|
|
827
|
+
take only `<ws>`'s services, `manifest_validate` takes only an absolute path
|
|
828
|
+
inside one of its repos (or the content), and `workspace_sync` syncs `<ws>`.
|
|
829
|
+
Tests run against the focused workspace, so `tests_available` and
|
|
830
|
+
`run_tests` are refused unless `<ws>` is the focused one.
|
|
831
|
+
- **Not workspace data**, served as unscoped: `environments`,
|
|
832
|
+
`security_latest`, `release_status` and `edge_status` (the org's and the
|
|
833
|
+
distribution's, not a workspace's).
|
|
834
|
+
- **Stamped.** Every web API call carries `x-den-mcp-agent: agent:<id>` and
|
|
835
|
+
`x-den-mcp-workspace: <ws>` beside the usual audit headers. The activity
|
|
836
|
+
entry gains `agent` and `workspace`, and the log line reads `· by
|
|
837
|
+
agent:<id> <client> <version> (workspace <ws>, mcp pid <pid>, session
|
|
838
|
+
<id>)` for every exec, run and sync.
|
|
839
|
+
- `<ws>` must exist in this distribution; until it does, every tool says so.
|
|
840
|
+
The id is letters, digits, dots, dashes and underscores (at most 64). A bad
|
|
841
|
+
or half-given scope is a usage error, exit 2. `--list` and `--call` take
|
|
842
|
+
the scope too.
|
|
843
|
+
|
|
844
|
+
**It binds cooperating agents only.** The scope lives in the MCP server's
|
|
845
|
+
process. The web server's HTTP API does not check it yet, so an agent that
|
|
846
|
+
calls the API itself (curl, a browser) is not held to its workspace. Enforcing
|
|
847
|
+
the scope in the HTTP server is WS-3a, planned for 0.4. `mcp connect` still
|
|
848
|
+
registers the unscoped server; to hand one agent a scoped server, add it to
|
|
849
|
+
that agent's config yourself, for example in a project's `.mcp.json`:
|
|
850
|
+
`"args": ["mcp", "--workspace", "smb", "--actor", "agent:planner"]`.
|
|
851
|
+
|
|
653
852
|
### One call from a shell
|
|
654
853
|
|
|
655
854
|
```sh
|
package/package.json
CHANGED
package/service/install.sh
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
#
|
|
16
16
|
# DEN_SERVICE_LABEL launchd label (default local.<dist dir name>.web)
|
|
17
17
|
# DEN_SERVICE_LOGS log directory (default ~/Library/Logs/<dist dir name>)
|
|
18
|
+
# DEN_SERVICE_NODE the node the agent runs (default: see pick_node)
|
|
18
19
|
# DEN_SERVICE_READY_CHECK=external install does not print "running": the
|
|
19
20
|
# caller (`<cli> service install`) checks the server
|
|
20
21
|
# answers first.
|
|
@@ -47,8 +48,36 @@ find_tsx() {
|
|
|
47
48
|
done
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
# A version manager's node (nvm, fnm, volta, asdf, mise) lives under a
|
|
52
|
+
# versioned path that disappears when that version is removed, and the agent
|
|
53
|
+
# then fails to start with no clear reason. Prefer a stable install of the
|
|
54
|
+
# same major version; otherwise keep it and say so.
|
|
55
|
+
STABLE_NODES="${DEN_SERVICE_STABLE_NODES:-/opt/homebrew/bin/node /usr/local/bin/node}"
|
|
56
|
+
node_major() { "$1" -p "process.versions.node.split('.')[0]" 2>/dev/null; }
|
|
57
|
+
pick_node() {
|
|
58
|
+
if [ -n "${DEN_SERVICE_NODE:-}" ]; then
|
|
59
|
+
[ -x "$DEN_SERVICE_NODE" ] || { echo "✗ DEN_SERVICE_NODE ($DEN_SERVICE_NODE) is not executable" >&2; exit 1; }
|
|
60
|
+
echo "$DEN_SERVICE_NODE"; return
|
|
61
|
+
fi
|
|
62
|
+
local node; node="$(command -v node)" || { echo "✗ node is not on PATH" >&2; exit 1; }
|
|
63
|
+
case "$node" in
|
|
64
|
+
*/.nvm/*|*/fnm/*|*/.fnm/*|*/.volta/*|*/.asdf/*|*/mise/*) ;;
|
|
65
|
+
*) echo "$node"; return ;;
|
|
66
|
+
esac
|
|
67
|
+
local want c; want="$(node_major "$node")"
|
|
68
|
+
for c in $STABLE_NODES; do
|
|
69
|
+
if [ -x "$c" ] && [ "$(node_major "$c")" = "$want" ]; then echo "$c"; return; fi
|
|
70
|
+
done
|
|
71
|
+
# Warn when installing; `print` writes the plist and nothing else.
|
|
72
|
+
if [ "${1:-}" = install ]; then
|
|
73
|
+
echo "⚠ the service will run $node, a version manager's node: it stops starting" >&2
|
|
74
|
+
echo " when that version is removed. Re-run install after switching, or set DEN_SERVICE_NODE." >&2
|
|
75
|
+
fi
|
|
76
|
+
echo "$node"
|
|
77
|
+
}
|
|
78
|
+
|
|
50
79
|
write_plist() { # $1 = output file
|
|
51
|
-
NODE="$(
|
|
80
|
+
NODE="$(pick_node "$MODE")"
|
|
52
81
|
TSX="$(find_tsx)" || { echo "✗ tsx not installed: run npm ci in $DIST" >&2; exit 1; }
|
|
53
82
|
DOCKER_DIR="$(dirname "$(command -v docker || echo /usr/local/bin/docker)")"
|
|
54
83
|
SVC_PATH="$(dirname "$NODE"):$DOCKER_DIR:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
|
@@ -79,7 +108,8 @@ else:
|
|
|
79
108
|
PY
|
|
80
109
|
}
|
|
81
110
|
|
|
82
|
-
|
|
111
|
+
MODE="${1:-install}"
|
|
112
|
+
case "$MODE" in
|
|
83
113
|
install)
|
|
84
114
|
mkdir -p "$LOGS" "$(dirname "$PLIST")"
|
|
85
115
|
write_plist "$PLIST.new"
|
|
@@ -115,6 +145,15 @@ case "${1:-install}" in
|
|
|
115
145
|
write_plist -
|
|
116
146
|
;;
|
|
117
147
|
status)
|
|
148
|
+
# The node the agent was installed with may be gone (a removed nvm
|
|
149
|
+
# version): launchd then restarts it forever with no useful message.
|
|
150
|
+
if [ -f "$PLIST" ] && command -v plutil >/dev/null 2>&1; then
|
|
151
|
+
svc_node="$(plutil -extract ProgramArguments.0 raw -o - "$PLIST" 2>/dev/null || true)"
|
|
152
|
+
if [ -n "$svc_node" ] && [ ! -x "$svc_node" ]; then
|
|
153
|
+
echo "✗ $LABEL runs $svc_node, which no longer exists: re-run service install" >&2
|
|
154
|
+
exit 1
|
|
155
|
+
fi
|
|
156
|
+
fi
|
|
118
157
|
# launchctl's own "Could not find service" is noise when it isn't loaded.
|
|
119
158
|
if out="$(launchctl print "$DOMAIN/$LABEL" 2>/dev/null)"; then
|
|
120
159
|
printf '%s\n' "$out" | grep -E '^\s*(state|pid|last exit code|runs|spawn type)'
|
|
@@ -12,8 +12,20 @@ export const AUDIT_HEADERS = {
|
|
|
12
12
|
clientVersion: "x-den-mcp-client-version",
|
|
13
13
|
pid: "x-den-mcp-pid",
|
|
14
14
|
session: "x-den-mcp-session",
|
|
15
|
+
/** A scoped server's actor id (`agent:<id>`, AG-2a-core). */
|
|
16
|
+
agent: "x-den-mcp-agent",
|
|
17
|
+
/** The workspace a scoped server is pinned to (AG-2a-core). */
|
|
18
|
+
workspace: "x-den-mcp-workspace",
|
|
15
19
|
} as const;
|
|
16
20
|
|
|
21
|
+
/**
|
|
22
|
+
* `agent:<id>`: what `<cli> mcp --actor` takes. The id is letters, digits,
|
|
23
|
+
* dots, dashes and underscores, 1-64 long, starting with a letter or digit.
|
|
24
|
+
*/
|
|
25
|
+
export const AGENT_ACTOR_RE = /^agent:[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
26
|
+
/** A workspace name, as the config accepts it (1-64 long here). */
|
|
27
|
+
export const WORKSPACE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
28
|
+
|
|
17
29
|
export interface AgentInfo {
|
|
18
30
|
/** The MCP client's self-reported name (clientInfo.name). */
|
|
19
31
|
client: string;
|
|
@@ -23,6 +35,10 @@ export interface AgentInfo {
|
|
|
23
35
|
mcpPid: number | null;
|
|
24
36
|
/** The MCP server's session id (one per `<cli> mcp` process). */
|
|
25
37
|
session: string;
|
|
38
|
+
/** A scoped server's actor (`agent:<id>`); absent when unscoped. */
|
|
39
|
+
agent?: string;
|
|
40
|
+
/** The workspace a scoped server is pinned to; absent when unscoped. */
|
|
41
|
+
workspace?: string;
|
|
26
42
|
}
|
|
27
43
|
|
|
28
44
|
export interface AgentActor extends AgentInfo {
|
|
@@ -49,6 +65,13 @@ export function agentAuditHeaders(info: AgentInfo): Record<string, string> {
|
|
|
49
65
|
[AUDIT_HEADERS.clientVersion]: clean(info.clientVersion) || "unknown",
|
|
50
66
|
[AUDIT_HEADERS.pid]: info.mcpPid === null ? "" : String(info.mcpPid),
|
|
51
67
|
[AUDIT_HEADERS.session]: clean(info.session).replace(/[^A-Za-z0-9-]/g, ""),
|
|
68
|
+
// Only a scoped server sends these: an unscoped one's headers are as before.
|
|
69
|
+
...(info.agent && AGENT_ACTOR_RE.test(info.agent)
|
|
70
|
+
? { [AUDIT_HEADERS.agent]: info.agent }
|
|
71
|
+
: {}),
|
|
72
|
+
...(info.workspace && WORKSPACE_NAME_RE.test(info.workspace)
|
|
73
|
+
? { [AUDIT_HEADERS.workspace]: info.workspace }
|
|
74
|
+
: {}),
|
|
52
75
|
};
|
|
53
76
|
}
|
|
54
77
|
|
|
@@ -59,19 +82,31 @@ export function actorFromHeaders(
|
|
|
59
82
|
const h = headers as Record<string, string | string[] | undefined>;
|
|
60
83
|
if (clean(h[AUDIT_HEADERS.actor]) !== "agent") return null;
|
|
61
84
|
const pid = clean(h[AUDIT_HEADERS.pid], 10);
|
|
85
|
+
// Kept only when well-formed: a malformed value is dropped, never cleaned
|
|
86
|
+
// into something that looks like another agent's id.
|
|
87
|
+
const agent = clean(h[AUDIT_HEADERS.agent], 80);
|
|
88
|
+
const workspace = clean(h[AUDIT_HEADERS.workspace], 80);
|
|
62
89
|
return {
|
|
63
90
|
kind: "agent",
|
|
64
91
|
client: clean(h[AUDIT_HEADERS.client]) || "unknown",
|
|
65
92
|
clientVersion: clean(h[AUDIT_HEADERS.clientVersion], 32) || "unknown",
|
|
66
93
|
mcpPid: /^\d+$/.test(pid) ? Number(pid) : null,
|
|
67
94
|
session: clean(h[AUDIT_HEADERS.session]).replace(/[^A-Za-z0-9-]/g, ""),
|
|
95
|
+
...(AGENT_ACTOR_RE.test(agent) ? { agent } : {}),
|
|
96
|
+
...(WORKSPACE_NAME_RE.test(workspace) ? { workspace } : {}),
|
|
68
97
|
};
|
|
69
98
|
}
|
|
70
99
|
|
|
71
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* "agent <client> <version> (mcp pid <pid>, session <id>)"; a scoped
|
|
102
|
+
* server's actor leads: "agent:<id> <client> <version> (workspace <ws>,
|
|
103
|
+
* mcp pid <pid>, session <id>)".
|
|
104
|
+
*/
|
|
72
105
|
export function actorLabel(actor: AgentActor): string {
|
|
106
|
+
const who = actor.agent ?? "agent";
|
|
107
|
+
const ws = actor.workspace ? `workspace ${actor.workspace}, ` : "";
|
|
73
108
|
return (
|
|
74
|
-
|
|
75
|
-
`(mcp pid ${actor.mcpPid ?? "?"}, session ${actor.session || "?"})`
|
|
109
|
+
`${who} ${actor.client} ${actor.clientVersion} ` +
|
|
110
|
+
`(${ws}mcp pid ${actor.mcpPid ?? "?"}, session ${actor.session || "?"})`
|
|
76
111
|
);
|
|
77
112
|
}
|