@monster-orange/den 0.4.13 → 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 +176 -0
- package/README.md +7 -7
- package/docs/distributions.md +185 -7
- package/package.json +1 -1
- package/src/shared/actor-token.ts +12 -0
- package/src/shared/agents/conversations.ts +256 -0
- package/src/shared/agents/den-claude.ts +19 -0
- package/src/shared/agents/records.ts +23 -0
- package/src/shared/agents/runtime.ts +14 -0
- package/src/shared/claim-code.ts +163 -0
- package/src/shared/cli-command.ts +31 -0
- package/src/shared/den-config.ts +14 -0
- package/src/shared/environment-manager/service-runner.ts +31 -12
- package/src/shared/environment-manager/workspaces.ts +31 -0
- package/src/shared/web-session.ts +64 -5
- package/src/ui/cli/agent-ops.ts +320 -18
- package/src/ui/cli/agents-command.ts +89 -2
- 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 +26 -3
- package/src/ui/mcp/server.ts +12 -2
- package/src/ui/mcp/tools.ts +427 -1
- package/src/ui/server/agent-resume-grant.ts +35 -0
- package/src/ui/server/agent-routes.ts +149 -1
- 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 +288 -8
- package/src/ui/server/public/agents.html +18 -0
- 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 +52 -0
- package/src/ui/server/public/i18n/ja.js +55 -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 +45 -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 +336 -0
- package/src/ui/server/request-guard.ts +19 -4
- package/src/ui/server/route-classes.ts +41 -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
|
@@ -260,6 +260,8 @@
|
|
|
260
260
|
"err.signin.noTerminal":
|
|
261
261
|
"{host} のターミナルがありませんか?このコンピューターで次を実行します:",
|
|
262
262
|
"err.signin.openTerminal": "ターミナルを開く",
|
|
263
|
+
"err.signin.sshUserNote":
|
|
264
|
+
"{host} のアカウント({user})とそのパスワード、または SSH キーでログインします。",
|
|
263
265
|
"err.signin.sshNote":
|
|
264
266
|
"「ターミナルを開く」はターミナルアプリで ssh://{target} を開きます。{target} への SSH アクセスが必要です。",
|
|
265
267
|
"err.signin.whyTitle": "なぜ?",
|
|
@@ -439,6 +441,14 @@
|
|
|
439
441
|
"ag.new.plainP":
|
|
440
442
|
"ワークスペースのフォルダーで開くターミナル。エージェントなし。",
|
|
441
443
|
"ag.new.task": "最初の指示(任意)",
|
|
444
|
+
"ag.new.name": "名前(任意)",
|
|
445
|
+
"ag.new.as": "{id} として開始します",
|
|
446
|
+
"ag.new.openRunning": "実行中のものを開く",
|
|
447
|
+
"ag.new.isRunning": "{id} はすでに実行中です",
|
|
448
|
+
"ag.new.openIt": "開く",
|
|
449
|
+
"ag.new.isStopped": "{id} は停止しています",
|
|
450
|
+
"ag.new.resumeIt": "再開する",
|
|
451
|
+
"ag.new.orNext": "または開始で {id} を作成します",
|
|
442
452
|
"ag.new.taskPh":
|
|
443
453
|
"例: 空の状態のストーリーを担当。デザインに合わせ、テストを追加する。",
|
|
444
454
|
"ag.new.adv": "詳細設定: モデルと権限",
|
|
@@ -448,6 +458,51 @@
|
|
|
448
458
|
"ag.new.modelDef": "ペルソナの既定({m})",
|
|
449
459
|
"ag.new.ver": "バージョン",
|
|
450
460
|
"ag.new.verDef": "ペルソナの既定({v})",
|
|
461
|
+
// AG-17.1, Bring in.
|
|
462
|
+
"ag.bring": "取り込む",
|
|
463
|
+
"ag.bring.title": "会話を取り込む",
|
|
464
|
+
"ag.bring.conv": "会話",
|
|
465
|
+
"ag.bring.help":
|
|
466
|
+
"このワークスペースのフォルダーまたはそのリポジトリのフォルダーで、Den の外で始めた会話です。新しい順に並びます。",
|
|
467
|
+
"ag.bring.none":
|
|
468
|
+
"このワークスペースのフォルダーやそのリポジトリのフォルダーに、Den の外で始めた会話はありません。",
|
|
469
|
+
"ag.bring.untitled": "無題の会話",
|
|
470
|
+
"ag.bring.open": "別の場所で開いています",
|
|
471
|
+
"ag.bring.openTip":
|
|
472
|
+
"まだ別のターミナルで開いています。先にそちらで閉じてください",
|
|
473
|
+
"ag.bring.agent": "エージェント {id}",
|
|
474
|
+
"ag.bring.note":
|
|
475
|
+
"実行していたフォルダーで続けます。ここからは Den のツールとガードが適用されます。ペインを閉じても実行は続きます。先にほかのターミナルで閉じてください。Den が開いていると判断できるのは、直近 1 分以内に書き込まれた会話だけです。",
|
|
476
|
+
"ag.bring.go": "取り込む",
|
|
477
|
+
"ag.bring.going": "取り込み中…",
|
|
478
|
+
"ag.bring.done": "{id} として取り込みました",
|
|
479
|
+
"ag.bring.now": "たった今",
|
|
480
|
+
"ag.bring.min": "{n} 分前",
|
|
481
|
+
"ag.bring.h": "{n} 時間前",
|
|
482
|
+
"ap.title": "{agent}が次のことを求めています:",
|
|
483
|
+
"ap.agent": "エージェント {name} ",
|
|
484
|
+
"ap.noAgent": "このマシン上の何か(エージェントの識別なし)",
|
|
485
|
+
"ap.noAgentWarn":
|
|
486
|
+
"エージェントにこれを頼んでいなければ、拒否してください。",
|
|
487
|
+
"ap.signin.title": "承認を待っているものがあります。",
|
|
488
|
+
"ap.signin":
|
|
489
|
+
"サインインすると表示されます: {cmd} を実行してリンクを開いてください。",
|
|
490
|
+
"ap.do.workspace_create": "ワークスペース「{name}」を作成",
|
|
491
|
+
"ap.do.repo_add":
|
|
492
|
+
"{path} のリポジトリをワークスペース「{workspace}」に追加",
|
|
493
|
+
"ap.do.repo_add.new":
|
|
494
|
+
"{path} のリポジトリを新しいワークスペース「{workspace}」に追加",
|
|
495
|
+
"ap.do.service_try": "{service} を起動",
|
|
496
|
+
"ap.says": "エージェントのコメント:",
|
|
497
|
+
"ap.review": "確認",
|
|
498
|
+
"ap.hide": "詳細を隠す",
|
|
499
|
+
"ap.later": "あとで",
|
|
500
|
+
"ap.deny": "拒否",
|
|
501
|
+
"ap.allow": "許可",
|
|
502
|
+
"ap.waits": "許可するまで何も変わりません。{time} まで待ちます。",
|
|
503
|
+
"ap.more": "ほかに {n} 件が待機中",
|
|
504
|
+
"ap.failed": "実行しましたが失敗しました: {error}",
|
|
505
|
+
"ap.done": "完了: {what}",
|
|
451
506
|
"ag.new.note":
|
|
452
507
|
"ワークスペースのフォルダーで開始します。ペインを閉じても実行は続きます。",
|
|
453
508
|
"ag.new.started": "{w} で {p} を開始しました",
|
|
@@ -344,7 +344,7 @@
|
|
|
344
344
|
cursor: pointer;
|
|
345
345
|
}
|
|
346
346
|
.new-ws-form button:hover {
|
|
347
|
-
|
|
347
|
+
filter: brightness(0.92);
|
|
348
348
|
}
|
|
349
349
|
/* ── Content ──────────────────────────────────────────────────────────── */
|
|
350
350
|
.content {
|
|
@@ -552,7 +552,7 @@
|
|
|
552
552
|
cursor: pointer;
|
|
553
553
|
}
|
|
554
554
|
.add-btn:hover {
|
|
555
|
-
|
|
555
|
+
filter: brightness(0.92);
|
|
556
556
|
}
|
|
557
557
|
.form-msg {
|
|
558
558
|
font-size: 11px;
|
|
@@ -688,8 +688,9 @@
|
|
|
688
688
|
.add-svc-row.selected {
|
|
689
689
|
background: color-mix(in srgb, var(--accent) 9%, transparent);
|
|
690
690
|
}
|
|
691
|
+
/* Already in the workspace: readable (WCAG AA), not dimmed. */
|
|
691
692
|
.add-svc-row.already {
|
|
692
|
-
|
|
693
|
+
color: var(--muted);
|
|
693
694
|
}
|
|
694
695
|
.add-svc-row-info {
|
|
695
696
|
flex: 1;
|
|
@@ -839,7 +840,7 @@
|
|
|
839
840
|
cursor: pointer;
|
|
840
841
|
}
|
|
841
842
|
.add-svc-submit:hover {
|
|
842
|
-
|
|
843
|
+
filter: brightness(0.92);
|
|
843
844
|
}
|
|
844
845
|
.add-svc-submit:disabled {
|
|
845
846
|
opacity: 0.45;
|
|
@@ -315,6 +315,20 @@ dialog.pe-dlg::backdrop {
|
|
|
315
315
|
text-overflow: ellipsis;
|
|
316
316
|
white-space: nowrap;
|
|
317
317
|
}
|
|
318
|
+
/* Bring in: the title is cut, never its badge ("Agent reviewer"). */
|
|
319
|
+
.pr .pn.pnc {
|
|
320
|
+
display: flex;
|
|
321
|
+
align-items: center;
|
|
322
|
+
gap: 6px;
|
|
323
|
+
}
|
|
324
|
+
.pr .pnc .pt {
|
|
325
|
+
min-width: 0;
|
|
326
|
+
overflow: hidden;
|
|
327
|
+
text-overflow: ellipsis;
|
|
328
|
+
}
|
|
329
|
+
.pr .pnc .tagx {
|
|
330
|
+
flex: none;
|
|
331
|
+
}
|
|
318
332
|
.pr .pn .role,
|
|
319
333
|
.prow .pn .role {
|
|
320
334
|
color: var(--faint);
|
|
@@ -537,3 +551,34 @@ dialog.pe-dlg::backdrop {
|
|
|
537
551
|
padding: 2px 9px;
|
|
538
552
|
font-size: 12.5px;
|
|
539
553
|
}
|
|
554
|
+
|
|
555
|
+
/* AG-17.1, Bring in: a conversation that can't be picked (open elsewhere,
|
|
556
|
+
or an agent already continues it). */
|
|
557
|
+
.pr[aria-disabled="true"] {
|
|
558
|
+
opacity: 0.6;
|
|
559
|
+
cursor: default;
|
|
560
|
+
}
|
|
561
|
+
.pr[aria-disabled="true"]:hover {
|
|
562
|
+
background: none;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/* ON-3's approval dialog (shell.js, every page): the Start dialog's frame,
|
|
566
|
+
the request in the body text's size and weight, and Review's lines in
|
|
567
|
+
the mono box the persona Details use. */
|
|
568
|
+
.den-ap-dlg .den-ap-what {
|
|
569
|
+
color: var(--text);
|
|
570
|
+
font: 600 14.5px/1.45 var(--sans);
|
|
571
|
+
overflow-wrap: anywhere;
|
|
572
|
+
}
|
|
573
|
+
.den-ap-dlg .den-ap-det {
|
|
574
|
+
margin: 0;
|
|
575
|
+
max-height: 260px;
|
|
576
|
+
overflow: auto;
|
|
577
|
+
padding: 10px 12px;
|
|
578
|
+
border: 1px solid var(--border);
|
|
579
|
+
border-radius: var(--radius-button, 6px);
|
|
580
|
+
background: var(--surface, var(--raised));
|
|
581
|
+
font: 12px/1.5 var(--mono);
|
|
582
|
+
white-space: pre-wrap;
|
|
583
|
+
overflow-wrap: anywhere;
|
|
584
|
+
}
|
|
@@ -160,7 +160,9 @@ export function deviceLine(d, now = Date.now()) {
|
|
|
160
160
|
? `signed in with GitHub${d.login ? ` (@${d.login})` : ""}`
|
|
161
161
|
: d.via === "code"
|
|
162
162
|
? "approved with a code"
|
|
163
|
-
:
|
|
163
|
+
: d.via === "claim"
|
|
164
|
+
? "claimed with a setup link"
|
|
165
|
+
: "signed in with a one-time link";
|
|
164
166
|
const name = d.name || "Unknown browser";
|
|
165
167
|
const from = d.client || d.host;
|
|
166
168
|
return {
|
|
@@ -340,10 +340,18 @@
|
|
|
340
340
|
.set-radio:hover {
|
|
341
341
|
background: var(--sunken);
|
|
342
342
|
}
|
|
343
|
+
/* Not installed: the option stays readable (WCAG AA); only its
|
|
344
|
+
radio dims. */
|
|
343
345
|
.set-radio.off {
|
|
344
|
-
|
|
346
|
+
color: var(--muted);
|
|
345
347
|
cursor: default;
|
|
346
348
|
}
|
|
349
|
+
.set-radio.off:hover {
|
|
350
|
+
background: none;
|
|
351
|
+
}
|
|
352
|
+
.set-radio.off input {
|
|
353
|
+
opacity: 0.5;
|
|
354
|
+
}
|
|
347
355
|
.set-radio input {
|
|
348
356
|
accent-color: var(--accent);
|
|
349
357
|
margin: 0;
|
|
@@ -383,6 +383,8 @@
|
|
|
383
383
|
"err.signin.noTerminal":
|
|
384
384
|
"Don't have a terminal on {host}? From this computer, run:",
|
|
385
385
|
"err.signin.openTerminal": "Open Terminal",
|
|
386
|
+
"err.signin.sshUserNote":
|
|
387
|
+
"Log in with your {host} account ({user}) and its password, or an SSH key.",
|
|
386
388
|
"err.signin.sshNote":
|
|
387
389
|
"Open Terminal opens ssh://{target} in your terminal app. It needs SSH access to {target}.",
|
|
388
390
|
"err.signin.whyTitle": "Why?",
|
|
@@ -566,6 +568,14 @@
|
|
|
566
568
|
"ag.new.plain": "Plain session",
|
|
567
569
|
"ag.new.plainP": "A terminal in the workspace folder. No agent.",
|
|
568
570
|
"ag.new.task": "First instruction (optional)",
|
|
571
|
+
"ag.new.name": "Name (optional)",
|
|
572
|
+
"ag.new.as": "Starts as {id}",
|
|
573
|
+
"ag.new.openRunning": "Open the running one",
|
|
574
|
+
"ag.new.isRunning": "{id} is already running",
|
|
575
|
+
"ag.new.openIt": "Open it",
|
|
576
|
+
"ag.new.isStopped": "{id} is stopped",
|
|
577
|
+
"ag.new.resumeIt": "Resume it",
|
|
578
|
+
"ag.new.orNext": "or Start makes {id}",
|
|
569
579
|
"ag.new.taskPh":
|
|
570
580
|
"e.g. Take the empty-state story. Match the design and add a test.",
|
|
571
581
|
"ag.new.adv": "Advanced: model and permissions",
|
|
@@ -575,6 +585,47 @@
|
|
|
575
585
|
"ag.new.modelDef": "Persona default ({m})",
|
|
576
586
|
"ag.new.ver": "Version",
|
|
577
587
|
"ag.new.verDef": "Persona default ({v})",
|
|
588
|
+
// AG-17.1, Bring in.
|
|
589
|
+
"ag.bring": "Bring in",
|
|
590
|
+
"ag.bring.title": "Bring in a conversation",
|
|
591
|
+
"ag.bring.conv": "Conversation",
|
|
592
|
+
"ag.bring.help":
|
|
593
|
+
"Conversations started outside Den in this workspace's folder or its repos' folders, newest first.",
|
|
594
|
+
"ag.bring.none":
|
|
595
|
+
"No conversations started outside Den in this workspace's folder or its repos' folders.",
|
|
596
|
+
"ag.bring.untitled": "Untitled conversation",
|
|
597
|
+
"ag.bring.open": "Open elsewhere",
|
|
598
|
+
"ag.bring.openTip": "Still open in another terminal. Close it there first",
|
|
599
|
+
"ag.bring.agent": "Agent {id}",
|
|
600
|
+
"ag.bring.note":
|
|
601
|
+
"Continues it in the folder it ran in, with Den's tools and guards from now on. It keeps running if you close the pane. Close it in any other terminal first: Den only sees a conversation as open if it was written in the last minute.",
|
|
602
|
+
"ag.bring.go": "Bring in",
|
|
603
|
+
"ag.bring.going": "Bringing in…",
|
|
604
|
+
"ag.bring.done": "Brought in as {id}",
|
|
605
|
+
"ag.bring.now": "just now",
|
|
606
|
+
"ag.bring.min": "{n} min ago",
|
|
607
|
+
"ag.bring.h": "{n} h ago",
|
|
608
|
+
"ap.title": "{agent} wants to:",
|
|
609
|
+
"ap.agent": "The {name} agent",
|
|
610
|
+
"ap.noAgent": "Something on this machine (no agent identity)",
|
|
611
|
+
"ap.noAgentWarn": "If you didn't ask an agent to do this, Deny.",
|
|
612
|
+
"ap.signin.title": "Something is waiting for your approval.",
|
|
613
|
+
"ap.signin": "Sign in to see it: run {cmd} and open the link.",
|
|
614
|
+
"ap.do.workspace_create": 'Create the workspace "{name}"',
|
|
615
|
+
"ap.do.repo_add": 'Add the repo at {path} to workspace "{workspace}"',
|
|
616
|
+
"ap.do.repo_add.new":
|
|
617
|
+
'Add the repo at {path} to a new workspace "{workspace}"',
|
|
618
|
+
"ap.do.service_try": "Start {service}",
|
|
619
|
+
"ap.says": "Agent says:",
|
|
620
|
+
"ap.review": "Review",
|
|
621
|
+
"ap.hide": "Hide details",
|
|
622
|
+
"ap.later": "Later",
|
|
623
|
+
"ap.deny": "Deny",
|
|
624
|
+
"ap.allow": "Allow",
|
|
625
|
+
"ap.waits": "Nothing changes until you allow it. It waits until {time}.",
|
|
626
|
+
"ap.more": "{n} more waiting",
|
|
627
|
+
"ap.failed": "It ran, and failed: {error}",
|
|
628
|
+
"ap.done": "Done: {what}",
|
|
578
629
|
"ag.new.note":
|
|
579
630
|
"Starts in the workspace folder. It keeps running if you close the pane.",
|
|
580
631
|
"ag.new.started": "Started {p} in {w}",
|
|
@@ -3829,6 +3880,37 @@
|
|
|
3829
3880
|
// server start: a 403 `csrf` (the server restarted) fetches a fresh one
|
|
3830
3881
|
// and retries once. Never sent to another origin.
|
|
3831
3882
|
var CSRF_METHODS = { POST: 1, PUT: 1, PATCH: 1, DELETE: 1 };
|
|
3883
|
+
// 0.4.15: the approval key (web-session.ts). A sign-in's 303 carries it
|
|
3884
|
+
// in the URL fragment (#ak=…), which never reaches a server: keep it in
|
|
3885
|
+
// this origin's localStorage (other localhost ports can't read it),
|
|
3886
|
+
// strip it from the address bar, and send it with every same-origin
|
|
3887
|
+
// change as X-Den-Approve-Key. Only approving needs it.
|
|
3888
|
+
var APPROVE_KEY_STORE = "den.approveKey";
|
|
3889
|
+
(function takeApproveKey() {
|
|
3890
|
+
var m = /(?:^#|&)ak=([A-Za-z0-9_-]{43})(?:&|$)/.exec(location.hash || "");
|
|
3891
|
+
if (!m) return;
|
|
3892
|
+
try {
|
|
3893
|
+
localStorage.setItem(APPROVE_KEY_STORE, m[1]);
|
|
3894
|
+
} catch (e) {
|
|
3895
|
+
/* storage blocked: this browser signs in again to approve */
|
|
3896
|
+
}
|
|
3897
|
+
try {
|
|
3898
|
+
history.replaceState(
|
|
3899
|
+
history.state,
|
|
3900
|
+
"",
|
|
3901
|
+
location.pathname + location.search,
|
|
3902
|
+
);
|
|
3903
|
+
} catch (e) {
|
|
3904
|
+
location.hash = "";
|
|
3905
|
+
}
|
|
3906
|
+
})();
|
|
3907
|
+
function approveKey() {
|
|
3908
|
+
try {
|
|
3909
|
+
return localStorage.getItem(APPROVE_KEY_STORE);
|
|
3910
|
+
} catch (e) {
|
|
3911
|
+
return null;
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3832
3914
|
var csrf = { token: null, pending: null };
|
|
3833
3915
|
function csrfToken(fresh) {
|
|
3834
3916
|
if (fresh) csrf.token = null;
|
|
@@ -3867,6 +3949,9 @@
|
|
|
3867
3949
|
(init && init.headers) || (isRequest(input) ? input.headers : undefined),
|
|
3868
3950
|
);
|
|
3869
3951
|
headers.set("X-Den-CSRF", token);
|
|
3952
|
+
var ak = approveKey();
|
|
3953
|
+
if (ak && !headers.has("X-Den-Approve-Key"))
|
|
3954
|
+
headers.set("X-Den-Approve-Key", ak);
|
|
3870
3955
|
return Object.assign({}, init || {}, { headers: headers });
|
|
3871
3956
|
}
|
|
3872
3957
|
function csrfFetch(input, init) {
|
|
@@ -3935,6 +4020,257 @@
|
|
|
3935
4020
|
if (!apiOffPage) csrfToken(false);
|
|
3936
4021
|
}
|
|
3937
4022
|
|
|
4023
|
+
// ── ON-3: an agent's held write waits for the person ─────────────────
|
|
4024
|
+
// (approval-routes.ts) Every page asks GET /api/approvals every few
|
|
4025
|
+
// seconds while it is in view, and opens the oldest request: Den's own
|
|
4026
|
+
// words for it (the agent's note only quoted), Review (what it will do),
|
|
4027
|
+
// Later, Deny, Allow. Allow and Deny go through the wrapped fetch above,
|
|
4028
|
+
// so they carry the CSRF token and the approval key; the server decides
|
|
4029
|
+
// who may approve (a browser that isn't signed in is told to sign in).
|
|
4030
|
+
// The frame is the Start dialog's (persona-ui.css's .pe-dlg).
|
|
4031
|
+
var ap = { dlg: null, shown: null, later: {}, busy: false, css: null };
|
|
4032
|
+
function apCss() {
|
|
4033
|
+
if (ap.css) return ap.css;
|
|
4034
|
+
ap.css = new Promise(function (done) {
|
|
4035
|
+
if (document.querySelector('link[href="/persona-ui.css"]')) return done();
|
|
4036
|
+
var l = document.createElement("link");
|
|
4037
|
+
l.rel = "stylesheet";
|
|
4038
|
+
l.href = "/persona-ui.css";
|
|
4039
|
+
l.onload = l.onerror = function () {
|
|
4040
|
+
done();
|
|
4041
|
+
};
|
|
4042
|
+
document.head.appendChild(l);
|
|
4043
|
+
});
|
|
4044
|
+
return ap.css;
|
|
4045
|
+
}
|
|
4046
|
+
function apWhat(a) {
|
|
4047
|
+
var p = a.params || {};
|
|
4048
|
+
var key = "ap.do." + a.action + (p.isNew ? ".new" : "");
|
|
4049
|
+
var s = t(key, p);
|
|
4050
|
+
if (s !== key) return s;
|
|
4051
|
+
s = String(a.summary || a.action);
|
|
4052
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
4053
|
+
}
|
|
4054
|
+
function apDialog() {
|
|
4055
|
+
if (ap.dlg) return ap.dlg;
|
|
4056
|
+
var d = document.createElement("dialog");
|
|
4057
|
+
d.className = "pe-dlg den-ap-dlg";
|
|
4058
|
+
d.id = "den-ap-dlg";
|
|
4059
|
+
d.setAttribute("aria-labelledby", "den-ap-h");
|
|
4060
|
+
d.innerHTML =
|
|
4061
|
+
'<form method="dialog">' +
|
|
4062
|
+
'<h2 id="den-ap-h"></h2>' +
|
|
4063
|
+
'<p class="help den-ap-who" id="den-ap-who" hidden></p>' +
|
|
4064
|
+
'<p class="den-ap-what" id="den-ap-what"></p>' +
|
|
4065
|
+
'<p class="help" id="den-ap-note" hidden></p>' +
|
|
4066
|
+
'<pre class="den-ap-det" id="den-ap-det" hidden></pre>' +
|
|
4067
|
+
'<p class="help" id="den-ap-waits"></p>' +
|
|
4068
|
+
'<p class="err" id="den-ap-err" role="alert" hidden></p>' +
|
|
4069
|
+
'<div class="btns">' +
|
|
4070
|
+
'<button type="button" class="den-btn" id="den-ap-review" aria-expanded="false" aria-controls="den-ap-det"></button>' +
|
|
4071
|
+
'<span class="sp help" id="den-ap-more"></span>' +
|
|
4072
|
+
'<button type="button" class="den-btn" id="den-ap-later"></button>' +
|
|
4073
|
+
'<button type="button" class="den-btn" id="den-ap-deny"></button>' +
|
|
4074
|
+
'<button type="button" class="den-btn primary" id="den-ap-allow"></button>' +
|
|
4075
|
+
"</div></form>";
|
|
4076
|
+
document.body.appendChild(d);
|
|
4077
|
+
var $ = function (id) {
|
|
4078
|
+
return d.querySelector("#" + id);
|
|
4079
|
+
};
|
|
4080
|
+
$("den-ap-review").addEventListener("click", function () {
|
|
4081
|
+
var det = $("den-ap-det");
|
|
4082
|
+
det.hidden = !det.hidden;
|
|
4083
|
+
$("den-ap-review").textContent = t(det.hidden ? "ap.review" : "ap.hide");
|
|
4084
|
+
$("den-ap-review").setAttribute("aria-expanded", String(!det.hidden));
|
|
4085
|
+
});
|
|
4086
|
+
$("den-ap-later").addEventListener("click", function () {
|
|
4087
|
+
apLater();
|
|
4088
|
+
});
|
|
4089
|
+
// Esc is Later: the request keeps waiting (and the agent with it).
|
|
4090
|
+
d.addEventListener("cancel", function (e) {
|
|
4091
|
+
e.preventDefault();
|
|
4092
|
+
apLater();
|
|
4093
|
+
});
|
|
4094
|
+
$("den-ap-deny").addEventListener("click", function () {
|
|
4095
|
+
apDecide("deny");
|
|
4096
|
+
});
|
|
4097
|
+
$("den-ap-allow").addEventListener("click", function () {
|
|
4098
|
+
apDecide("allow");
|
|
4099
|
+
});
|
|
4100
|
+
ap.dlg = d;
|
|
4101
|
+
return d;
|
|
4102
|
+
}
|
|
4103
|
+
function apLater() {
|
|
4104
|
+
if (ap.shown) ap.later[ap.shown.id] = 1;
|
|
4105
|
+
apClose();
|
|
4106
|
+
}
|
|
4107
|
+
function apClose() {
|
|
4108
|
+
ap.shown = null;
|
|
4109
|
+
if (ap.dlg && ap.dlg.open) ap.dlg.close();
|
|
4110
|
+
}
|
|
4111
|
+
function apShow(a, more) {
|
|
4112
|
+
var d = apDialog();
|
|
4113
|
+
var $ = function (id) {
|
|
4114
|
+
return d.querySelector("#" + id);
|
|
4115
|
+
};
|
|
4116
|
+
var same = ap.shown && ap.shown.id === a.id;
|
|
4117
|
+
ap.shown = a;
|
|
4118
|
+
// No agent token: say so, and what to do if no agent was asked.
|
|
4119
|
+
$("den-ap-h").textContent = t("ap.title", {
|
|
4120
|
+
agent: a.agent ? t("ap.agent", { name: a.agent }) : t("ap.noAgent"),
|
|
4121
|
+
});
|
|
4122
|
+
$("den-ap-who").hidden = !!a.agent;
|
|
4123
|
+
$("den-ap-who").textContent = a.agent ? "" : t("ap.noAgentWarn");
|
|
4124
|
+
$("den-ap-what").textContent = apWhat(a);
|
|
4125
|
+
var note = $("den-ap-note");
|
|
4126
|
+
note.hidden = !a.note;
|
|
4127
|
+
note.textContent = a.note ? t("ap.says") + " “" + a.note + "”" : "";
|
|
4128
|
+
$("den-ap-det").textContent = (a.details || []).join("\n");
|
|
4129
|
+
var time = new Date(a.expiresAt);
|
|
4130
|
+
$("den-ap-waits").textContent = t("ap.waits", {
|
|
4131
|
+
time: isNaN(time)
|
|
4132
|
+
? ""
|
|
4133
|
+
: time.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
|
4134
|
+
});
|
|
4135
|
+
$("den-ap-more").textContent = more ? t("ap.more", { n: more }) : "";
|
|
4136
|
+
$("den-ap-later").textContent = t("ap.later");
|
|
4137
|
+
$("den-ap-deny").textContent = t("ap.deny");
|
|
4138
|
+
$("den-ap-allow").textContent = t("ap.allow");
|
|
4139
|
+
if (!same) {
|
|
4140
|
+
$("den-ap-det").hidden = true;
|
|
4141
|
+
$("den-ap-review").textContent = t("ap.review");
|
|
4142
|
+
$("den-ap-review").setAttribute("aria-expanded", "false");
|
|
4143
|
+
$("den-ap-err").hidden = true;
|
|
4144
|
+
}
|
|
4145
|
+
if (!d.open)
|
|
4146
|
+
apCss().then(function () {
|
|
4147
|
+
if (ap.shown && ap.shown.id === a.id && !d.open) d.showModal();
|
|
4148
|
+
});
|
|
4149
|
+
}
|
|
4150
|
+
function apError(msg) {
|
|
4151
|
+
var e = apDialog().querySelector("#den-ap-err");
|
|
4152
|
+
e.textContent = msg;
|
|
4153
|
+
e.hidden = false;
|
|
4154
|
+
}
|
|
4155
|
+
function apDecide(decision) {
|
|
4156
|
+
var a = ap.shown;
|
|
4157
|
+
if (!a || ap.busy) return;
|
|
4158
|
+
ap.busy = true;
|
|
4159
|
+
var btns = ap.dlg.querySelectorAll("button");
|
|
4160
|
+
for (var i = 0; i < btns.length; i++) btns[i].disabled = true;
|
|
4161
|
+
fetch("/api/approvals/" + encodeURIComponent(a.id) + "/decide", {
|
|
4162
|
+
method: "POST",
|
|
4163
|
+
headers: { "Content-Type": "application/json" },
|
|
4164
|
+
body: JSON.stringify({ decision: decision }),
|
|
4165
|
+
})
|
|
4166
|
+
.then(function (r) {
|
|
4167
|
+
return r.json().then(
|
|
4168
|
+
function (b) {
|
|
4169
|
+
return { ok: r.ok, b: b || {} };
|
|
4170
|
+
},
|
|
4171
|
+
function () {
|
|
4172
|
+
return { ok: false, b: {} };
|
|
4173
|
+
},
|
|
4174
|
+
);
|
|
4175
|
+
})
|
|
4176
|
+
.then(
|
|
4177
|
+
function (x) {
|
|
4178
|
+
if (!x.ok) {
|
|
4179
|
+
apError(x.b.error || t("ap.failed", { error: x.b.code || "?" }));
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
4182
|
+
var res = x.b.result;
|
|
4183
|
+
if (decision === "allow" && res && res.ok === false)
|
|
4184
|
+
toast(t("ap.failed", { error: res.error || "?" }));
|
|
4185
|
+
else if (decision === "allow")
|
|
4186
|
+
toast(t("ap.done", { what: apWhat(a) }));
|
|
4187
|
+
apClose();
|
|
4188
|
+
apPoll();
|
|
4189
|
+
},
|
|
4190
|
+
function (e) {
|
|
4191
|
+
apError(String((e && e.message) || e));
|
|
4192
|
+
},
|
|
4193
|
+
)
|
|
4194
|
+
.then(function () {
|
|
4195
|
+
ap.busy = false;
|
|
4196
|
+
for (var j = 0; j < btns.length; j++) btns[j].disabled = false;
|
|
4197
|
+
});
|
|
4198
|
+
}
|
|
4199
|
+
// Reading what waits needs what approving needs (approvals.ts): the
|
|
4200
|
+
// session's CSRF token and the approval key, on a GET too. A browser
|
|
4201
|
+
// without them is told only that something waits, and to sign in.
|
|
4202
|
+
function apList(fresh) {
|
|
4203
|
+
var ak = approveKey();
|
|
4204
|
+
// No approval key: only the count, to say "sign in to see it".
|
|
4205
|
+
if (!ak)
|
|
4206
|
+
return rawFetch("/api/approvals?count=1", {
|
|
4207
|
+
cache: "no-store",
|
|
4208
|
+
credentials: "same-origin",
|
|
4209
|
+
}).then(function (r) {
|
|
4210
|
+
return r.ok
|
|
4211
|
+
? r.json().then(function (b) {
|
|
4212
|
+
return { code: "signin_to_approve", waiting: b && b.waiting };
|
|
4213
|
+
})
|
|
4214
|
+
: null;
|
|
4215
|
+
});
|
|
4216
|
+
return csrfToken(fresh).then(function (token) {
|
|
4217
|
+
var h = { "X-Den-Approve-Key": ak };
|
|
4218
|
+
if (token) h["X-Den-CSRF"] = token;
|
|
4219
|
+
return rawFetch("/api/approvals", {
|
|
4220
|
+
cache: "no-store",
|
|
4221
|
+
credentials: "same-origin",
|
|
4222
|
+
headers: h,
|
|
4223
|
+
}).then(function (r) {
|
|
4224
|
+
return r.json().then(
|
|
4225
|
+
function (b) {
|
|
4226
|
+
if (!fresh && r.status === 403 && b && b.code === "csrf")
|
|
4227
|
+
return apList(true);
|
|
4228
|
+
return b;
|
|
4229
|
+
},
|
|
4230
|
+
function () {
|
|
4231
|
+
return null;
|
|
4232
|
+
},
|
|
4233
|
+
);
|
|
4234
|
+
});
|
|
4235
|
+
});
|
|
4236
|
+
}
|
|
4237
|
+
function apSignin(b) {
|
|
4238
|
+
if (b && b.code === "signin_to_approve" && b.waiting > 0)
|
|
4239
|
+
showBanner("approvals", {
|
|
4240
|
+
title: t("ap.signin.title"),
|
|
4241
|
+
text: t("ap.signin", { cmd: "__DEN_CLI__ web login" }),
|
|
4242
|
+
});
|
|
4243
|
+
else hideBanner("approvals");
|
|
4244
|
+
}
|
|
4245
|
+
function apPoll() {
|
|
4246
|
+
if (document.hidden || ap.busy) return;
|
|
4247
|
+
apList(false).then(
|
|
4248
|
+
function (b) {
|
|
4249
|
+
apSignin(b);
|
|
4250
|
+
var list = (b && b.ok && b.pending) || [];
|
|
4251
|
+
var open = list.filter(function (a) {
|
|
4252
|
+
return !ap.later[a.id];
|
|
4253
|
+
});
|
|
4254
|
+
if (!open.length) return apClose();
|
|
4255
|
+
var cur =
|
|
4256
|
+
(ap.shown &&
|
|
4257
|
+
open.filter(function (a) {
|
|
4258
|
+
return a.id === ap.shown.id;
|
|
4259
|
+
})[0]) ||
|
|
4260
|
+
open[0];
|
|
4261
|
+
apShow(cur, open.length - 1);
|
|
4262
|
+
},
|
|
4263
|
+
function () {},
|
|
4264
|
+
);
|
|
4265
|
+
}
|
|
4266
|
+
if (!apiOffPage && !configBad && rawFetch) {
|
|
4267
|
+
setInterval(apPoll, 3000);
|
|
4268
|
+
document.addEventListener("visibilitychange", function () {
|
|
4269
|
+
if (!document.hidden) apPoll();
|
|
4270
|
+
});
|
|
4271
|
+
setTimeout(apPoll, 300);
|
|
4272
|
+
}
|
|
4273
|
+
|
|
3938
4274
|
// ── the UI language (R19): a user pref ───────────────────────────────
|
|
3939
4275
|
// /api/settings `language` (user.yml), then a reload: the server serves
|
|
3940
4276
|
// every page with that language's catalog from its first paint.
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* and sign-in (/api/web/*, /auth, /api/session/logout), the lock (R8:
|
|
18
18
|
* /api/sudo-password*), or a shell-grade route other than its own
|
|
19
19
|
* session's /api/sessions/<id>/*: agent start and resume mint tokens.
|
|
20
|
-
* 403 `agent_forbidden`.
|
|
21
|
-
* when mcp.actions lists it,
|
|
22
|
-
* or a granted one (agent-resume-grant.ts). WS-3a (0.4.2) adds the workspace check at
|
|
20
|
+
* 403 `agent_forbidden`. Two exceptions: MCP agent_resume (AG-14.4)
|
|
21
|
+
* and agent_import (AG-17.1), each when mcp.actions lists it, inside
|
|
22
|
+
* its own workspace or a granted one (agent-resume-grant.ts). WS-3a (0.4.2) adds the workspace check at
|
|
23
23
|
* agentRefusal's seam.
|
|
24
24
|
* - Sign-in (§4, route-classes.ts): beyond loopback (a LAN name, a peer
|
|
25
25
|
* that isn't loopback, or the edge hostname for a client on another
|
|
@@ -61,7 +61,12 @@
|
|
|
61
61
|
import { randomBytes, timingSafeEqual, createHash } from "crypto";
|
|
62
62
|
import type * as http from "http";
|
|
63
63
|
import { readMcpConfig } from "../../shared/den-config.js";
|
|
64
|
-
import {
|
|
64
|
+
import {
|
|
65
|
+
agentImportRefusal,
|
|
66
|
+
agentImportTarget,
|
|
67
|
+
agentResumeRefusal,
|
|
68
|
+
agentResumeTarget,
|
|
69
|
+
} from "./agent-resume-grant.js";
|
|
65
70
|
import * as net from "net";
|
|
66
71
|
import * as os from "os";
|
|
67
72
|
import {
|
|
@@ -105,6 +110,8 @@ export interface GuardSession {
|
|
|
105
110
|
key: string;
|
|
106
111
|
/** The session's own CSRF token. */
|
|
107
112
|
csrf: string;
|
|
113
|
+
/** sha256 of its approval key (approvals.ts); unset before 0.4.15. */
|
|
114
|
+
approveKeyHash?: string;
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
/** How a request reached Den. `edge` and `lan` are beyond loopback. */
|
|
@@ -718,6 +725,14 @@ export function agentRefusal(
|
|
|
718
725
|
agentResumeRefusal(p, target.ws, target.id, mcpActions()) ??
|
|
719
726
|
workspaceRefusal(p, pathname)
|
|
720
727
|
);
|
|
728
|
+
// AG-17.1: MCP agent_import, opted in, inside the agent's scope (its
|
|
729
|
+
// list as well as its import: conversation titles are a person's).
|
|
730
|
+
const imp = agentImportTarget(pathname);
|
|
731
|
+
if (imp)
|
|
732
|
+
return (
|
|
733
|
+
agentImportRefusal(p, imp.ws, mcpActions()) ??
|
|
734
|
+
workspaceRefusal(p, pathname)
|
|
735
|
+
);
|
|
721
736
|
if (cls === "shell-grade")
|
|
722
737
|
return `${p.actor} can't ${method === "GET" ? "open" : "use"} ${pathname}: starting or resuming an agent, and the browser sessions, are a person's.`;
|
|
723
738
|
return workspaceRefusal(p, pathname);
|