@monster-orange/den 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +593 -0
  2. package/docs/distributions.md +429 -18
  3. package/package.json +6 -1
  4. package/src/shared/actor-token.ts +275 -0
  5. package/src/shared/agent-actor.ts +12 -2
  6. package/src/shared/agents/default-pack/be-dev.md +20 -0
  7. package/src/shared/agents/default-pack/devops.md +14 -0
  8. package/src/shared/agents/default-pack/fe-dev.md +20 -0
  9. package/src/shared/agents/default-pack/orchestrator.md +16 -0
  10. package/src/shared/agents/default-pack/planner.md +17 -0
  11. package/src/shared/agents/default-pack/reviewer.md +14 -0
  12. package/src/shared/agents/default-pack/ux-designer.md +15 -0
  13. package/src/shared/agents/den-claude.ts +271 -0
  14. package/src/shared/agents/generic.ts +52 -0
  15. package/src/shared/agents/personas.ts +218 -0
  16. package/src/shared/agents/records.ts +118 -0
  17. package/src/shared/agents/runtime.ts +199 -0
  18. package/src/shared/agents/runtimes.ts +54 -0
  19. package/src/shared/den-config.ts +116 -0
  20. package/src/shared/engine.ts +23 -0
  21. package/src/shared/environment-manager/scope.ts +22 -0
  22. package/src/shared/environment-manager/service-runner.ts +94 -2
  23. package/src/shared/environment-manager/workspaces.ts +19 -0
  24. package/src/shared/profile.ts +15 -4
  25. package/src/shared/secret-env.ts +70 -0
  26. package/src/shared/sessions/client.ts +284 -0
  27. package/src/shared/sessions/den-pty.ts +97 -0
  28. package/src/shared/sessions/env.ts +89 -0
  29. package/src/shared/sessions/host.ts +690 -0
  30. package/src/shared/sessions/names.ts +85 -0
  31. package/src/shared/sessions/paths.ts +27 -0
  32. package/src/shared/sessions/protocol.ts +72 -0
  33. package/src/shared/sessions/pty.ts +141 -0
  34. package/src/shared/sessions/ring-buffer.ts +160 -0
  35. package/src/shared/sessions/types.ts +81 -0
  36. package/src/shared/web-secret.ts +181 -0
  37. package/src/shared/web-session.ts +491 -0
  38. package/src/ui/cli/agent-ops.ts +843 -0
  39. package/src/ui/cli/agents-command.ts +294 -0
  40. package/src/ui/cli/doctor-command.ts +138 -9
  41. package/src/ui/cli/env-probe.ts +266 -0
  42. package/src/ui/cli/flag-parser.ts +32 -3
  43. package/src/ui/cli/index.ts +128 -12
  44. package/src/ui/cli/sessions-command.ts +750 -0
  45. package/src/ui/cli/target-workspace.ts +105 -0
  46. package/src/ui/cli/web-command.ts +536 -14
  47. package/src/ui/mcp/api.ts +7 -0
  48. package/src/ui/mcp/scope.ts +123 -19
  49. package/src/ui/mcp/server.ts +100 -7
  50. package/src/ui/mcp/tools.ts +44 -5
  51. package/src/ui/server/agent-routes.ts +323 -0
  52. package/src/ui/server/audit.ts +111 -22
  53. package/src/ui/server/http-errors.ts +21 -3
  54. package/src/ui/server/index.ts +820 -65
  55. package/src/ui/server/public/agents-page.js +648 -0
  56. package/src/ui/server/public/agents-section.js +299 -0
  57. package/src/ui/server/public/agents.html +439 -0
  58. package/src/ui/server/public/appearance.js +3 -0
  59. package/src/ui/server/public/error-page.js +103 -0
  60. package/src/ui/server/public/header.js +45 -11
  61. package/src/ui/server/public/i18n/es.js +90 -1
  62. package/src/ui/server/public/i18n/ja.js +86 -1
  63. package/src/ui/server/public/local-dev.html +53 -0
  64. package/src/ui/server/public/logs.html +3 -0
  65. package/src/ui/server/public/panel.js +1 -1
  66. package/src/ui/server/public/release.html +8 -0
  67. package/src/ui/server/public/settings-page.js +5 -0
  68. package/src/ui/server/public/settings-registry.js +33 -0
  69. package/src/ui/server/public/shell.js +248 -5
  70. package/src/ui/server/public/topbar.js +51 -2
  71. package/src/ui/server/readonly-routes.ts +25 -2
  72. package/src/ui/server/request-guard.ts +670 -0
  73. package/src/ui/server/route-classes.ts +317 -0
  74. package/src/ui/server/session-routes.ts +445 -0
  75. package/src/ui/server/shutdown-guard.ts +91 -0
  76. package/src/ui/server/sudo-store.ts +29 -3
  77. package/src/ui/server/ui-assets.ts +10 -1
  78. package/src/ui/server/websocket.ts +326 -0
  79. package/src/ui/server/workspace-routes.ts +16 -22
