@monster-orange/den 0.4.18 → 0.4.20
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 +160 -0
- package/README.md +14 -13
- package/docs/distributions.md +148 -30
- package/docs/reference.md +1 -1
- package/package.json +2 -1
- package/skills/install-den/SKILL.md +35 -75
- package/src/shared/agents/den-claude.ts +40 -6
- package/src/shared/agents/guardrails/stop-hook.ts +30 -4
- package/src/shared/agents/guardrails.ts +40 -4
- package/src/shared/agents/handback.ts +22 -4
- package/src/shared/agents/personas.ts +32 -5
- package/src/shared/agents/runtime.ts +8 -0
- package/src/shared/agents/server-agent.ts +57 -0
- package/src/shared/agents/server-pack/server.md +45 -0
- package/src/shared/environment-manager/workspaces.ts +4 -0
- package/src/shared/secrets/index.ts +71 -3
- package/src/shared/secrets/onepassword.ts +87 -14
- package/src/shared/secrets/op-token.ts +270 -0
- package/src/shared/secrets/secret-env.ts +19 -6
- package/src/shared/secrets/strategies.ts +6 -0
- package/src/shared/secrets/types.ts +8 -0
- package/src/ui/cli/agent-ops.ts +41 -6
- package/src/ui/cli/create.ts +2 -2
- package/src/ui/cli/doctor-command.ts +54 -2
- package/src/ui/cli/index.ts +1 -1
- package/src/ui/cli/repo-command.ts +8 -1
- package/src/ui/cli/repo-detect.ts +58 -0
- package/src/ui/cli/server-agent-ops.ts +411 -0
- package/src/ui/mcp/server.ts +29 -12
- package/src/ui/mcp/tools.ts +4 -2
- package/src/ui/server/agent-routes.ts +1 -1
- package/src/ui/server/held-actions.ts +35 -7
- package/src/ui/server/index.ts +25 -6
- package/src/ui/server/public/agents-page.js +10 -1
- package/src/ui/server/public/error-page.js +39 -1
- package/src/ui/server/public/graph.css +21 -0
- package/src/ui/server/public/graph.js +49 -6
- package/src/ui/server/public/header.js +29 -7
- package/src/ui/server/public/i18n/es.js +38 -0
- package/src/ui/server/public/i18n/ja.js +40 -0
- package/src/ui/server/public/local-dev.html +22 -1
- package/src/ui/server/public/persona-ui.css +6 -0
- package/src/ui/server/public/server-agent-page.js +415 -0
- package/src/ui/server/public/server-agent.html +222 -0
- package/src/ui/server/public/settings-registry.js +22 -0
- package/src/ui/server/public/shell.js +67 -6
- package/src/ui/server/public/svc-panel.js +27 -14
- package/src/ui/server/public/topbar.js +13 -1
- package/src/ui/server/route-classes.ts +21 -0
- package/src/ui/server/server-agent-routes.ts +176 -0
- package/src/ui/server/session-routes.ts +12 -1
- package/src/ui/server/ui-assets.ts +2 -0
- package/src/ui/server/workspace-routes.ts +3 -0
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: install-den
|
|
3
|
-
description: Install
|
|
3
|
+
description: Install Den, Monster Orange's local dev orchestrator, and hand the person over to Den's own agent, in five minutes. Use when the person says "set up Den", "install Den", "get me started with Den", or pastes a link to this file.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Install Den
|
|
7
7
|
|
|
8
|
-
You are
|
|
9
|
-
|
|
8
|
+
You are installing Den on this person's machine. Den runs their repos locally and shows them in a browser. Your job
|
|
9
|
+
is short: install it, name it, start it, and hand over the link. **Den's own agent takes it from there.** It adds
|
|
10
|
+
their repos and starts their services, with the person allowing each change in the browser.
|
|
11
|
+
|
|
12
|
+
You do three things only. You don't add repos or create workspaces: those are Den's own agent's job. Keep your
|
|
13
|
+
messages short and plain.
|
|
10
14
|
|
|
11
15
|
## Hard rules
|
|
12
16
|
|
|
13
17
|
- **Never ask for a token, password or secret in chat.** If a step needs one, tell the person which command to run
|
|
14
18
|
themselves.
|
|
15
|
-
- **Never edit the person's repos.**
|
|
16
|
-
- **Send nothing off this machine.** No uploads, no telemetry
|
|
17
|
-
|
|
18
|
-
- **
|
|
19
|
-
own prompt (in the terminal or the browser), and the person types it there.
|
|
19
|
+
- **Never edit the person's repos.** Don't touch them at all.
|
|
20
|
+
- **Send nothing off this machine.** No uploads, no telemetry. The only download is Den from npm.
|
|
21
|
+
- **Sudo is the person's job.** Never run `sudo` yourself.
|
|
22
|
+
- **Never open the setup link yourself**, and never fetch it. It signs in one browser, once. Give it to the person.
|
|
20
23
|
- Every step below is safe to re-run. If you are unsure whether a step ran, run it again.
|
|
21
24
|
|
|
22
|
-
## 1.
|
|
25
|
+
## 1. Install Den, after checking Node and git
|
|
23
26
|
|
|
24
27
|
Run these and read the results. Don't install anything without saying so first.
|
|
25
28
|
|
|
@@ -27,88 +30,45 @@ Run these and read the results. Don't install anything without saying so first.
|
|
|
27
30
|
(for example `brew install node`, or `nvm install 22`).
|
|
28
31
|
2. `npm --version`: needs 9 or later. It comes with Node.
|
|
29
32
|
3. `git --version`: needs to be there.
|
|
30
|
-
4. `docker info`: only matters if the repo runs in containers (it has a `docker-compose.yml` or `compose.yaml`).
|
|
31
|
-
If Docker is needed and not running, ask them to start it.
|
|
32
33
|
|
|
33
34
|
If anything is missing, stop here, tell them what and how to fix it, and wait.
|
|
34
35
|
|
|
35
|
-
## 2.
|
|
36
|
-
|
|
37
|
-
Ask them together, each with a default, so the person can just say "yes":
|
|
36
|
+
## 2. Pick the folder and the name
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
2. **Which repo should we start with?** Default: the folder you're in now. A local path or a git URL.
|
|
41
|
-
3. **What should we call your Den?** Default: `den`. Lowercase letters, numbers and dashes. This also becomes the
|
|
42
|
-
command name, so `acme` gives you `./bin/acme`.
|
|
38
|
+
Ask both together, each with a default, so the person can just say "yes":
|
|
43
39
|
|
|
44
|
-
|
|
40
|
+
1. **What should we call your Den?** Default: `den`. Lowercase letters, numbers and dashes. This also becomes its
|
|
41
|
+
command, so `acme` gives you `./bin/acme`.
|
|
42
|
+
2. **Where should it live?** Default: `~/<name>`. It must be a new or empty folder.
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
Below, `<name>` is the name and `<dir>` the folder.
|
|
47
45
|
|
|
48
46
|
Every Den command below takes `--json`. It then prints one JSON object on stdout and its log on stderr. Read `ok`
|
|
49
47
|
first. When `ok` is false, the object says why (`error`) and either a command to run (`fix`) or a question for the
|
|
50
|
-
person (`ask
|
|
48
|
+
person (`ask`).
|
|
51
49
|
|
|
52
|
-
1. `npx @monster-orange/den create <name> <dir> --yes --json`. It answers `{ok, dir, cli, existed, port
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
(`portPicked`). Remember the port for the hand-back.
|
|
50
|
+
1. `npx @monster-orange/den@latest create <name> <dir> --yes --json`. It answers `{ok, dir, cli, existed, port}`. If the
|
|
51
|
+
folder already holds this Den, it answers `ok` with `existed: true` and changes nothing. When another Den already
|
|
52
|
+
uses the usual port, it picks the next free one and says so (`portPicked`).
|
|
56
53
|
2. `cd <dir> && npm install`
|
|
57
54
|
3. From here on, the Den command is `./bin/<name>`, run from `<dir>`.
|
|
58
55
|
|
|
59
|
-
##
|
|
60
|
-
|
|
61
|
-
1. `./bin/<name>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
`./bin/<name> repo add <repo> --run "<their command>" --json`.
|
|
71
|
-
3. The answer names the repo (`repo`), the workspace it joined (`workspace`) and where its manifest is (`manifest`).
|
|
72
|
-
A workspace is Den's name for a set of repos that run together. The first one is called `default`.
|
|
73
|
-
|
|
74
|
-
## 5. Start it and check it
|
|
75
|
-
|
|
76
|
-
1. `./bin/<name> up <repo> --json`. `<repo>` is the `repo` value from step 4. Each service comes back with a
|
|
77
|
-
`result` (`started` or `already-running`) and a `health` (`ok`, or `unknown` when it has no health check).
|
|
78
|
-
2. `./bin/<name> web up --no-open --json`. This starts the browser UI, and answers `{ok, url, port}`. The MCP server
|
|
79
|
-
reads through it.
|
|
80
|
-
3. `./bin/<name> doctor --json`. Each check comes back as `ok`, `warn` or `fail`. The checks are Node, git, Docker
|
|
81
|
-
(only if a repo needs it), the web server (with its `url` and `port`) and Den's MCP. Fix every `fail` before you hand back.
|
|
82
|
-
|
|
83
|
-
## 6. Connect yourself to Den's MCP
|
|
84
|
-
|
|
85
|
-
1. `./bin/<name> mcp connect --runtime <you> --json`. `<you>` is `claude`, `codex`, `gemini` or `grok`. For any other
|
|
86
|
-
agent, `./bin/<name> mcp config` prints the JSON to add by hand.
|
|
87
|
-
2. `./bin/<name> mcp clients` shows whether you are connected.
|
|
88
|
-
3. If the answer has `restart: true`, you load the new MCP server only when you start again. Tell the person to
|
|
89
|
-
restart you once.
|
|
90
|
-
|
|
91
|
-
## 7. Hand back
|
|
92
|
-
|
|
93
|
-
Tell the person, in a few short sentences. Explain every name you use the first time (for example, "a workspace is a
|
|
94
|
-
set of repos that run together").
|
|
95
|
-
|
|
96
|
-
- **Open:** the `url` from `web up` or from the doctor's `web` check, for example `http://localhost:<port>`. Never
|
|
97
|
-
assume a port: use the one Den gave you.
|
|
98
|
-
- **What's running:** each service from `up`, and whether it's healthy.
|
|
99
|
-
- **Your Den:** where it lives (`<dir>`) and its command (`./bin/<name>`).
|
|
100
|
-
- **Decisions, if any:** as numbered yes/no questions, for example "1. Restart me now so I can use Den's tools?"
|
|
101
|
-
- **Three next things we can do together:**
|
|
102
|
-
1. Add another repo.
|
|
103
|
-
2. Look at a service's logs, or run its tests.
|
|
104
|
-
3. Start an agent session on this repo from the browser.
|
|
56
|
+
## 3. Start the web server and hand over the link
|
|
57
|
+
|
|
58
|
+
1. `./bin/<name> web up --no-open --json`. It answers `{ok, url, port}`.
|
|
59
|
+
2. `./bin/<name> web claim-link --json`. It answers `{url, expiresAt}`: a one-time setup link that signs the
|
|
60
|
+
person's browser in, so they can allow Den's agent's changes. It lasts 30 minutes and works once.
|
|
61
|
+
3. Hand over, in a few short sentences:
|
|
62
|
+
- **Open** the `url` from `claim-link`, in your browser on this machine. "Den's own agent takes it from there."
|
|
63
|
+
- **Your Den** lives in `<dir>`, and its command is `./bin/<name>`.
|
|
64
|
+
- If the link has expired, run `./bin/<name> web claim-link` yourself for a new one.
|
|
65
|
+
|
|
66
|
+
That's all. Don't go on to add repos.
|
|
105
67
|
|
|
106
68
|
## When something fails
|
|
107
69
|
|
|
108
70
|
1. Read the JSON. If it has `fix`, run that command, then re-run the step that failed.
|
|
109
|
-
2. If it has `ask`, the person has to decide or act
|
|
110
|
-
question, show any `choices` numbered, and wait.
|
|
71
|
+
2. If it has `ask`, the person has to decide or act. Ask them the question, show any `choices` numbered, and wait.
|
|
111
72
|
3. If there is neither, ask the person **one** clear question, with your best guess as the default.
|
|
112
|
-
4.
|
|
113
|
-
5. If you're stuck after two tries, stop. Tell them what you ran, what it said, and where Den writes its logs
|
|
73
|
+
4. If you're stuck after two tries, stop. Tell them what you ran, what it said, and where Den writes its logs
|
|
114
74
|
(`~/.<name>/`), so they can get help.
|
|
@@ -48,10 +48,13 @@
|
|
|
48
48
|
import { randomUUID } from "crypto";
|
|
49
49
|
import * as fs from "fs";
|
|
50
50
|
import * as path from "path";
|
|
51
|
-
import { configuredRuntimeBin } from "../den-config.js";
|
|
51
|
+
import { MCP_ACTIONS, configuredRuntimeBin } from "../den-config.js";
|
|
52
52
|
import { rolePrompt } from "./personas.js";
|
|
53
53
|
import { GUARDRAILS_DIR, guardrailHooks, hookCommands } from "./guardrails.js";
|
|
54
54
|
import { DEFAULT_ROTATE } from "./rotation.js";
|
|
55
|
+
import { idsInNames } from "./handback.js";
|
|
56
|
+
import { isServerScope } from "./server-agent.js";
|
|
57
|
+
import { product } from "../product.js";
|
|
55
58
|
import {
|
|
56
59
|
LaunchError,
|
|
57
60
|
mcpConfigJson,
|
|
@@ -140,6 +143,25 @@ export const DEN_CLAUDE_PERMISSIONS: RuntimePermissions = {
|
|
|
140
143
|
commandRules: COMMAND_RULES,
|
|
141
144
|
};
|
|
142
145
|
|
|
146
|
+
/**
|
|
147
|
+
* ON-6, the server agent: Den's own MCP server's tools as Claude Code
|
|
148
|
+
* permission rules (https://code.claude.com/docs/en/iam, MCP rules:
|
|
149
|
+
* `mcp__<server>` matches every tool of that server, `mcp__<server>__*`
|
|
150
|
+
* the same with a wildcard). `ask` keeps the org's immediate actions
|
|
151
|
+
* (mcp.actions: they run at once, with no browser Allow) asking in the
|
|
152
|
+
* terminal: Claude Code checks ask before allow. `server` is the name in
|
|
153
|
+
* the --mcp-config file.
|
|
154
|
+
*/
|
|
155
|
+
export function denToolRules(server: string): {
|
|
156
|
+
allow: string[];
|
|
157
|
+
ask: string[];
|
|
158
|
+
} {
|
|
159
|
+
return {
|
|
160
|
+
allow: [`mcp__${server}`, `mcp__${server}__*`],
|
|
161
|
+
ask: MCP_ACTIONS.map((a) => `mcp__${server}__${a}`),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
143
165
|
/** A command prefix's allow rules (permissions.ts checked it). */
|
|
144
166
|
export function allowRules(commands: readonly string[]): string[] {
|
|
145
167
|
return commands.flatMap((c) =>
|
|
@@ -336,13 +358,24 @@ function common(input: LaunchInput): {
|
|
|
336
358
|
const hooks = input.persona.guardrails?.hooks
|
|
337
359
|
? guardrailHooks(
|
|
338
360
|
GUARDRAILS_DIR,
|
|
339
|
-
hookCommands(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
361
|
+
hookCommands(
|
|
362
|
+
GUARDRAILS_DIR,
|
|
363
|
+
process.execPath,
|
|
364
|
+
{
|
|
365
|
+
files: input.filesDir,
|
|
366
|
+
after: input.rotateAfter ?? DEFAULT_ROTATE.afterCompactions,
|
|
367
|
+
},
|
|
368
|
+
// A Den named "den-t2" says "Den T2": a name, not an ID.
|
|
369
|
+
idsInNames([product().title, input.workspace.name]),
|
|
370
|
+
// ON-6: the server agent talks with the person; it hands nothing
|
|
371
|
+
// back, so its messages get the plain-writing check only.
|
|
372
|
+
!isServerScope(input.workspace.name),
|
|
373
|
+
),
|
|
343
374
|
)
|
|
344
375
|
: null;
|
|
345
|
-
const
|
|
376
|
+
const den = input.denToolsAllowed ? denToolRules(input.mcp.name) : null;
|
|
377
|
+
const allow = [...allowRules(input.allow ?? []), ...(den?.allow ?? [])];
|
|
378
|
+
const ask = den?.ask ?? [];
|
|
346
379
|
const settings =
|
|
347
380
|
input.stateDir || hooks || allow.length ? settingsPath(input) : null;
|
|
348
381
|
return {
|
|
@@ -372,6 +405,7 @@ function common(input: LaunchInput): {
|
|
|
372
405
|
? {
|
|
373
406
|
permissions: {
|
|
374
407
|
...(allow.length ? { allow } : {}),
|
|
408
|
+
...(ask.length ? { ask } : {}),
|
|
375
409
|
...(input.stateDir
|
|
376
410
|
? {
|
|
377
411
|
deny: stateDirDenyRules(
|
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
* (stop_hook_active), it lets the turn end, so it never loops. Any error
|
|
15
15
|
* lets the turn end too.
|
|
16
16
|
*
|
|
17
|
+
* `--no-handback` (the server agent, ON-6): it talks with the person and
|
|
18
|
+
* hands no work back, so even a message with the Hand-back heading gets
|
|
19
|
+
* only the plain-writing check.
|
|
20
|
+
*
|
|
21
|
+
* `--known <word>` (repeated): ID-shaped words in the names Den gives
|
|
22
|
+
* (its title, the workspace), say "T2" in "Den T2". They are names, never
|
|
23
|
+
* flagged as bare IDs.
|
|
24
|
+
*
|
|
17
25
|
* Session rotation (den#4): run with `--files <agent files dir> --after
|
|
18
26
|
* <n>`, while a rotation is due (rotation.ts) and handoff.md is missing or
|
|
19
27
|
* fails the handoff check, it blocks once with what to write. Once the
|
|
@@ -54,11 +62,23 @@ function rotationBlock(
|
|
|
54
62
|
});
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
/** The `--known <word>` values in the hook's arguments. */
|
|
66
|
+
export function knownArgs(argv: readonly string[]): string[] {
|
|
67
|
+
const out: string[] = [];
|
|
68
|
+
argv.forEach((a, i) => {
|
|
69
|
+
if (a === "--known" && argv[i + 1]) out.push(argv[i + 1]);
|
|
70
|
+
});
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
/** The hook's stdout for one Stop input ("" lets the turn end). */
|
|
58
75
|
export function stopDecision(
|
|
59
76
|
input: unknown,
|
|
60
77
|
rotation?: RotationHookArgs | null,
|
|
78
|
+
knownNames: readonly string[] = [],
|
|
79
|
+
handbackCheck: boolean = true,
|
|
61
80
|
): string {
|
|
81
|
+
const known = new Set(knownNames);
|
|
62
82
|
if (!input || typeof input !== "object") return "";
|
|
63
83
|
const i = input as {
|
|
64
84
|
stop_hook_active?: unknown;
|
|
@@ -74,10 +94,10 @@ export function stopDecision(
|
|
|
74
94
|
if (typeof msg !== "string") return "";
|
|
75
95
|
// A hand-back is checked whole. Any other final message is what the
|
|
76
96
|
// person reads too, so it gets the plain-writing check: no bare IDs.
|
|
77
|
-
const handback = looksLikeHandback(msg);
|
|
97
|
+
const handback = handbackCheck && looksLikeHandback(msg);
|
|
78
98
|
const problems = handback
|
|
79
|
-
? checkHandback(msg).problems
|
|
80
|
-
: bareIds(msg).map(
|
|
99
|
+
? checkHandback(msg, known).problems
|
|
100
|
+
: bareIds(msg, new Set(), known).map(
|
|
81
101
|
(id) =>
|
|
82
102
|
`"${id}" is not explained. Say what it is, e.g. "${id} (what it is)".`,
|
|
83
103
|
);
|
|
@@ -98,7 +118,13 @@ async function main(): Promise<void> {
|
|
|
98
118
|
for await (const chunk of process.stdin) text += String(chunk);
|
|
99
119
|
let out = "";
|
|
100
120
|
try {
|
|
101
|
-
|
|
121
|
+
const argv = process.argv.slice(2);
|
|
122
|
+
out = stopDecision(
|
|
123
|
+
JSON.parse(text),
|
|
124
|
+
parseHookArgs(argv),
|
|
125
|
+
knownArgs(argv),
|
|
126
|
+
!argv.includes("--no-handback"),
|
|
127
|
+
);
|
|
102
128
|
} catch {
|
|
103
129
|
out = "";
|
|
104
130
|
}
|
|
@@ -50,9 +50,34 @@ export const GUARDRAILS_DIR = enginePath(
|
|
|
50
50
|
"guardrails",
|
|
51
51
|
);
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
export
|
|
55
|
-
|
|
53
|
+
/** What the server agent reads in place of "Handing work back" (ON-6). */
|
|
54
|
+
export const NO_HANDBACK_SECTION = `### Talking with the person
|
|
55
|
+
|
|
56
|
+
You talk with the person as you go; there is no work to hand back, so don't
|
|
57
|
+
write a hand-back. End each turn with a plain sentence or one question.
|
|
58
|
+
Every message you end a turn with is read by a person: explain each ID in it.`;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The shared persona instructions, as Den ships them. `handback: false`
|
|
62
|
+
* (the server agent, ON-6) swaps the "Handing work back" section, a build
|
|
63
|
+
* agent's template (URL, Branch, yes/no decisions), for NO_HANDBACK_SECTION.
|
|
64
|
+
*/
|
|
65
|
+
export function sharedInstructions(
|
|
66
|
+
dir: string = GUARDRAILS_DIR,
|
|
67
|
+
opts: { handback?: boolean } = {},
|
|
68
|
+
): string {
|
|
69
|
+
const text = fs
|
|
70
|
+
.readFileSync(path.join(dir, "instructions.md"), "utf-8")
|
|
71
|
+
.trim();
|
|
72
|
+
if (opts.handback !== false) return text;
|
|
73
|
+
const at = text.indexOf("### Handing work back");
|
|
74
|
+
if (at < 0) return text;
|
|
75
|
+
const next = text.indexOf("\n### ", at + 1);
|
|
76
|
+
return (
|
|
77
|
+
text.slice(0, at) +
|
|
78
|
+
NO_HANDBACK_SECTION +
|
|
79
|
+
(next < 0 ? "" : `\n${text.slice(next)}`)
|
|
80
|
+
).trim();
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
/** One argument for a POSIX shell, single-quoted when it needs it. */
|
|
@@ -88,6 +113,10 @@ export function hookCommands(
|
|
|
88
113
|
dir: string = GUARDRAILS_DIR,
|
|
89
114
|
node: string = process.execPath,
|
|
90
115
|
rotation?: RotationArgs,
|
|
116
|
+
/** ID-shaped words in Den's own names, for the Stop hook (stop-hook.ts). */
|
|
117
|
+
known: readonly string[] = [],
|
|
118
|
+
/** false: the Stop hook never runs the hand-back check (the server agent). */
|
|
119
|
+
handback: boolean = true,
|
|
91
120
|
): HookCommands {
|
|
92
121
|
const more = (c: EngineCommand): EngineCommand =>
|
|
93
122
|
rotation
|
|
@@ -108,7 +137,14 @@ export function hookCommands(
|
|
|
108
137
|
args: [path.join(dir, "pretooluse.mjs")],
|
|
109
138
|
}),
|
|
110
139
|
stop: commandLine(
|
|
111
|
-
|
|
140
|
+
((c: EngineCommand): EngineCommand => ({
|
|
141
|
+
command: c.command,
|
|
142
|
+
args: [
|
|
143
|
+
...c.args,
|
|
144
|
+
...known.flatMap((k) => ["--known", k]),
|
|
145
|
+
...(handback ? [] : ["--no-handback"]),
|
|
146
|
+
],
|
|
147
|
+
}))(more(scriptEntrypoint("src/shared/agents/guardrails/stop-hook.ts"))),
|
|
112
148
|
),
|
|
113
149
|
rotation: commandLine(
|
|
114
150
|
more(scriptEntrypoint("src/shared/agents/guardrails/rotation-hook.ts")),
|
|
@@ -151,10 +151,25 @@ export function isGlossed(line: string, start: number, end: number): boolean {
|
|
|
151
151
|
return false;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* The ID-shaped words in names Den itself gives (the product's title, the
|
|
156
|
+
* workspace): a Den named "den-t2" is "Den T2", and its agent says so.
|
|
157
|
+
* They are names, not IDs to explain.
|
|
158
|
+
*/
|
|
159
|
+
export function idsInNames(names: readonly (string | undefined)[]): string[] {
|
|
160
|
+
const out = new Set<string>();
|
|
161
|
+
for (const n of names)
|
|
162
|
+
for (const re of ID_PATTERNS)
|
|
163
|
+
for (const m of String(n ?? "").matchAll(new RegExp(re.source, "g")))
|
|
164
|
+
out.add(m[0]);
|
|
165
|
+
return [...out];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Bare IDs in the text, in order, each once. `known` are names, never bare. */
|
|
155
169
|
export function bareIds(
|
|
156
170
|
text: string,
|
|
157
171
|
skipLines: Set<number> = new Set(),
|
|
172
|
+
known: ReadonlySet<string> = new Set(),
|
|
158
173
|
): string[] {
|
|
159
174
|
const glossed = new Set<string>();
|
|
160
175
|
const bare: string[] = [];
|
|
@@ -172,7 +187,7 @@ export function bareIds(
|
|
|
172
187
|
hits.push({ id: m[0], start: m.index!, end: m.index! + m[0].length });
|
|
173
188
|
hits.sort((a, b) => a.start - b.start);
|
|
174
189
|
for (const h of hits) {
|
|
175
|
-
if (glossed.has(h.id)) continue;
|
|
190
|
+
if (glossed.has(h.id) || known.has(h.id)) continue;
|
|
176
191
|
if (isGlossed(line, h.start, h.end)) glossed.add(h.id);
|
|
177
192
|
else if (!bare.includes(h.id)) bare.push(h.id);
|
|
178
193
|
}
|
|
@@ -208,7 +223,10 @@ function decisionProblems(items: string[]): string[] {
|
|
|
208
223
|
}
|
|
209
224
|
|
|
210
225
|
/** Check a hand-back. Pure: the text in, the problems out. */
|
|
211
|
-
export function checkHandback(
|
|
226
|
+
export function checkHandback(
|
|
227
|
+
text: string,
|
|
228
|
+
known: ReadonlySet<string> = new Set(),
|
|
229
|
+
): HandbackResult {
|
|
212
230
|
const lines = String(text ?? "").split(/\r?\n/);
|
|
213
231
|
const { found, literal } = splitParts(lines);
|
|
214
232
|
const problems: string[] = [];
|
|
@@ -233,7 +251,7 @@ export function checkHandback(text: string): HandbackResult {
|
|
|
233
251
|
`"URL:" has no link. Give the full address, with http:// or https://, or say none if this work has no page.`,
|
|
234
252
|
);
|
|
235
253
|
}
|
|
236
|
-
for (const id of bareIds(lines.join("\n"), literal))
|
|
254
|
+
for (const id of bareIds(lines.join("\n"), literal, known))
|
|
237
255
|
problems.push(
|
|
238
256
|
`"${id}" is not explained. Say what it is, e.g. "${id} (what it is)".`,
|
|
239
257
|
);
|
|
@@ -42,6 +42,11 @@ import { WORKSPACE_NAME_RE } from "../agent-actor.js";
|
|
|
42
42
|
import { checkCommands } from "./permissions.js";
|
|
43
43
|
import { DEN_ROLES, REGISTRY_ID_RE } from "./roles.js";
|
|
44
44
|
import { enginePath } from "../engine.js";
|
|
45
|
+
import {
|
|
46
|
+
SERVER_PACK_DIR,
|
|
47
|
+
SERVER_PERSONA_ID,
|
|
48
|
+
isServerScope,
|
|
49
|
+
} from "./server-agent.js";
|
|
45
50
|
import {
|
|
46
51
|
DEFAULT_PACK_GUARDRAILS,
|
|
47
52
|
PACK_GUARDRAILS,
|
|
@@ -356,6 +361,23 @@ export function loadPersonas(
|
|
|
356
361
|
return all;
|
|
357
362
|
}
|
|
358
363
|
|
|
364
|
+
/**
|
|
365
|
+
* The server agent's persona (ON-6), from Den's own server pack, with the
|
|
366
|
+
* default pack's guardrails (the plain-writing rule among them). Only the
|
|
367
|
+
* server scope loads it, so it is never offered for a workspace.
|
|
368
|
+
*/
|
|
369
|
+
export function serverPersona(): Persona {
|
|
370
|
+
const p = readPack(SERVER_PACK_DIR, "builtin").find(
|
|
371
|
+
(x) => x.id === SERVER_PERSONA_ID,
|
|
372
|
+
);
|
|
373
|
+
if (!p)
|
|
374
|
+
throw new Error(`${SERVER_PACK_DIR}: no "${SERVER_PERSONA_ID}" persona`);
|
|
375
|
+
return {
|
|
376
|
+
...p,
|
|
377
|
+
guardrails: { ...DEFAULT_PACK_GUARDRAILS, ...p.guardrailsSet },
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
359
381
|
/**
|
|
360
382
|
* The role prompt as an agent gets it: who and where it is, what to read
|
|
361
383
|
* first, the persona's own prompt, then Den's shared instructions
|
|
@@ -368,14 +390,19 @@ export function rolePrompt(
|
|
|
368
390
|
workspace: string,
|
|
369
391
|
mcpName: string,
|
|
370
392
|
): string {
|
|
371
|
-
const lines =
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
393
|
+
const lines = isServerScope(workspace)
|
|
394
|
+
? [
|
|
395
|
+
`You are agent ${agentId}, this Den's own server agent, with the ${persona.id} persona. You belong to no workspace.`,
|
|
396
|
+
`Act only through the ${mcpName} MCP server; it serves the whole Den, and each write waits for the person's Allow.`,
|
|
397
|
+
]
|
|
398
|
+
: [
|
|
399
|
+
`You are agent ${agentId} in workspace ${workspace}, with the ${persona.id} persona.`,
|
|
400
|
+
`Act on services, logs and tests only through the ${mcpName} MCP server; it serves this workspace.`,
|
|
401
|
+
];
|
|
375
402
|
if (persona.context.length)
|
|
376
403
|
lines.push(`Read first: ${persona.context.join("; ")}.`);
|
|
377
404
|
const shared = persona.guardrails?.instructions
|
|
378
|
-
? `\n\n${sharedInstructions()}`
|
|
405
|
+
? `\n\n${sharedInstructions(undefined, { handback: !isServerScope(workspace) })}`
|
|
379
406
|
: "";
|
|
380
407
|
return `${lines.join("\n")}\n\n${persona.prompt}${shared}`;
|
|
381
408
|
}
|
|
@@ -107,6 +107,14 @@ export interface LaunchInput {
|
|
|
107
107
|
* role's preset and the extra commands, checked (permissions.ts).
|
|
108
108
|
*/
|
|
109
109
|
allow?: string[];
|
|
110
|
+
/**
|
|
111
|
+
* ON-6: the server agent's own: Den's MCP tools run without the
|
|
112
|
+
* runtime's own prompt. Den's reads only read, and its writes are held
|
|
113
|
+
* for the person's Allow in the browser, so each write asks once there.
|
|
114
|
+
* The org's immediate actions (mcp.actions) still ask in the terminal.
|
|
115
|
+
* Nothing outside Den's MCP server is allowed by it.
|
|
116
|
+
*/
|
|
117
|
+
denToolsAllowed?: boolean;
|
|
110
118
|
/**
|
|
111
119
|
* The new conversation's first prompt (a relaunch from handoff.md).
|
|
112
120
|
* Only a runtime with `firstPrompt: true` is given one.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server agent (ON-6): the Den server's own agent, named after the
|
|
3
|
+
* distribution ("<Product> agent"). It belongs to no workspace. It runs
|
|
4
|
+
* under a reserved scope, `den-server`, so it is a normal Den agent
|
|
5
|
+
* underneath (records, sessions, resume, rotation, hooks) while its MCP
|
|
6
|
+
* server sees the whole Den. It acts only through Den's MCP actions, and
|
|
7
|
+
* each write is held for the person's Allow (ON-3).
|
|
8
|
+
*
|
|
9
|
+
* No workspace may take the reserved name: `workspace create` and the
|
|
10
|
+
* MCP's workspace_create refuse it.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import { enginePath } from "../engine.js";
|
|
15
|
+
|
|
16
|
+
/** The reserved scope the server agent's records and session live in. */
|
|
17
|
+
export const SERVER_SCOPE = "den-server";
|
|
18
|
+
|
|
19
|
+
/** The server agent's id, and its persona's. */
|
|
20
|
+
export const SERVER_AGENT_ID = "server";
|
|
21
|
+
export const SERVER_PERSONA_ID = "server";
|
|
22
|
+
|
|
23
|
+
/** Den's own pack for it, shipped in the package (src/ ships whole). */
|
|
24
|
+
export const SERVER_PACK_DIR = enginePath(
|
|
25
|
+
"src",
|
|
26
|
+
"shared",
|
|
27
|
+
"agents",
|
|
28
|
+
"server-pack",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/** Whether `ws` is the server agent's scope (never a workspace). */
|
|
32
|
+
export function isServerScope(ws: unknown): boolean {
|
|
33
|
+
return ws === SERVER_SCOPE;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The server agent's folder under the state dir: an empty folder it
|
|
38
|
+
* starts in, so it has no checkout to edit and acts through Den's MCP.
|
|
39
|
+
* Made when asked for.
|
|
40
|
+
*/
|
|
41
|
+
export function serverAgentDir(stateDir: string): string {
|
|
42
|
+
const dir = path.join(stateDir, "server-agent");
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
44
|
+
return dir;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Its name: the distribution's, then "agent" ("Acme agent"). */
|
|
48
|
+
export function serverAgentName(title: string): string {
|
|
49
|
+
return `${title} agent`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The refusal for a workspace that would take the reserved name. */
|
|
53
|
+
export function reservedWorkspaceError(ws: string): string | null {
|
|
54
|
+
return isServerScope(ws)
|
|
55
|
+
? `"${ws}" is reserved for the server agent; pick another workspace name`
|
|
56
|
+
: null;
|
|
57
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: server
|
|
3
|
+
category: operations
|
|
4
|
+
description: This Den's own agent. Sets it up and looks after it, through Den's MCP actions.
|
|
5
|
+
permission_mode: ask
|
|
6
|
+
resume_on_restart: true
|
|
7
|
+
context: [onboard_status, the doctor's checks]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
You are this Den's own agent. You belong to no workspace: your scope is
|
|
11
|
+
the whole Den. You act only through Den's MCP tools. You have no checkout:
|
|
12
|
+
don't edit files or run shell commands to change Den or the person's repos.
|
|
13
|
+
|
|
14
|
+
Every write (workspace_create, repo_add, service_try) waits for the person
|
|
15
|
+
to Allow it in Den's browser dialog. Until they do, nothing changes, and
|
|
16
|
+
you only read. Say so the first time you ask for one.
|
|
17
|
+
|
|
18
|
+
## On a fresh Den, set it up
|
|
19
|
+
|
|
20
|
+
1. Call onboard_status to see what is there.
|
|
21
|
+
2. Ask where their repos are, with the folder above the one Den lives in as
|
|
22
|
+
the default. Call repos_detect on it and suggest workspaces. A workspace
|
|
23
|
+
is a set of repos that run together: say so the first time.
|
|
24
|
+
3. Call workspace_create for each workspace the person agrees to. It also
|
|
25
|
+
makes the workspace active, so its services can start.
|
|
26
|
+
4. For each repo, call manifest_draft, then repo_add. Den brings the repo in
|
|
27
|
+
unchanged, with a side-car manifest. If Den can't tell how a repo starts,
|
|
28
|
+
ask the person one question.
|
|
29
|
+
5. Call service_try on one service. If it fails, call doctor and read the
|
|
30
|
+
service's logs. Fix what you can through Den's tools; otherwise tell the
|
|
31
|
+
person the one command to run.
|
|
32
|
+
6. Finish with "Here's what's running, and three things to try next.", then
|
|
33
|
+
the services and their health, then three numbered things to try.
|
|
34
|
+
|
|
35
|
+
## After setup
|
|
36
|
+
|
|
37
|
+
The person comes back to you for anything about the server itself: add a
|
|
38
|
+
repo, fix a failing service, change a setting, explain what's running.
|
|
39
|
+
|
|
40
|
+
## Never
|
|
41
|
+
|
|
42
|
+
- Never ask for a token, password or secret in chat. Name the command the
|
|
43
|
+
person runs themselves.
|
|
44
|
+
- Never run sudo. When Den needs the password, it asks in its own prompt.
|
|
45
|
+
- Never edit the person's repos.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import * as fs from "fs";
|
|
13
|
+
import { reservedWorkspaceError } from "../agents/server-agent.js";
|
|
13
14
|
import * as path from "path";
|
|
14
15
|
import * as yaml from "yaml";
|
|
15
16
|
import type {
|
|
@@ -508,6 +509,9 @@ export function setActiveWorkspace(name: string): void {
|
|
|
508
509
|
}
|
|
509
510
|
|
|
510
511
|
export function createWorkspace(name: string, config: WorkspaceConfig): void {
|
|
512
|
+
// ON-6: the server agent's scope is never a workspace.
|
|
513
|
+
const reserved = reservedWorkspaceError(name);
|
|
514
|
+
if (reserved) throw new Error(reserved);
|
|
511
515
|
const file = readWorkspacesFile();
|
|
512
516
|
file.workspaces[name] = config;
|
|
513
517
|
writeWorkspacesFile(file);
|