@monster-orange/den 0.4.14 → 0.4.15
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 +127 -0
- package/README.md +7 -7
- package/docs/distributions.md +144 -8
- package/package.json +1 -1
- package/src/shared/actor-token.ts +12 -0
- package/src/shared/agents/conversations.ts +33 -4
- package/src/shared/agents/den-claude.ts +13 -3
- package/src/shared/agents/records.ts +18 -0
- package/src/shared/agents/runtime.ts +6 -1
- package/src/shared/claim-code.ts +163 -0
- package/src/shared/cli-command.ts +31 -0
- package/src/shared/den-config.ts +12 -0
- package/src/shared/environment-manager/workspaces.ts +20 -0
- package/src/shared/web-session.ts +64 -5
- package/src/ui/cli/agent-ops.ts +75 -22
- package/src/ui/cli/agents-command.ts +4 -1
- package/src/ui/cli/doctor-command.ts +29 -3
- package/src/ui/cli/doctor-onboard.ts +26 -0
- package/src/ui/cli/flag-parser.ts +9 -1
- package/src/ui/cli/index.ts +29 -3
- package/src/ui/cli/onboard-command.ts +180 -0
- package/src/ui/cli/onboard-reads.ts +613 -0
- package/src/ui/cli/repo-command.ts +156 -10
- package/src/ui/cli/web-command.ts +92 -1
- package/src/ui/mcp/api.ts +14 -2
- package/src/ui/mcp/server.ts +12 -2
- package/src/ui/mcp/tools.ts +368 -1
- package/src/ui/server/agent-routes.ts +2 -5
- package/src/ui/server/approval-routes.ts +390 -0
- package/src/ui/server/approvals.ts +416 -0
- package/src/ui/server/github-routes.ts +12 -4
- package/src/ui/server/held-actions.ts +203 -0
- package/src/ui/server/http-helpers.ts +35 -0
- package/src/ui/server/index.ts +286 -16
- package/src/ui/server/lifecycle-exec.ts +70 -0
- package/src/ui/server/onboard-routes.ts +132 -0
- package/src/ui/server/pair-routes.ts +7 -4
- package/src/ui/server/public/agents-page.js +91 -4
- package/src/ui/server/public/error-page.js +27 -3
- package/src/ui/server/public/header.js +1 -1
- package/src/ui/server/public/i18n/es.js +29 -0
- package/src/ui/server/public/i18n/ja.js +31 -0
- package/src/ui/server/public/index.html +1 -1
- package/src/ui/server/public/local-dev.html +5 -4
- package/src/ui/server/public/persona-ui.css +21 -0
- package/src/ui/server/public/release.html +0 -1
- package/src/ui/server/public/settings-registry.js +3 -1
- package/src/ui/server/public/settings.html +9 -1
- package/src/ui/server/public/shell.js +313 -0
- package/src/ui/server/request-guard.ts +2 -0
- package/src/ui/server/route-classes.ts +28 -1
- package/src/ui/server/session-routes.ts +11 -3
- package/src/ui/server/workspace-routes.ts +21 -10
- package/themes/den/den-theme.json +6 -6
|
@@ -20,6 +20,13 @@
|
|
|
20
20
|
* existed: true, and an existing side-car is never overwritten.
|
|
21
21
|
* - The workspace: `-w <ws>` (created and activated when missing), else the
|
|
22
22
|
* focused one, else a new `default` workspace, activated and focused.
|
|
23
|
+
* - `--dry-run` (ON-3, the MCP tool manifest_draft's twin): a local path
|
|
24
|
+
* only. Detection and the validator run as above, and the answer carries
|
|
25
|
+
* the side-car as YAML (`draft`) and `validation`; nothing is written, not
|
|
26
|
+
* the side-car, the workspace entry or the catalog.
|
|
27
|
+
*
|
|
28
|
+
* `<cli> repo detect <folder> [--json]` (ON-3, repos_detect's twin): the git
|
|
29
|
+
* repos in a folder and suggested workspaces (onboard-reads.ts).
|
|
23
30
|
*/
|
|
24
31
|
import { execFileSync } from "child_process";
|
|
25
32
|
import * as fs from "fs";
|
|
@@ -69,6 +76,8 @@ export interface RepoAddArgs {
|
|
|
69
76
|
health?: string;
|
|
70
77
|
branch?: string;
|
|
71
78
|
json: boolean;
|
|
79
|
+
/** `--dry-run`: detect and validate, write nothing (a local path only). */
|
|
80
|
+
dryRun?: boolean;
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
export interface RepoAddResult {
|
|
@@ -93,11 +102,17 @@ export interface RepoAddResult {
|
|
|
93
102
|
fix?: string;
|
|
94
103
|
ask?: string;
|
|
95
104
|
choices?: string[];
|
|
105
|
+
/** `--dry-run`: nothing was written. */
|
|
106
|
+
dryRun?: boolean;
|
|
107
|
+
/** `--dry-run`: the side-car Den would write, as YAML. */
|
|
108
|
+
draft?: string;
|
|
109
|
+
/** `--dry-run`: the validator's answer for the draft. */
|
|
110
|
+
validation?: { valid: boolean; errors: { path: string; message: string }[] };
|
|
96
111
|
}
|
|
97
112
|
|
|
98
113
|
const USAGE = () =>
|
|
99
114
|
`${product().cli} repo add <path|git-url> [--sidecar] [-w <workspace>] [--name <repo>] ` +
|
|
100
|
-
`[--run "<cmd>"] [--down "<cmd>"] [--health <url>] [--branch <branch>] [--json]`;
|
|
115
|
+
`[--run "<cmd>"] [--down "<cmd>"] [--health <url>] [--branch <branch>] [--dry-run] [--json]`;
|
|
101
116
|
|
|
102
117
|
export const REPO_HELP = () =>
|
|
103
118
|
[
|
|
@@ -111,7 +126,14 @@ export const REPO_HELP = () =>
|
|
|
111
126
|
' cannot decide, nothing is written and --json answers with "ask" and',
|
|
112
127
|
' "choices": re-run with --run "<cmd>". --health <url> adds an HTTP health',
|
|
113
128
|
" check. -w picks the workspace (default: the focused one, else a new",
|
|
114
|
-
" `default`). Safe to re-run: the same repo answers existed: true
|
|
129
|
+
" `default`). Safe to re-run: the same repo answers existed: true.",
|
|
130
|
+
" --dry-run (a local folder only): detect and validate the side-car and",
|
|
131
|
+
' print it ("draft"), writing nothing: no side-car, no workspace entry.\n',
|
|
132
|
+
`\n ${product().cli} repo detect <folder> [--json]\n`,
|
|
133
|
+
" Find the git repos in a folder inside your home (one or two levels deep),",
|
|
134
|
+
" say how each one runs, and suggest workspaces: the repos in one parent",
|
|
135
|
+
" folder go in one workspace named after it. Reads only: it never writes,",
|
|
136
|
+
" never runs a repo's tools and never follows a symlink.\n",
|
|
115
137
|
].join("\n");
|
|
116
138
|
|
|
117
139
|
const VALUE_FLAGS = new Set([
|
|
@@ -136,6 +158,7 @@ export function parseRepoAddArgs(argv: string[]): RepoAddArgs {
|
|
|
136
158
|
const flag = a.startsWith("--") && eq > 0 ? a.slice(0, eq) : a;
|
|
137
159
|
if (a === "--sidecar") out.sidecar = true;
|
|
138
160
|
else if (a === "--json") out.json = true;
|
|
161
|
+
else if (a === "--dry-run" || a === "-n") out.dryRun = true;
|
|
139
162
|
else if (VALUE_FLAGS.has(flag)) {
|
|
140
163
|
const v = flag !== a ? a.slice(eq + 1) : argv[++i];
|
|
141
164
|
if (v === undefined || v === "") throw new Error(`${flag} needs a value`);
|
|
@@ -236,6 +259,15 @@ export function repoAdd(
|
|
|
236
259
|
fix: `${bin} repo add ${a.spec} --health http://localhost:<port>/ --json`,
|
|
237
260
|
};
|
|
238
261
|
|
|
262
|
+
if (a.dryRun && url)
|
|
263
|
+
return {
|
|
264
|
+
ok: false,
|
|
265
|
+
dryRun: true,
|
|
266
|
+
error:
|
|
267
|
+
"--dry-run takes a local folder: a git URL would have to be cloned first",
|
|
268
|
+
fix: `${bin} repo add <path> --dry-run --json`,
|
|
269
|
+
};
|
|
270
|
+
|
|
239
271
|
// ── Where the repo is ────────────────────────────────────────────────────
|
|
240
272
|
let repoPath: string;
|
|
241
273
|
let mode: "dev-managed" | "managed";
|
|
@@ -299,6 +331,10 @@ export function repoAdd(
|
|
|
299
331
|
? (originRepoName(repoPath) ?? name)
|
|
300
332
|
: (originRepoName(repoPath) ?? path.basename(repoPath));
|
|
301
333
|
let manifestInfo: RepoAddResult["manifest"];
|
|
334
|
+
/** --dry-run: the side-car that would be written. */
|
|
335
|
+
let drafted:
|
|
336
|
+
| { manifest: Record<string, unknown>; text: string; valid: boolean }
|
|
337
|
+
| undefined;
|
|
302
338
|
if (own) {
|
|
303
339
|
manifestInfo = { source: "repo", file: own };
|
|
304
340
|
} else {
|
|
@@ -324,29 +360,43 @@ export function repoAdd(
|
|
|
324
360
|
};
|
|
325
361
|
const manifest = sidecarManifest(mName, d.commands, a.health);
|
|
326
362
|
const v = validate(manifest);
|
|
363
|
+
const dry = a.dryRun
|
|
364
|
+
? {
|
|
365
|
+
dryRun: true,
|
|
366
|
+
draft: formatManifestYaml(manifest),
|
|
367
|
+
validation: { valid: v.valid, errors: v.errors },
|
|
368
|
+
}
|
|
369
|
+
: {};
|
|
327
370
|
if (!v.valid)
|
|
328
371
|
return {
|
|
329
372
|
ok: false,
|
|
330
373
|
error: `the detected manifest is not valid: ${v.errors.map((e) => `${e.path} ${e.message}`).join("; ")}`,
|
|
331
374
|
fix: `${bin} repo add ${a.spec} --run "<command>" --json`,
|
|
375
|
+
...dry,
|
|
332
376
|
};
|
|
333
377
|
const file = sidecarFile(key);
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
378
|
+
if (a.dryRun) {
|
|
379
|
+
drafted = { manifest, text: dry.draft as string, valid: v.valid };
|
|
380
|
+
} else {
|
|
381
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
382
|
+
// "wx": never overwrite a side-car someone may have edited.
|
|
383
|
+
fs.writeFileSync(file, formatManifestYaml(manifest), {
|
|
384
|
+
encoding: "utf-8",
|
|
385
|
+
flag: "wx",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
340
388
|
manifestInfo = {
|
|
341
389
|
source: "sidecar",
|
|
342
390
|
file,
|
|
343
|
-
written:
|
|
391
|
+
written: !a.dryRun,
|
|
344
392
|
detected: d.commands.source,
|
|
345
393
|
};
|
|
346
394
|
}
|
|
347
395
|
}
|
|
348
396
|
|
|
349
|
-
const manifest =
|
|
397
|
+
const manifest = drafted
|
|
398
|
+
? (drafted.manifest as unknown as ReturnType<typeof readManifest>)
|
|
399
|
+
: readManifest(repoPath);
|
|
350
400
|
if (!manifest)
|
|
351
401
|
return {
|
|
352
402
|
ok: false,
|
|
@@ -366,6 +416,34 @@ export function repoAdd(
|
|
|
366
416
|
fix: `${bin} repo add ${a.spec} --name <repo> --json`,
|
|
367
417
|
};
|
|
368
418
|
|
|
419
|
+
if (a.dryRun) {
|
|
420
|
+
// Nothing is written: not the side-car, not the workspace entry.
|
|
421
|
+
const flags = [
|
|
422
|
+
a.name !== undefined ? ` --name ${a.name}` : "",
|
|
423
|
+
a.run !== undefined ? ` --run "${a.run}"` : "",
|
|
424
|
+
a.down !== undefined ? ` --down "${a.down}"` : "",
|
|
425
|
+
a.health !== undefined ? ` --health ${a.health}` : "",
|
|
426
|
+
a.workspace !== undefined ? ` -w ${a.workspace}` : "",
|
|
427
|
+
].join("");
|
|
428
|
+
return {
|
|
429
|
+
ok: true,
|
|
430
|
+
dryRun: true,
|
|
431
|
+
repo: name,
|
|
432
|
+
workspace: a.workspace ?? getFocusedWorkspaceName() ?? "default",
|
|
433
|
+
mode,
|
|
434
|
+
path: repoPath,
|
|
435
|
+
manifest: manifestInfo,
|
|
436
|
+
services: Object.keys(manifest.services ?? {}),
|
|
437
|
+
...(drafted
|
|
438
|
+
? {
|
|
439
|
+
draft: drafted.text,
|
|
440
|
+
validation: { valid: drafted.valid, errors: [] },
|
|
441
|
+
}
|
|
442
|
+
: {}),
|
|
443
|
+
next: [`${bin} repo add ${a.spec}${flags} --json`],
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
369
447
|
// ── The workspace entry ──────────────────────────────────────────────────
|
|
370
448
|
const ws = targetWorkspace(a.workspace);
|
|
371
449
|
const file = readWorkspacesFile();
|
|
@@ -423,6 +501,18 @@ export function repoAdd(
|
|
|
423
501
|
}
|
|
424
502
|
|
|
425
503
|
function printHuman(r: RepoAddResult): void {
|
|
504
|
+
if (r.dryRun && r.ok) {
|
|
505
|
+
console.log(
|
|
506
|
+
`✓ Dry run: ${r.repo} would be added to workspace ${r.workspace} (${r.mode}, ${r.path}); nothing was written`,
|
|
507
|
+
);
|
|
508
|
+
if (r.manifest)
|
|
509
|
+
console.log(
|
|
510
|
+
` manifest: ${r.manifest.source} ${r.manifest.file}${r.draft ? ` (would be written, from ${r.manifest.detected})` : ""}`,
|
|
511
|
+
);
|
|
512
|
+
if (r.draft) console.log(`\n${r.draft.replace(/^/gm, " ")}`);
|
|
513
|
+
console.log(` To add it: ${r.next?.[0] ?? ""}`);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
426
516
|
if (!r.ok) {
|
|
427
517
|
console.error(`✗ repo add: ${r.error}`);
|
|
428
518
|
if (r.ask) console.error(` ${r.ask}`);
|
|
@@ -442,6 +532,61 @@ function printHuman(r: RepoAddResult): void {
|
|
|
442
532
|
console.log(` Next: ${r.next?.[0] ?? ""}`);
|
|
443
533
|
}
|
|
444
534
|
|
|
535
|
+
/** `repo detect <folder> [--json]` (ON-3, repos_detect's twin). */
|
|
536
|
+
async function cmdRepoDetect(argv: string[]): Promise<number> {
|
|
537
|
+
const { processPathRules, reposDetect } = await import("./onboard-reads.js");
|
|
538
|
+
const { registeredRepos } = await import("./onboard-command.js");
|
|
539
|
+
const { getStateDir } = await import("../../shared/paths.js");
|
|
540
|
+
const json = argv.includes("--json");
|
|
541
|
+
const positional = argv.filter((a) => a !== "--json");
|
|
542
|
+
const bad = positional.find((a) => a.startsWith("-"));
|
|
543
|
+
if (bad || positional.length !== 1) {
|
|
544
|
+
const r = {
|
|
545
|
+
ok: false,
|
|
546
|
+
error: bad ? `unknown flag ${bad}` : "repo detect needs one <folder>",
|
|
547
|
+
fix: `${runnable()} repo detect <folder> --json`,
|
|
548
|
+
};
|
|
549
|
+
if (json) printJson(r);
|
|
550
|
+
else console.error(`✗ repo detect: ${r.error}`);
|
|
551
|
+
return 2;
|
|
552
|
+
}
|
|
553
|
+
// A relative folder is the person's own, from where they typed it.
|
|
554
|
+
const given = positional[0];
|
|
555
|
+
const folder =
|
|
556
|
+
given.startsWith("~") || path.isAbsolute(given)
|
|
557
|
+
? given
|
|
558
|
+
: path.resolve(getInvokeCwd(), given);
|
|
559
|
+
const r = reposDetect(folder, processPathRules(getStateDir()), {
|
|
560
|
+
registered: registeredRepos,
|
|
561
|
+
bin: runnable(),
|
|
562
|
+
});
|
|
563
|
+
if (json) printJson(r);
|
|
564
|
+
else if (!r.ok) console.error(`✗ repo detect: ${r.error}`);
|
|
565
|
+
else {
|
|
566
|
+
console.log(
|
|
567
|
+
`✓ ${r.repos.length} repo${r.repos.length === 1 ? "" : "s"} in ${r.path}${r.truncated ? " (stopped early: more are there)" : ""}`,
|
|
568
|
+
);
|
|
569
|
+
for (const x of r.repos) {
|
|
570
|
+
const how = x.runs
|
|
571
|
+
? x.runs.up
|
|
572
|
+
: x.manifest !== "none"
|
|
573
|
+
? `its ${x.manifest === "repo" ? "own" : "side-car"} manifest`
|
|
574
|
+
: (x.ask ?? "");
|
|
575
|
+
const inWs = x.registered?.length
|
|
576
|
+
? ` (in ${x.registered.map((g) => g.workspace).join(", ")})`
|
|
577
|
+
: "";
|
|
578
|
+
console.log(` ${x.name.padEnd(20)} ${how}${inWs}`);
|
|
579
|
+
}
|
|
580
|
+
if (r.workspaces.length) {
|
|
581
|
+
console.log("\n Suggested workspaces:");
|
|
582
|
+
for (const w of r.workspaces)
|
|
583
|
+
console.log(` ${w.name.padEnd(20)} ${w.repos.join(", ")}`);
|
|
584
|
+
}
|
|
585
|
+
if (r.next[0]) console.log(`\n Next: ${r.next[0]}`);
|
|
586
|
+
}
|
|
587
|
+
return r.ok ? 0 : 1;
|
|
588
|
+
}
|
|
589
|
+
|
|
445
590
|
/** `repo <sub> …`; returns the exit code. */
|
|
446
591
|
export async function cmdRepo(
|
|
447
592
|
argv: string[],
|
|
@@ -453,6 +598,7 @@ export async function cmdRepo(
|
|
|
453
598
|
return sub ? 0 : 1;
|
|
454
599
|
}
|
|
455
600
|
const json = rest.includes("--json");
|
|
601
|
+
if (sub === "detect") return cmdRepoDetect(rest);
|
|
456
602
|
if (sub !== "add") {
|
|
457
603
|
const r = {
|
|
458
604
|
ok: false,
|
|
@@ -64,6 +64,12 @@ import {
|
|
|
64
64
|
} from "./server-identity.js";
|
|
65
65
|
|
|
66
66
|
import { product } from "../../shared/product.js";
|
|
67
|
+
import {
|
|
68
|
+
CLAIM_CODE_TTL_MS,
|
|
69
|
+
claimLink,
|
|
70
|
+
mintClaimCode,
|
|
71
|
+
personGrantRefusal,
|
|
72
|
+
} from "../../shared/claim-code.js";
|
|
67
73
|
import { redactSecrets } from "../../shared/redact.js";
|
|
68
74
|
interface WebCommandOptions {
|
|
69
75
|
/** `open` is `up` that always opens a fresh sign-in tab (SEC-1b). */
|
|
@@ -1142,6 +1148,7 @@ export const WEB_SUBCOMMANDS_WITH_HELP = [
|
|
|
1142
1148
|
"logout",
|
|
1143
1149
|
"owner",
|
|
1144
1150
|
"approve",
|
|
1151
|
+
"claim-link",
|
|
1145
1152
|
] as const;
|
|
1146
1153
|
|
|
1147
1154
|
export function displayWebHelp(): void {
|
|
@@ -1190,6 +1197,9 @@ export function displayWebHelp(): void {
|
|
|
1190
1197
|
console.log(
|
|
1191
1198
|
" approve <code> Let in a computer whose signed-out page shows this code",
|
|
1192
1199
|
);
|
|
1200
|
+
console.log(
|
|
1201
|
+
" claim-link Print a one-time setup link (30 min) that signs this machine's browser in",
|
|
1202
|
+
);
|
|
1193
1203
|
console.log(" (start/stop remain accepted as legacy aliases for up/down)");
|
|
1194
1204
|
console.log();
|
|
1195
1205
|
console.log(chalk.bold("Options:"));
|
|
@@ -1276,6 +1286,10 @@ export async function cmdWeb(args: string[]): Promise<void> {
|
|
|
1276
1286
|
process.exitCode = await cmdWebApprove(args.slice(1));
|
|
1277
1287
|
return;
|
|
1278
1288
|
}
|
|
1289
|
+
if (args[0] === "claim-link") {
|
|
1290
|
+
process.exitCode = cmdWebClaimLink(args.slice(1));
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1279
1293
|
if (args.includes("--help") || args.includes("-h")) {
|
|
1280
1294
|
displayWebHelp();
|
|
1281
1295
|
return;
|
|
@@ -1400,7 +1414,8 @@ export async function cmdWeb(args: string[]): Promise<void> {
|
|
|
1400
1414
|
// SEC-1b: what opens is a sign-in link (localhost, one-time, 60 s). It
|
|
1401
1415
|
// is printed only when the browser couldn't be opened: the one line the
|
|
1402
1416
|
// person then needs.
|
|
1403
|
-
|
|
1417
|
+
// Inside an agent's session: the plain page, never a sign-in link.
|
|
1418
|
+
let link: string | null = personGrantRefusal(process.env) ? url : null;
|
|
1404
1419
|
const signIn = () => (link ??= freshLoginLink(options.port));
|
|
1405
1420
|
const browser = openOrFocusDenWebUrl(
|
|
1406
1421
|
signIn,
|
|
@@ -1450,9 +1465,65 @@ export function freshLoginLink(
|
|
|
1450
1465
|
origin: string = localhostOrigin(port),
|
|
1451
1466
|
stateDir: string = getStateDir(),
|
|
1452
1467
|
): string {
|
|
1468
|
+
const refused = personGrantRefusal(process.env);
|
|
1469
|
+
if (refused) throw new Error(refused);
|
|
1453
1470
|
return loginLink(origin, mintLoginCode(ensureWebSecret(stateDir)), next);
|
|
1454
1471
|
}
|
|
1455
1472
|
|
|
1473
|
+
/**
|
|
1474
|
+
* `<cli> web claim-link [--json] [--port <port>]` (0.4.15): a one-time
|
|
1475
|
+
* setup link, `http://localhost:<port>/?claim=<code>` (30 minutes), on
|
|
1476
|
+
* stdout alone. Opening it signs that browser in, so it can approve an
|
|
1477
|
+
* agent's changes from the first page. Refused inside an agent's session:
|
|
1478
|
+
* the person opens it, never an agent. Never logged.
|
|
1479
|
+
*/
|
|
1480
|
+
export function cmdWebClaimLink(args: string[], io: CliIo = stdIo): number {
|
|
1481
|
+
const usage = `Usage: ${product().cli} web claim-link [--json] [--port <port>]\n`;
|
|
1482
|
+
if (isHelp(args)) {
|
|
1483
|
+
io.out(
|
|
1484
|
+
usage +
|
|
1485
|
+
" Prints a one-time setup link (it lasts 30 minutes and works once). Open it\n" +
|
|
1486
|
+
" yourself, in the browser on this machine: it signs that browser in, so you\n" +
|
|
1487
|
+
" can approve what an agent asks to change. Don't let any agent open it.\n",
|
|
1488
|
+
);
|
|
1489
|
+
return 0;
|
|
1490
|
+
}
|
|
1491
|
+
const refused = personGrantRefusal(process.env);
|
|
1492
|
+
if (refused) {
|
|
1493
|
+
io.err(`${product().cli}: ${refused}\n`);
|
|
1494
|
+
return 1;
|
|
1495
|
+
}
|
|
1496
|
+
let port: number;
|
|
1497
|
+
let rest: string[];
|
|
1498
|
+
try {
|
|
1499
|
+
({ port, rest } = takePort(args));
|
|
1500
|
+
} catch (e) {
|
|
1501
|
+
io.err(`${(e as Error).message}\n`);
|
|
1502
|
+
return 2;
|
|
1503
|
+
}
|
|
1504
|
+
const asJson = rest.includes("--json");
|
|
1505
|
+
if (rest.some((a) => a !== "--json")) {
|
|
1506
|
+
io.err(usage);
|
|
1507
|
+
return 2;
|
|
1508
|
+
}
|
|
1509
|
+
const now = Date.now();
|
|
1510
|
+
const url = claimLink(
|
|
1511
|
+
localhostOrigin(port),
|
|
1512
|
+
mintClaimCode(ensureWebSecret(io.stateDir ?? getStateDir()), now),
|
|
1513
|
+
);
|
|
1514
|
+
if (asJson)
|
|
1515
|
+
io.out(
|
|
1516
|
+
`${JSON.stringify({ url, expiresAt: new Date(now + CLAIM_CODE_TTL_MS).toISOString() })}\n`,
|
|
1517
|
+
);
|
|
1518
|
+
else {
|
|
1519
|
+
io.err(
|
|
1520
|
+
`${product().cli}: open this link yourself within 30 minutes; it signs in one browser, once. Don't let any agent open it.\n`,
|
|
1521
|
+
);
|
|
1522
|
+
io.out(`${url}\n`);
|
|
1523
|
+
}
|
|
1524
|
+
return 0;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1456
1527
|
interface CliIo {
|
|
1457
1528
|
out: (s: string) => void;
|
|
1458
1529
|
err: (s: string) => void;
|
|
@@ -1570,6 +1641,11 @@ export function cmdWebLogin(args: string[], io: CliIo = stdIo): number {
|
|
|
1570
1641
|
io.out(LOGIN_HELP());
|
|
1571
1642
|
return 0;
|
|
1572
1643
|
}
|
|
1644
|
+
const refused = personGrantRefusal(process.env);
|
|
1645
|
+
if (refused) {
|
|
1646
|
+
io.err(`${product().cli}: ${refused}\n`);
|
|
1647
|
+
return 1;
|
|
1648
|
+
}
|
|
1573
1649
|
const a = parseLoginArgs(args, io);
|
|
1574
1650
|
if (typeof a === "number") return a;
|
|
1575
1651
|
const link = freshLoginLink(
|
|
@@ -1610,6 +1686,11 @@ export async function cmdWebLoginWait(
|
|
|
1610
1686
|
io.out(LOGIN_HELP());
|
|
1611
1687
|
return 0;
|
|
1612
1688
|
}
|
|
1689
|
+
const refused = personGrantRefusal(process.env);
|
|
1690
|
+
if (refused) {
|
|
1691
|
+
io.err(`${product().cli}: ${refused}\n`);
|
|
1692
|
+
return 1;
|
|
1693
|
+
}
|
|
1613
1694
|
const a = parseLoginArgs(args, io);
|
|
1614
1695
|
if (typeof a === "number") return a;
|
|
1615
1696
|
if (a.noWait)
|
|
@@ -2029,6 +2110,11 @@ export async function cmdWebApprove(
|
|
|
2029
2110
|
io.err(usage);
|
|
2030
2111
|
return 2;
|
|
2031
2112
|
}
|
|
2113
|
+
const refused = list ? null : personGrantRefusal(process.env);
|
|
2114
|
+
if (refused) {
|
|
2115
|
+
io.err(`${product().cli}: ${refused}\n`);
|
|
2116
|
+
return 1;
|
|
2117
|
+
}
|
|
2032
2118
|
const stateDir = io.stateDir ?? getStateDir();
|
|
2033
2119
|
const r = list
|
|
2034
2120
|
? await serverCall(stateDir, port, "GET", "/api/web/waiting")
|
|
@@ -2133,6 +2219,11 @@ export async function cmdWebOwner(
|
|
|
2133
2219
|
}
|
|
2134
2220
|
const stateDir = io.stateDir ?? getStateDir();
|
|
2135
2221
|
const cli = product().cli;
|
|
2222
|
+
const refused = sub === "show" ? null : personGrantRefusal(process.env);
|
|
2223
|
+
if (refused) {
|
|
2224
|
+
io.err(`${cli}: ${refused}\n`);
|
|
2225
|
+
return 1;
|
|
2226
|
+
}
|
|
2136
2227
|
if (sub === "show") {
|
|
2137
2228
|
const o = readWebOwner(stateDir);
|
|
2138
2229
|
if (asJson) io.out(`${JSON.stringify({ owner: o }, null, 2)}\n`);
|
package/src/ui/mcp/api.ts
CHANGED
|
@@ -129,6 +129,10 @@ const READ_ROUTES = new Set([
|
|
|
129
129
|
"/api/stacks",
|
|
130
130
|
"/api/edge",
|
|
131
131
|
"/api/server-info",
|
|
132
|
+
// ON-3: onboarding reads (nothing written).
|
|
133
|
+
"/api/onboard/status",
|
|
134
|
+
"/api/onboard/repos",
|
|
135
|
+
"/api/onboard/manifest",
|
|
132
136
|
]);
|
|
133
137
|
|
|
134
138
|
/** Action routes and their method. Anything else is never reached. */
|
|
@@ -138,6 +142,8 @@ const ACTION_ROUTES: Record<string, string> = {
|
|
|
138
142
|
"/api/service/exec": "GET",
|
|
139
143
|
"/api/run": "GET",
|
|
140
144
|
"/api/workspace/sync": "POST",
|
|
145
|
+
// ON-3's writes: asks the person to allow one (held; nothing runs here).
|
|
146
|
+
"/api/approvals": "POST",
|
|
141
147
|
};
|
|
142
148
|
|
|
143
149
|
/**
|
|
@@ -145,6 +151,8 @@ const ACTION_ROUTES: Record<string, string> = {
|
|
|
145
151
|
* and Bring in's list and import (MCP agent_import, AG-17.1).
|
|
146
152
|
*/
|
|
147
153
|
const ACTION_ROUTE_PATTERNS: { re: RegExp; method: string }[] = [
|
|
154
|
+
// One held request's state (its own, the server checks). Never /decide.
|
|
155
|
+
{ re: /^\/api\/approvals\/[A-Za-z0-9_-]{16}$/, method: "GET" },
|
|
148
156
|
{
|
|
149
157
|
re: /^\/api\/workspaces\/[A-Za-z0-9._-]{1,64}\/agents\/[A-Za-z0-9._-]{1,64}\/resume$/,
|
|
150
158
|
method: "POST",
|
|
@@ -222,6 +230,8 @@ export interface DenApi {
|
|
|
222
230
|
query?: Query,
|
|
223
231
|
method?: "GET" | "POST",
|
|
224
232
|
body?: unknown,
|
|
233
|
+
/** Extra request headers (an approval's ticket). */
|
|
234
|
+
headers?: Record<string, string>,
|
|
225
235
|
): Promise<unknown>;
|
|
226
236
|
/** An SSE route, read to a bound; the connection is closed after. */
|
|
227
237
|
stream(path: string, query: Query, opts: StreamOpts): Promise<StreamResult>;
|
|
@@ -302,6 +312,7 @@ export function createDenApi(opts: {
|
|
|
302
312
|
query: Query,
|
|
303
313
|
signal?: AbortSignal,
|
|
304
314
|
body?: unknown,
|
|
315
|
+
extra: Record<string, string> = {},
|
|
305
316
|
): Promise<Response> {
|
|
306
317
|
assertAllowedRequest(method, path, query);
|
|
307
318
|
await ensureOurs();
|
|
@@ -309,6 +320,7 @@ export function createDenApi(opts: {
|
|
|
309
320
|
const res = await fetch(buildUrl(baseUrl, path, query), {
|
|
310
321
|
method,
|
|
311
322
|
headers: {
|
|
323
|
+
...extra,
|
|
312
324
|
...opts.headers(),
|
|
313
325
|
...(opts.auth ?? cliAuthHeaders)(),
|
|
314
326
|
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
@@ -338,8 +350,8 @@ export function createDenApi(opts: {
|
|
|
338
350
|
return {
|
|
339
351
|
baseUrl,
|
|
340
352
|
|
|
341
|
-
async get(path, query = {}, method = "GET", body) {
|
|
342
|
-
const res = await request(method, path, query, undefined, body);
|
|
353
|
+
async get(path, query = {}, method = "GET", body, headers) {
|
|
354
|
+
const res = await request(method, path, query, undefined, body, headers);
|
|
343
355
|
const text = await res.text();
|
|
344
356
|
try {
|
|
345
357
|
return JSON.parse(text) as unknown;
|
package/src/ui/mcp/server.ts
CHANGED
|
@@ -22,7 +22,12 @@ import { product } from "../../shared/product.js";
|
|
|
22
22
|
import { DEFAULT_PORT } from "../../shared/constants.js";
|
|
23
23
|
import { createDenApi, type DenApi } from "./api.js";
|
|
24
24
|
import { ownServer } from "../cli/server-identity.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
approvalsState,
|
|
27
|
+
callTool,
|
|
28
|
+
toolsFor,
|
|
29
|
+
type ToolContext,
|
|
30
|
+
} from "./tools.js";
|
|
26
31
|
import { plural } from "../../shared/plural.js";
|
|
27
32
|
import {
|
|
28
33
|
checkGrants,
|
|
@@ -60,7 +65,8 @@ export function createDenMcpServer(opts: DenMcpOptions): McpServer {
|
|
|
60
65
|
`through the running ${p.title} web server (\`${opts.cli} web up\`). ` +
|
|
61
66
|
(opts.cfg.actions.length
|
|
62
67
|
? `Actions enabled: ${opts.cfg.actions.join(", ")}.`
|
|
63
|
-
: "
|
|
68
|
+
: "The org has enabled no actions (mcp.actions).") +
|
|
69
|
+
` The ${approvalsState(opts.cfg)}.` +
|
|
64
70
|
(opts.scope
|
|
65
71
|
? ` Scoped to workspace "${opts.scope.workspace}" as ${opts.scope.actor}: ` +
|
|
66
72
|
"a call naming another workspace is refused, and lists show this one only, " +
|
|
@@ -258,6 +264,10 @@ export async function runOneShot(
|
|
|
258
264
|
if (shot.kind === "list") {
|
|
259
265
|
const names = tools.map((t) => t.name);
|
|
260
266
|
out(names.length ? `${names.join("\n")}\n` : "");
|
|
267
|
+
// Stdout stays one name a line; the state goes to stderr.
|
|
268
|
+
(opts.err ?? ((t: string) => process.stderr.write(t)))(
|
|
269
|
+
`${approvalsState(opts.cfg)}\n`,
|
|
270
|
+
);
|
|
261
271
|
return 0;
|
|
262
272
|
}
|
|
263
273
|
const ctx: ToolContext = {
|