@@ -0,0 +1,275 @@
1
+ /**
2
+ * SEC-1c (SEC-1 spec §5): an agent's actor token.
3
+ *
4
+ * `agents start` (and resume) mints `den_at_<43 b64url>`: 32 random bytes,
5
+ * handed to the agent's session as DEN_ACTOR_TOKEN and to nothing else.
6
+ * The state dir keeps only its sha256 (`actor-tokens.json`, 0600, the dir
7
+ * 0700), with what it proves:
8
+ *
9
+ * { actor: "agent:<id>", workspace, grants, session: { name, id } }
10
+ *
11
+ * - One live token per agent: minting another (a resume, a restart)
12
+ * revokes the old one, so it stops working at once.
13
+ * - Revoked when the agent stops (`agents stop`, the page's Stop), and when
14
+ * its session ends: a token verifies only while the session it was minted
15
+ * for is listed as running in the session host's own record
16
+ * (`sessions/<id>.json`); an ended one's token is removed when presented.
17
+ * - It never derives from web.secret, so it proves nothing about the
18
+ * person. `web logout --all` (the "something leaked" button, SEC-1
19
+ * §6c.2) revokes every token as well as rotating the secret: each record
20
+ * is kept, marked `revokedAt`, so a running agent's next call is told
21
+ * `token_revoked` (restart or resume it) rather than a bare 401. Minting
22
+ * the agent's next token, or its session's end, drops the record.
23
+ *
24
+ * Nothing here prints or logs a token.
25
+ */
26
+ import { createHash, randomBytes } from "crypto";
27
+ import * as fs from "fs";
28
+ import * as path from "path";
29
+ import { AGENT_ACTOR_RE, WORKSPACE_NAME_RE } from "./agent-actor.js";
30
+ import { sessionPaths } from "./sessions/paths.js";
31
+
32
+ export const ACTOR_TOKEN_PREFIX = "den_at_";
33
+ /** The agent's session carries its token here (AG spec §5a-env). */
34
+ export const ACTOR_TOKEN_ENV = "DEN_ACTOR_TOKEN";
35
+ /** A person's token in an env: never forwarded to any session. */
36
+ export const PERSON_TOKEN_ENV = "DEN_PERSON_TOKEN";
37
+ export const ACTOR_TOKENS_FILE = "actor-tokens.json";
38
+
39
+ const TOKEN_RE = /^den_at_[A-Za-z0-9_-]{43}$/;
40
+ /** web-secret.ts's PERSON_TOKEN_PREFIX and WEB_SECRET_FILE (it imports this module). */
41
+ const PERSON_PREFIX = "den_pt_";
42
+ const SECRET_FILE = "web.secret";
43
+
44
+ export interface ActorTokenRecord {
45
+ actor: `agent:${string}`;
46
+ workspace: string;
47
+ /** The agent's grants (agents.<id>.grants) when it was minted. */
48
+ grants: string[];
49
+ /** The session it was minted for: its name, and its id once started. */
50
+ session: { name: string; id?: string };
51
+ /** ISO 8601. */
52
+ createdAt: string;
53
+ /** ISO 8601: revoked by `web logout --all`. A revoked token never verifies. */
54
+ revokedAt?: string;
55
+ }
56
+
57
+ interface StoreFile {
58
+ v: 1;
59
+ /** sha256(token), hex → what it proves. */
60
+ tokens: Record<string, ActorTokenRecord>;
61
+ }
62
+
63
+ export function actorTokensPath(stateDir: string): string {
64
+ return path.join(stateDir, ACTOR_TOKENS_FILE);
65
+ }
66
+
67
+ /** The token's key in the store: sha256, hex. */
68
+ export function actorTokenKey(token: string): string {
69
+ return createHash("sha256").update(token).digest("hex");
70
+ }
71
+
72
+ /** Shaped like an actor token (says nothing about whether it is live). */
73
+ export function isActorTokenShape(v: string | undefined): v is string {
74
+ return typeof v === "string" && TOKEN_RE.test(v);
75
+ }
76
+
77
+ function load(stateDir: string): StoreFile {
78
+ try {
79
+ const raw = JSON.parse(
80
+ fs.readFileSync(actorTokensPath(stateDir), "utf-8"),
81
+ ) as Partial<StoreFile>;
82
+ const tokens =
83
+ raw.tokens && typeof raw.tokens === "object" && !Array.isArray(raw.tokens)
84
+ ? raw.tokens
85
+ : {};
86
+ return { v: 1, tokens };
87
+ } catch {
88
+ return { v: 1, tokens: {} };
89
+ }
90
+ }
91
+
92
+ function save(stateDir: string, data: StoreFile): void {
93
+ fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
94
+ const file = actorTokensPath(stateDir);
95
+ const tmp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
96
+ fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
97
+ fs.chmodSync(tmp, 0o600);
98
+ fs.renameSync(tmp, file);
99
+ }
100
+
101
+ /**
102
+ * A fresh token for agent `agentId`, its hash recorded, every older token
103
+ * of the same agent revoked. The token itself is returned once and never
104
+ * stored.
105
+ */
106
+ export function mintActorToken(
107
+ stateDir: string,
108
+ o: {
109
+ agentId: string;
110
+ workspace: string;
111
+ grants?: readonly string[];
112
+ sessionName: string;
113
+ },
114
+ ): { token: string; key: string } {
115
+ const actor = `agent:${o.agentId}` as const;
116
+ if (!AGENT_ACTOR_RE.test(actor))
117
+ throw new Error(`"${o.agentId}" is not an agent id`);
118
+ if (!WORKSPACE_NAME_RE.test(o.workspace))
119
+ throw new Error(`"${o.workspace}" is not a workspace name`);
120
+ const token = ACTOR_TOKEN_PREFIX + randomBytes(32).toString("base64url");
121
+ const key = actorTokenKey(token);
122
+ const data = load(stateDir);
123
+ for (const [k, r] of Object.entries(data.tokens))
124
+ if (r.actor === actor) delete data.tokens[k];
125
+ data.tokens[key] = {
126
+ actor,
127
+ workspace: o.workspace,
128
+ grants: (o.grants ?? []).filter((g) => WORKSPACE_NAME_RE.test(g)),
129
+ session: { name: o.sessionName },
130
+ createdAt: new Date().toISOString(),
131
+ };
132
+ save(stateDir, data);
133
+ return { token, key };
134
+ }
135
+
136
+ /** Bind a minted token to the session id the host gave its session. */
137
+ export function bindActorSession(
138
+ stateDir: string,
139
+ key: string,
140
+ sessionId: string,
141
+ ): void {
142
+ const data = load(stateDir);
143
+ const rec = data.tokens[key];
144
+ if (!rec) return;
145
+ rec.session = { ...rec.session, id: sessionId };
146
+ save(stateDir, data);
147
+ }
148
+
149
+ /** Revoke one token (by its key). */
150
+ export function revokeActorToken(stateDir: string, key: string): boolean {
151
+ const data = load(stateDir);
152
+ if (!data.tokens[key]) return false;
153
+ delete data.tokens[key];
154
+ save(stateDir, data);
155
+ return true;
156
+ }
157
+
158
+ /** Revoke every token of agent `agentId`; how many there were. */
159
+ export function revokeAgentTokens(stateDir: string, agentId: string): number {
160
+ const data = load(stateDir);
161
+ const actor = `agent:${agentId}`;
162
+ let n = 0;
163
+ for (const [k, r] of Object.entries(data.tokens))
164
+ if (r.actor === actor) {
165
+ delete data.tokens[k];
166
+ n++;
167
+ }
168
+ if (n) save(stateDir, data);
169
+ return n;
170
+ }
171
+
172
+ /**
173
+ * Revoke every agent's token (`web logout --all`, SEC-1 §6c.2): each record
174
+ * is marked revoked, so its agent learns why. How many were live.
175
+ */
176
+ export function revokeAllActorTokens(stateDir: string): number {
177
+ const data = load(stateDir);
178
+ const now = new Date().toISOString();
179
+ let n = 0;
180
+ for (const r of Object.values(data.tokens))
181
+ if (!r.revokedAt) {
182
+ r.revokedAt = now;
183
+ n++;
184
+ }
185
+ if (n) save(stateDir, data);
186
+ return n;
187
+ }
188
+
189
+ /**
190
+ * The agents whose token `web logout --all` revoked and whose record is
191
+ * still kept (their session still runs): the Agents tab marks them, so a
192
+ * person knows to restart or resume them (AG-4a-term). Ids only.
193
+ */
194
+ export function revokedAgentIds(stateDir: string): Set<string> {
195
+ const out = new Set<string>();
196
+ for (const r of Object.values(load(stateDir).tokens))
197
+ if (r.revokedAt && AGENT_ACTOR_RE.test(r.actor))
198
+ out.add(r.actor.slice("agent:".length));
199
+ return out;
200
+ }
201
+
202
+ /** What a presented token was minted as, live or not; null when unknown. */
203
+ export function lookupActorToken(
204
+ stateDir: string,
205
+ token: string,
206
+ ): ActorTokenRecord | null {
207
+ if (!isActorTokenShape(token)) return null;
208
+ return load(stateDir).tokens[actorTokenKey(token)] ?? null;
209
+ }
210
+
211
+ /** Whether the session host lists session `id` as running (its own record). */
212
+ export function sessionRunning(stateDir: string, id: string): boolean {
213
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(id)) return false;
214
+ try {
215
+ const rec = JSON.parse(
216
+ fs.readFileSync(
217
+ path.join(sessionPaths(stateDir).dir, `${id}.json`),
218
+ "utf-8",
219
+ ),
220
+ ) as { exited?: unknown };
221
+ return rec.exited !== true;
222
+ } catch {
223
+ return false;
224
+ }
225
+ }
226
+
227
+ /**
228
+ * What a presented token proves: its record; `"revoked"` when `web logout
229
+ * --all` revoked it and its session still runs (the agent is told to
230
+ * restart or resume); or null: unknown, not yet bound to a session, or its
231
+ * session has ended (then its record is removed here).
232
+ */
233
+ export function checkActorToken(
234
+ stateDir: string,
235
+ token: string,
236
+ ): ActorTokenRecord | "revoked" | null {
237
+ const rec = lookupActorToken(stateDir, token);
238
+ if (!rec?.session.id) return null;
239
+ if (!sessionRunning(stateDir, rec.session.id)) {
240
+ try {
241
+ revokeActorToken(stateDir, actorTokenKey(token));
242
+ } catch {
243
+ /* a read-only state dir: still refused */
244
+ }
245
+ return null;
246
+ }
247
+ return rec.revokedAt ? "revoked" : rec;
248
+ }
249
+
250
+ /** The record a presented token proves, or null (checkActorToken, revoked or not). */
251
+ export function verifyActorToken(
252
+ stateDir: string,
253
+ token: string,
254
+ ): ActorTokenRecord | null {
255
+ const r = checkActorToken(stateDir, token);
256
+ return r === "revoked" ? null : r;
257
+ }
258
+
259
+ /**
260
+ * A person's credential in an env (SEC-1 §5): DEN_PERSON_TOKEN, a value
261
+ * shaped like a person token, or a value that names a web.secret file.
262
+ * No session Den starts ever gets one, and `--keep-env` can't forward one.
263
+ */
264
+ export function isPersonCredentialEnv(
265
+ name: string,
266
+ value: string | undefined,
267
+ ): boolean {
268
+ if (name === PERSON_TOKEN_ENV) return true;
269
+ if (value === undefined) return false;
270
+ const v = value.trim();
271
+ if (v.includes(PERSON_PREFIX)) return true;
272
+ return v
273
+ .split(/[:;,\s]+/)
274
+ .some((part) => path.basename(part.replace(/[/\\]+$/, "")) === SECRET_FILE);
275
+ }
@@ -43,6 +43,11 @@ export interface AgentInfo {
43
43
 
44
44
  export interface AgentActor extends AgentInfo {
45
45
  kind: "agent";
46
+ /**
47
+ * SEC-1c: set when this came from headers the caller set, not from an
48
+ * agent's actor token. The audit keeps it as a claim, never as the actor.
49
+ */
50
+ unverified?: true;
46
51
  }
47
52
 
48
53
  /** null: a person in the web UI (no agent headers). */
@@ -75,7 +80,11 @@ export function agentAuditHeaders(info: AgentInfo): Record<string, string> {
75
80
  };
76
81
  }
