@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/README.md +7 -7
  3. package/docs/distributions.md +185 -7
  4. package/package.json +1 -1
  5. package/src/shared/actor-token.ts +12 -0
  6. package/src/shared/agents/conversations.ts +256 -0
  7. package/src/shared/agents/den-claude.ts +19 -0
  8. package/src/shared/agents/records.ts +23 -0
  9. package/src/shared/agents/runtime.ts +14 -0
  10. package/src/shared/claim-code.ts +163 -0
  11. package/src/shared/cli-command.ts +31 -0
  12. package/src/shared/den-config.ts +14 -0
  13. package/src/shared/environment-manager/service-runner.ts +31 -12
  14. package/src/shared/environment-manager/workspaces.ts +31 -0
  15. package/src/shared/web-session.ts +64 -5
  16. package/src/ui/cli/agent-ops.ts +320 -18
  17. package/src/ui/cli/agents-command.ts +89 -2
  18. package/src/ui/cli/doctor-command.ts +29 -3
  19. package/src/ui/cli/doctor-onboard.ts +26 -0
  20. package/src/ui/cli/flag-parser.ts +9 -1
  21. package/src/ui/cli/index.ts +29 -3
  22. package/src/ui/cli/onboard-command.ts +180 -0
  23. package/src/ui/cli/onboard-reads.ts +613 -0
  24. package/src/ui/cli/repo-command.ts +156 -10
  25. package/src/ui/cli/web-command.ts +92 -1
  26. package/src/ui/mcp/api.ts +26 -3
  27. package/src/ui/mcp/server.ts +12 -2
  28. package/src/ui/mcp/tools.ts +427 -1
  29. package/src/ui/server/agent-resume-grant.ts +35 -0
  30. package/src/ui/server/agent-routes.ts +149 -1
  31. package/src/ui/server/approval-routes.ts +390 -0
  32. package/src/ui/server/approvals.ts +416 -0
  33. package/src/ui/server/github-routes.ts +12 -4
  34. package/src/ui/server/held-actions.ts +203 -0
  35. package/src/ui/server/http-helpers.ts +35 -0
  36. package/src/ui/server/index.ts +286 -16
  37. package/src/ui/server/lifecycle-exec.ts +70 -0
  38. package/src/ui/server/onboard-routes.ts +132 -0
  39. package/src/ui/server/pair-routes.ts +7 -4
  40. package/src/ui/server/public/agents-page.js +288 -8
  41. package/src/ui/server/public/agents.html +18 -0
  42. package/src/ui/server/public/error-page.js +27 -3
  43. package/src/ui/server/public/header.js +1 -1
  44. package/src/ui/server/public/i18n/es.js +52 -0
  45. package/src/ui/server/public/i18n/ja.js +55 -0
  46. package/src/ui/server/public/index.html +1 -1
  47. package/src/ui/server/public/local-dev.html +5 -4
  48. package/src/ui/server/public/persona-ui.css +45 -0
  49. package/src/ui/server/public/release.html +0 -1
  50. package/src/ui/server/public/settings-registry.js +3 -1
  51. package/src/ui/server/public/settings.html +9 -1
  52. package/src/ui/server/public/shell.js +336 -0
  53. package/src/ui/server/request-guard.ts +19 -4
  54. package/src/ui/server/route-classes.ts +41 -1
  55. package/src/ui/server/session-routes.ts +11 -3
  56. package/src/ui/server/workspace-routes.ts +21 -10
  57. 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} を開始しました",
@@ -355,7 +355,7 @@
355
355
  white-space: nowrap;
356
356
  }
357
357
  .run-btn:hover:not(:disabled) {
358
- opacity: 0.85;
358
+ filter: brightness(0.92);
359
359
  }
360
360
  .run-btn:disabled {
361
361
  opacity: 0.45;
@@ -344,7 +344,7 @@
344
344
  cursor: pointer;
345
345
  }
346
346
  .new-ws-form button:hover {
347
- opacity: 0.85;
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
- opacity: 0.85;
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
- opacity: 0.4;
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
- opacity: 0.85;
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
+ }
@@ -398,7 +398,6 @@
398
398
  .loading-note .sub {
399
399
  display: block;
400
400
  margin-top: 4px;
401
- opacity: 0.75;
402
401
  font-size: 11px;
403
402
  }
404
403
  .loading-dots::after {
@@ -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
- : "signed in with a one-time link";
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
- opacity: 0.5;
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`. One exception (AG-14.4): MCP agent_resume,
21
- * when mcp.actions lists it, for another agent of its own workspace
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 { agentResumeRefusal, agentResumeTarget } from "./agent-resume-grant.js";
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);