77
82
 
78
- /** The actor an incoming request names, or null for a person in the UI. */
83
+ /**
84
+ * The actor an incoming request's headers claim, or null. SEC-1c: a claim,
85
+ * never the audit actor; the server's auditActorOf (request-guard.ts)
86
+ * takes the actor from the credential and keeps this as unverified.
87
+ */
79
88
  export function actorFromHeaders(
80
89
  headers: IncomingHttpHeaders | Record<string, string | undefined>,
81
90
  ): Actor {
@@ -107,6 +116,7 @@ export function actorLabel(actor: AgentActor): string {
107
116
  const ws = actor.workspace ? `workspace ${actor.workspace}, ` : "";
108
117
  return (
109
118
  `${who} ${actor.client} ${actor.clientVersion} ` +
110
- `(${ws}mcp pid ${actor.mcpPid ?? "?"}, session ${actor.session || "?"})`
119
+ `(${ws}mcp pid ${actor.mcpPid ?? "?"}, session ${actor.session || "?"})` +
120
+ (actor.unverified ? " (unverified)" : "")
111
121
  );
112
122
  }
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: be-dev
3
+ description: Implements back-end stories, test first, keeping contracts stable.
4
+ context:
5
+ [
6
+ the story and its acceptance criteria,
7
+ the service's API contracts,
8
+ the repo's conventions,
9
+ ]
10
+ ---
11
+
12
+ You are a back-end developer. Implement one story end to end.
13
+
14
+ - Write a failing test for each acceptance criterion first, then make it pass.
15
+ - Keep public contracts backward compatible unless the story says otherwise;
16
+ call out any migration.
17
+ - Validate input at the boundary, handle errors explicitly, and never log
18
+ secrets.
19
+ - Run the repo's checks before opening a pull request, and describe what you
20
+ verified in it.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: devops
3
+ description: Builds and maintains pipelines, environments and service configuration.
4
+ context: [the story, the workspace's service manifests, CI configuration]
5
+ ---
6
+
7
+ You are a devops engineer. Make services easy to build, run and ship.
8
+
9
+ - Prefer declarative configuration and small, reversible changes.
10
+ - Keep secrets in the configured secret store; never write a value into a
11
+ file, a log or a pull request.
12
+ - Prove each change locally (bring the service up, check its health) before
13
+ proposing it.
14
+ - Explain the operational impact and the rollback in the pull request.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: fe-dev
3
+ description: Implements front-end stories, test first, to the design system.
4
+ context:
5
+ [
6
+ the story and its acceptance criteria,
7
+ the design system,
8
+ the front-end conventions of the repo,
9
+ ]
10
+ ---
11
+
12
+ You are a front-end developer. Implement one story end to end.
13
+
14
+ - Write a failing test for each acceptance criterion first, then make it pass.
15
+ - Use the design system's components and tokens; do not invent new ones
16
+ without the ux-designer.
17
+ - Keep the change small and focused on the story. Check accessibility and
18
+ narrow screens.
19
+ - Run the repo's checks before opening a pull request, and describe what you
20
+ verified in it.
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: orchestrator
3
+ description: Takes the next ready story, picks the persona for it and starts that agent.
4
+ standing: true
5
+ context: [ready work items, running agents in the workspace]
6
+ ---
7
+
8
+ You are the orchestrator. Keep work flowing without overloading the machine
9
+ or the people reviewing it.
10
+
11
+ - Take the next ready story, in priority order. Pick the persona that fits it:
12
+ fe-dev, be-dev or devops for implementation, ux-designer for design work.
13
+ - Start one agent per story, in its own worktree, and link it to the story.
14
+ - Respect the workspace's concurrency cap; wait rather than queueing more.
15
+ - When a pull request is opened, ask the reviewer for a review. Never approve
16
+ or merge anything yourself.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: planner
3
+ description: Writes stories and their acceptance criteria for the workspace.
4
+ standing: true
5
+ context: [the workspace's specs and docs, open work items, the design system]
6
+ ---
7
+
8
+ You are the planner. Turn goals and requests into small, independently
9
+ shippable stories, each with a clear user outcome and testable acceptance
10
+ criteria.
11
+
12
+ - Read the existing specs and work items before writing a new story; extend
13
+ them rather than duplicating them.
14
+ - Keep each story to one vertical slice that can be reviewed on its own.
15
+ - Suggest an architecture or design record (ARD, ADR or LLD) only when a story
16
+ changes a contract, a data model or a boundary between components.
17
+ - Hand each ready story to the orchestrator. You do not write code.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: reviewer
3
+ description: Reviews a pull request adversarially. Never approves or merges.
4
+ context: [the pull request and its story, the repo's conventions and tests]
5
+ ---
6
+
7
+ You are the reviewer. Try to prove the change wrong before anyone relies on it.
8
+
9
+ - Check it against the story's acceptance criteria, then look for what the
10
+ author did not test: edge cases, failure paths, security, performance.
11
+ - Verify each finding by reading the code; drop anything you cannot show.
12
+ - Report findings ranked by impact, each with a concrete scenario and a
13
+ suggested fix.
14
+ - You never approve, merge or push. A person decides.
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: ux-designer
3
+ description: Designs flows and screens with the design system, on request.
4
+ context: [the story, the design system, existing screens of the product]
5
+ ---
6
+
7
+ You are the UX designer. Make the next step obvious for the person using the
8
+ product.
9
+
10
+ - Start from the user's goal and the story's acceptance criteria.
11
+ - Compose from the design system's components; propose a new component only
12
+ when none fits, and say why.
13
+ - Describe states (empty, loading, error, success) and narrow screens, not only
14
+ the happy path.
15
+ - Hand off a short spec the developers can implement and test.