@monster-orange/den 0.4.14 → 0.4.16
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 +194 -0
- package/README.md +7 -7
- package/docs/distributions.md +226 -19
- package/package.json +1 -1
- package/src/shared/actor-token.ts +12 -0
- package/src/shared/agents/conversations.ts +155 -8
- package/src/shared/agents/den-claude.ts +25 -3
- package/src/shared/agents/records.ts +37 -0
- package/src/shared/agents/runtime.ts +11 -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/github-app.ts +7 -0
- package/src/shared/web-session.ts +64 -5
- package/src/ui/cli/agent-ops.ts +372 -23
- package/src/ui/cli/agents-command.ts +71 -4
- 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 +91 -7
- package/src/ui/server/approval-routes.ts +390 -0
- package/src/ui/server/approvals.ts +416 -0
- package/src/ui/server/github-routes.ts +13 -5
- package/src/ui/server/held-actions.ts +203 -0
- package/src/ui/server/http-helpers.ts +35 -0
- package/src/ui/server/index.ts +308 -21
- 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 +378 -30
- package/src/ui/server/public/agents.html +1 -0
- package/src/ui/server/public/error-page.js +97 -18
- package/src/ui/server/public/error.html +18 -0
- package/src/ui/server/public/header.js +1 -1
- package/src/ui/server/public/i18n/es.js +65 -5
- package/src/ui/server/public/i18n/ja.js +67 -5
- 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 +32 -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 +348 -5
- package/src/ui/server/request-guard.ts +2 -0
- package/src/ui/server/route-classes.ts +34 -1
- package/src/ui/server/session-routes.ts +14 -3
- package/src/ui/server/workspace-routes.ts +21 -10
- package/themes/den/den-theme.json +6 -6
|
@@ -366,12 +366,18 @@
|
|
|
366
366
|
"err.signin.title": "Sign in to {name}",
|
|
367
367
|
"err.signin.where":
|
|
368
368
|
"You're opening {name} from another computer. {name} runs on {host}, so it needs to know this is you.",
|
|
369
|
-
"err.signin.
|
|
370
|
-
"
|
|
371
|
-
"err.signin.
|
|
372
|
-
"
|
|
369
|
+
"err.signin.ghStep1":
|
|
370
|
+
"On {host}, run {cmd}, or use Settings › Access there.",
|
|
371
|
+
"err.signin.ghStep2":
|
|
372
|
+
"It shows a code. Enter it at github.com/login/device and approve.",
|
|
373
|
+
"err.signin.ghStep3":
|
|
374
|
+
"Then come back here and press Sign in with GitHub, with the same GitHub account.",
|
|
375
|
+
"err.signin.ghOwner": "Owner: @{login}",
|
|
376
|
+
"err.signin.ghOnly":
|
|
377
|
+
"Only the owner's GitHub account can sign in this way. For anyone else, use Approve this computer.",
|
|
378
|
+
"err.signin.ghLearn": "Learn more",
|
|
373
379
|
"err.signin.githubRefused":
|
|
374
|
-
"@{login} isn't the owner of this {name}.
|
|
380
|
+
"@{login} isn't the owner of this {name}. Only the owner's account can use Sign in with GitHub. Use Approve this computer instead.",
|
|
375
381
|
"err.signin.approveOr": "Or approve this computer",
|
|
376
382
|
"err.signin.approve": "Approve this computer",
|
|
377
383
|
"err.signin.step1": "On {host} (not this computer), run:",
|
|
@@ -383,6 +389,8 @@
|
|
|
383
389
|
"err.signin.noTerminal":
|
|
384
390
|
"Don't have a terminal on {host}? From this computer, run:",
|
|
385
391
|
"err.signin.openTerminal": "Open Terminal",
|
|
392
|
+
"err.signin.sshUserNote":
|
|
393
|
+
"Log in with your {host} account ({user}) and its password, or an SSH key.",
|
|
386
394
|
"err.signin.sshNote":
|
|
387
395
|
"Open Terminal opens ssh://{target} in your terminal app. It needs SSH access to {target}.",
|
|
388
396
|
"err.signin.whyTitle": "Why?",
|
|
@@ -569,6 +577,11 @@
|
|
|
569
577
|
"ag.new.name": "Name (optional)",
|
|
570
578
|
"ag.new.as": "Starts as {id}",
|
|
571
579
|
"ag.new.openRunning": "Open the running one",
|
|
580
|
+
"ag.new.isRunning": "{id} is already running",
|
|
581
|
+
"ag.new.openIt": "Open it",
|
|
582
|
+
"ag.new.isStopped": "{id} is stopped",
|
|
583
|
+
"ag.new.resumeIt": "Resume it",
|
|
584
|
+
"ag.new.orNext": "or Start makes {id}",
|
|
572
585
|
"ag.new.taskPh":
|
|
573
586
|
"e.g. Take the empty-state story. Match the design and add a test.",
|
|
574
587
|
"ag.new.adv": "Advanced: model and permissions",
|
|
@@ -592,12 +605,56 @@
|
|
|
592
605
|
"ag.bring.agent": "Agent {id}",
|
|
593
606
|
"ag.bring.note":
|
|
594
607
|
"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.",
|
|
608
|
+
"ag.keep.model": "Keeps {m} from the conversation.",
|
|
609
|
+
"ag.keep.modelNew": "Runs on {m} from now on.",
|
|
610
|
+
"ag.keep.from":
|
|
611
|
+
"From now on it works as {p}: its role, its permissions ({mode}), Den's tools and guards. Its history stays.",
|
|
612
|
+
"ag.keep.lost":
|
|
613
|
+
"It won't have: {s}. Den runs agents with Den's tools only.",
|
|
614
|
+
"ag.keep.opt": "Keep {m}",
|
|
615
|
+
"ag.keep.optNone": "Keep the model it runs",
|
|
616
|
+
"ag.keep.advNote":
|
|
617
|
+
"The model it runs stays unless you pick another here. Permissions come from the persona.",
|
|
618
|
+
"ag.pc.item": "Change persona…",
|
|
619
|
+
"ag.pc.title": "Change {id}'s persona",
|
|
620
|
+
"ag.pc.note":
|
|
621
|
+
"Den stops it and resumes the same conversation as the new persona. Its id, name, folder and tab stay.",
|
|
622
|
+
"ag.pc.go": "Change persona",
|
|
623
|
+
"ag.pc.going": "Changing…",
|
|
624
|
+
"ag.pc.current": "current",
|
|
625
|
+
"ag.pc.busy": "It's working. Change after this turn, or stop it now?",
|
|
626
|
+
"ag.pc.after": "After this turn",
|
|
627
|
+
"ag.pc.now": "Stop it now",
|
|
628
|
+
"ag.pc.done": "{id} is now {p}",
|
|
629
|
+
"ag.pc.pending": "{id} becomes {p} when this turn ends",
|
|
630
|
+
"ag.more": "More for {id}",
|
|
595
631
|
"ag.bring.go": "Bring in",
|
|
596
632
|
"ag.bring.going": "Bringing in…",
|
|
597
633
|
"ag.bring.done": "Brought in as {id}",
|
|
598
634
|
"ag.bring.now": "just now",
|
|
599
635
|
"ag.bring.min": "{n} min ago",
|
|
600
636
|
"ag.bring.h": "{n} h ago",
|
|
637
|
+
"ap.title": "{agent} wants to:",
|
|
638
|
+
"ap.agent": "The {name} agent",
|
|
639
|
+
"ap.noAgent": "Something on this machine (no agent identity)",
|
|
640
|
+
"ap.noAgentWarn": "If you didn't ask an agent to do this, Deny.",
|
|
641
|
+
"ap.signin.title": "Something is waiting for your approval.",
|
|
642
|
+
"ap.signin": "Sign in to see it: run {cmd} and open the link.",
|
|
643
|
+
"ap.do.workspace_create": 'Create the workspace "{name}"',
|
|
644
|
+
"ap.do.repo_add": 'Add the repo at {path} to workspace "{workspace}"',
|
|
645
|
+
"ap.do.repo_add.new":
|
|
646
|
+
'Add the repo at {path} to a new workspace "{workspace}"',
|
|
647
|
+
"ap.do.service_try": "Start {service}",
|
|
648
|
+
"ap.says": "Agent says:",
|
|
649
|
+
"ap.review": "Review",
|
|
650
|
+
"ap.hide": "Hide details",
|
|
651
|
+
"ap.later": "Later",
|
|
652
|
+
"ap.deny": "Deny",
|
|
653
|
+
"ap.allow": "Allow",
|
|
654
|
+
"ap.waits": "Nothing changes until you allow it. It waits until {time}.",
|
|
655
|
+
"ap.more": "{n} more waiting",
|
|
656
|
+
"ap.failed": "It ran, and failed: {error}",
|
|
657
|
+
"ap.done": "Done: {what}",
|
|
601
658
|
"ag.new.note":
|
|
602
659
|
"Starts in the workspace folder. It keeps running if you close the pane.",
|
|
603
660
|
"ag.new.started": "Started {p} in {w}",
|
|
@@ -717,6 +774,7 @@
|
|
|
717
774
|
"Its token was revoked (web logout --all): stop and resume it, or start it again, to give it a new one",
|
|
718
775
|
"ag.ended":
|
|
719
776
|
"This session has ended. Its record stays until it is resumed or removed.",
|
|
777
|
+
"ag.switched": "Switched to {p}",
|
|
720
778
|
"ag.exitedLine": "[the session exited{code}]",
|
|
721
779
|
"ag.lost": "[disconnected: {why}]",
|
|
722
780
|
"ag.reconnect": "Reconnect",
|
|
@@ -3852,6 +3910,37 @@
|
|
|
3852
3910
|
// server start: a 403 `csrf` (the server restarted) fetches a fresh one
|
|
3853
3911
|
// and retries once. Never sent to another origin.
|
|
3854
3912
|
var CSRF_METHODS = { POST: 1, PUT: 1, PATCH: 1, DELETE: 1 };
|
|
3913
|
+
// 0.4.15: the approval key (web-session.ts). A sign-in's 303 carries it
|
|
3914
|
+
// in the URL fragment (#ak=…), which never reaches a server: keep it in
|
|
3915
|
+
// this origin's localStorage (other localhost ports can't read it),
|
|
3916
|
+
// strip it from the address bar, and send it with every same-origin
|
|
3917
|
+
// change as X-Den-Approve-Key. Only approving needs it.
|
|
3918
|
+
var APPROVE_KEY_STORE = "den.approveKey";
|
|
3919
|
+
(function takeApproveKey() {
|
|
3920
|
+
var m = /(?:^#|&)ak=([A-Za-z0-9_-]{43})(?:&|$)/.exec(location.hash || "");
|
|
3921
|
+
if (!m) return;
|
|
3922
|
+
try {
|
|
3923
|
+
localStorage.setItem(APPROVE_KEY_STORE, m[1]);
|
|
3924
|
+
} catch (e) {
|
|
3925
|
+
/* storage blocked: this browser signs in again to approve */
|
|
3926
|
+
}
|
|
3927
|
+
try {
|
|
3928
|
+
history.replaceState(
|
|
3929
|
+
history.state,
|
|
3930
|
+
"",
|
|
3931
|
+
location.pathname + location.search,
|
|
3932
|
+
);
|
|
3933
|
+
} catch (e) {
|
|
3934
|
+
location.hash = "";
|
|
3935
|
+
}
|
|
3936
|
+
})();
|
|
3937
|
+
function approveKey() {
|
|
3938
|
+
try {
|
|
3939
|
+
return localStorage.getItem(APPROVE_KEY_STORE);
|
|
3940
|
+
} catch (e) {
|
|
3941
|
+
return null;
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3855
3944
|
var csrf = { token: null, pending: null };
|
|
3856
3945
|
function csrfToken(fresh) {
|
|
3857
3946
|
if (fresh) csrf.token = null;
|
|
@@ -3890,6 +3979,9 @@
|
|
|
3890
3979
|
(init && init.headers) || (isRequest(input) ? input.headers : undefined),
|
|
3891
3980
|
);
|
|
3892
3981
|
headers.set("X-Den-CSRF", token);
|
|
3982
|
+
var ak = approveKey();
|
|
3983
|
+
if (ak && !headers.has("X-Den-Approve-Key"))
|
|
3984
|
+
headers.set("X-Den-Approve-Key", ak);
|
|
3893
3985
|
return Object.assign({}, init || {}, { headers: headers });
|
|
3894
3986
|
}
|
|
3895
3987
|
function csrfFetch(input, init) {
|
|
@@ -3958,6 +4050,257 @@
|
|
|
3958
4050
|
if (!apiOffPage) csrfToken(false);
|
|
3959
4051
|
}
|
|
3960
4052
|
|
|
4053
|
+
// ── ON-3: an agent's held write waits for the person ─────────────────
|
|
4054
|
+
// (approval-routes.ts) Every page asks GET /api/approvals every few
|
|
4055
|
+
// seconds while it is in view, and opens the oldest request: Den's own
|
|
4056
|
+
// words for it (the agent's note only quoted), Review (what it will do),
|
|
4057
|
+
// Later, Deny, Allow. Allow and Deny go through the wrapped fetch above,
|
|
4058
|
+
// so they carry the CSRF token and the approval key; the server decides
|
|
4059
|
+
// who may approve (a browser that isn't signed in is told to sign in).
|
|
4060
|
+
// The frame is the Start dialog's (persona-ui.css's .pe-dlg).
|
|
4061
|
+
var ap = { dlg: null, shown: null, later: {}, busy: false, css: null };
|
|
4062
|
+
function apCss() {
|
|
4063
|
+
if (ap.css) return ap.css;
|
|
4064
|
+
ap.css = new Promise(function (done) {
|
|
4065
|
+
if (document.querySelector('link[href="/persona-ui.css"]')) return done();
|
|
4066
|
+
var l = document.createElement("link");
|
|
4067
|
+
l.rel = "stylesheet";
|
|
4068
|
+
l.href = "/persona-ui.css";
|
|
4069
|
+
l.onload = l.onerror = function () {
|
|
4070
|
+
done();
|
|
4071
|
+
};
|
|
4072
|
+
document.head.appendChild(l);
|
|
4073
|
+
});
|
|
4074
|
+
return ap.css;
|
|
4075
|
+
}
|
|
4076
|
+
function apWhat(a) {
|
|
4077
|
+
var p = a.params || {};
|
|
4078
|
+
var key = "ap.do." + a.action + (p.isNew ? ".new" : "");
|
|
4079
|
+
var s = t(key, p);
|
|
4080
|
+
if (s !== key) return s;
|
|
4081
|
+
s = String(a.summary || a.action);
|
|
4082
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
4083
|
+
}
|
|
4084
|
+
function apDialog() {
|
|
4085
|
+
if (ap.dlg) return ap.dlg;
|
|
4086
|
+
var d = document.createElement("dialog");
|
|
4087
|
+
d.className = "pe-dlg den-ap-dlg";
|
|
4088
|
+
d.id = "den-ap-dlg";
|
|
4089
|
+
d.setAttribute("aria-labelledby", "den-ap-h");
|
|
4090
|
+
d.innerHTML =
|
|
4091
|
+
'<form method="dialog">' +
|
|
4092
|
+
'<h2 id="den-ap-h"></h2>' +
|
|
4093
|
+
'<p class="help den-ap-who" id="den-ap-who" hidden></p>' +
|
|
4094
|
+
'<p class="den-ap-what" id="den-ap-what"></p>' +
|
|
4095
|
+
'<p class="help" id="den-ap-note" hidden></p>' +
|
|
4096
|
+
'<pre class="den-ap-det" id="den-ap-det" hidden></pre>' +
|
|
4097
|
+
'<p class="help" id="den-ap-waits"></p>' +
|
|
4098
|
+
'<p class="err" id="den-ap-err" role="alert" hidden></p>' +
|
|
4099
|
+
'<div class="btns">' +
|
|
4100
|
+
'<button type="button" class="den-btn" id="den-ap-review" aria-expanded="false" aria-controls="den-ap-det"></button>' +
|
|
4101
|
+
'<span class="sp help" id="den-ap-more"></span>' +
|
|
4102
|
+
'<button type="button" class="den-btn" id="den-ap-later"></button>' +
|
|
4103
|
+
'<button type="button" class="den-btn" id="den-ap-deny"></button>' +
|
|
4104
|
+
'<button type="button" class="den-btn primary" id="den-ap-allow"></button>' +
|
|
4105
|
+
"</div></form>";
|
|
4106
|
+
document.body.appendChild(d);
|
|
4107
|
+
var $ = function (id) {
|
|
4108
|
+
return d.querySelector("#" + id);
|
|
4109
|
+
};
|
|
4110
|
+
$("den-ap-review").addEventListener("click", function () {
|
|
4111
|
+
var det = $("den-ap-det");
|
|
4112
|
+
det.hidden = !det.hidden;
|
|
4113
|
+
$("den-ap-review").textContent = t(det.hidden ? "ap.review" : "ap.hide");
|
|
4114
|
+
$("den-ap-review").setAttribute("aria-expanded", String(!det.hidden));
|
|
4115
|
+
});
|
|
4116
|
+
$("den-ap-later").addEventListener("click", function () {
|
|
4117
|
+
apLater();
|
|
4118
|
+
});
|
|
4119
|
+
// Esc is Later: the request keeps waiting (and the agent with it).
|
|
4120
|
+
d.addEventListener("cancel", function (e) {
|
|
4121
|
+
e.preventDefault();
|
|
4122
|
+
apLater();
|
|
4123
|
+
});
|
|
4124
|
+
$("den-ap-deny").addEventListener("click", function () {
|
|
4125
|
+
apDecide("deny");
|
|
4126
|
+
});
|
|
4127
|
+
$("den-ap-allow").addEventListener("click", function () {
|
|
4128
|
+
apDecide("allow");
|
|
4129
|
+
});
|
|
4130
|
+
ap.dlg = d;
|
|
4131
|
+
return d;
|
|
4132
|
+
}
|
|
4133
|
+
function apLater() {
|
|
4134
|
+
if (ap.shown) ap.later[ap.shown.id] = 1;
|
|
4135
|
+
apClose();
|
|
4136
|
+
}
|
|
4137
|
+
function apClose() {
|
|
4138
|
+
ap.shown = null;
|
|
4139
|
+
if (ap.dlg && ap.dlg.open) ap.dlg.close();
|
|
4140
|
+
}
|
|
4141
|
+
function apShow(a, more) {
|
|
4142
|
+
var d = apDialog();
|
|
4143
|
+
var $ = function (id) {
|
|
4144
|
+
return d.querySelector("#" + id);
|
|
4145
|
+
};
|
|
4146
|
+
var same = ap.shown && ap.shown.id === a.id;
|
|
4147
|
+
ap.shown = a;
|
|
4148
|
+
// No agent token: say so, and what to do if no agent was asked.
|
|
4149
|
+
$("den-ap-h").textContent = t("ap.title", {
|
|
4150
|
+
agent: a.agent ? t("ap.agent", { name: a.agent }) : t("ap.noAgent"),
|
|
4151
|
+
});
|
|
4152
|
+
$("den-ap-who").hidden = !!a.agent;
|
|
4153
|
+
$("den-ap-who").textContent = a.agent ? "" : t("ap.noAgentWarn");
|
|
4154
|
+
$("den-ap-what").textContent = apWhat(a);
|
|
4155
|
+
var note = $("den-ap-note");
|
|
4156
|
+
note.hidden = !a.note;
|
|
4157
|
+
note.textContent = a.note ? t("ap.says") + " “" + a.note + "”" : "";
|
|
4158
|
+
$("den-ap-det").textContent = (a.details || []).join("\n");
|
|
4159
|
+
var time = new Date(a.expiresAt);
|
|
4160
|
+
$("den-ap-waits").textContent = t("ap.waits", {
|
|
4161
|
+
time: isNaN(time)
|
|
4162
|
+
? ""
|
|
4163
|
+
: time.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
|
4164
|
+
});
|
|
4165
|
+
$("den-ap-more").textContent = more ? t("ap.more", { n: more }) : "";
|
|
4166
|
+
$("den-ap-later").textContent = t("ap.later");
|
|
4167
|
+
$("den-ap-deny").textContent = t("ap.deny");
|
|
4168
|
+
$("den-ap-allow").textContent = t("ap.allow");
|
|
4169
|
+
if (!same) {
|
|
4170
|
+
$("den-ap-det").hidden = true;
|
|
4171
|
+
$("den-ap-review").textContent = t("ap.review");
|
|
4172
|
+
$("den-ap-review").setAttribute("aria-expanded", "false");
|
|
4173
|
+
$("den-ap-err").hidden = true;
|
|
4174
|
+
}
|
|
4175
|
+
if (!d.open)
|
|
4176
|
+
apCss().then(function () {
|
|
4177
|
+
if (ap.shown && ap.shown.id === a.id && !d.open) d.showModal();
|
|
4178
|
+
});
|
|
4179
|
+
}
|
|
4180
|
+
function apError(msg) {
|
|
4181
|
+
var e = apDialog().querySelector("#den-ap-err");
|
|
4182
|
+
e.textContent = msg;
|
|
4183
|
+
e.hidden = false;
|
|
4184
|
+
}
|
|
4185
|
+
function apDecide(decision) {
|
|
4186
|
+
var a = ap.shown;
|
|
4187
|
+
if (!a || ap.busy) return;
|
|
4188
|
+
ap.busy = true;
|
|
4189
|
+
var btns = ap.dlg.querySelectorAll("button");
|
|
4190
|
+
for (var i = 0; i < btns.length; i++) btns[i].disabled = true;
|
|
4191
|
+
fetch("/api/approvals/" + encodeURIComponent(a.id) + "/decide", {
|
|
4192
|
+
method: "POST",
|
|
4193
|
+
headers: { "Content-Type": "application/json" },
|
|
4194
|
+
body: JSON.stringify({ decision: decision }),
|
|
4195
|
+
})
|
|
4196
|
+
.then(function (r) {
|
|
4197
|
+
return r.json().then(
|
|
4198
|
+
function (b) {
|
|
4199
|
+
return { ok: r.ok, b: b || {} };
|
|
4200
|
+
},
|
|
4201
|
+
function () {
|
|
4202
|
+
return { ok: false, b: {} };
|
|
4203
|
+
},
|
|
4204
|
+
);
|
|
4205
|
+
})
|
|
4206
|
+
.then(
|
|
4207
|
+
function (x) {
|
|
4208
|
+
if (!x.ok) {
|
|
4209
|
+
apError(x.b.error || t("ap.failed", { error: x.b.code || "?" }));
|
|
4210
|
+
return;
|
|
4211
|
+
}
|
|
4212
|
+
var res = x.b.result;
|
|
4213
|
+
if (decision === "allow" && res && res.ok === false)
|
|
4214
|
+
toast(t("ap.failed", { error: res.error || "?" }));
|
|
4215
|
+
else if (decision === "allow")
|
|
4216
|
+
toast(t("ap.done", { what: apWhat(a) }));
|
|
4217
|
+
apClose();
|
|
4218
|
+
apPoll();
|
|
4219
|
+
},
|
|
4220
|
+
function (e) {
|
|
4221
|
+
apError(String((e && e.message) || e));
|
|
4222
|
+
},
|
|
4223
|
+
)
|
|
4224
|
+
.then(function () {
|
|
4225
|
+
ap.busy = false;
|
|
4226
|
+
for (var j = 0; j < btns.length; j++) btns[j].disabled = false;
|
|
4227
|
+
});
|
|
4228
|
+
}
|
|
4229
|
+
// Reading what waits needs what approving needs (approvals.ts): the
|
|
4230
|
+
// session's CSRF token and the approval key, on a GET too. A browser
|
|
4231
|
+
// without them is told only that something waits, and to sign in.
|
|
4232
|
+
function apList(fresh) {
|
|
4233
|
+
var ak = approveKey();
|
|
4234
|
+
// No approval key: only the count, to say "sign in to see it".
|
|
4235
|
+
if (!ak)
|
|
4236
|
+
return rawFetch("/api/approvals?count=1", {
|
|
4237
|
+
cache: "no-store",
|
|
4238
|
+
credentials: "same-origin",
|
|
4239
|
+
}).then(function (r) {
|
|
4240
|
+
return r.ok
|
|
4241
|
+
? r.json().then(function (b) {
|
|
4242
|
+
return { code: "signin_to_approve", waiting: b && b.waiting };
|
|
4243
|
+
})
|
|
4244
|
+
: null;
|
|
4245
|
+
});
|
|
4246
|
+
return csrfToken(fresh).then(function (token) {
|
|
4247
|
+
var h = { "X-Den-Approve-Key": ak };
|
|
4248
|
+
if (token) h["X-Den-CSRF"] = token;
|
|
4249
|
+
return rawFetch("/api/approvals", {
|
|
4250
|
+
cache: "no-store",
|
|
4251
|
+
credentials: "same-origin",
|
|
4252
|
+
headers: h,
|
|
4253
|
+
}).then(function (r) {
|
|
4254
|
+
return r.json().then(
|
|
4255
|
+
function (b) {
|
|
4256
|
+
if (!fresh && r.status === 403 && b && b.code === "csrf")
|
|
4257
|
+
return apList(true);
|
|
4258
|
+
return b;
|
|
4259
|
+
},
|
|
4260
|
+
function () {
|
|
4261
|
+
return null;
|
|
4262
|
+
},
|
|
4263
|
+
);
|
|
4264
|
+
});
|
|
4265
|
+
});
|
|
4266
|
+
}
|
|
4267
|
+
function apSignin(b) {
|
|
4268
|
+
if (b && b.code === "signin_to_approve" && b.waiting > 0)
|
|
4269
|
+
showBanner("approvals", {
|
|
4270
|
+
title: t("ap.signin.title"),
|
|
4271
|
+
text: t("ap.signin", { cmd: "__DEN_CLI__ web login" }),
|
|
4272
|
+
});
|
|
4273
|
+
else hideBanner("approvals");
|
|
4274
|
+
}
|
|
4275
|
+
function apPoll() {
|
|
4276
|
+
if (document.hidden || ap.busy) return;
|
|
4277
|
+
apList(false).then(
|
|
4278
|
+
function (b) {
|
|
4279
|
+
apSignin(b);
|
|
4280
|
+
var list = (b && b.ok && b.pending) || [];
|
|
4281
|
+
var open = list.filter(function (a) {
|
|
4282
|
+
return !ap.later[a.id];
|
|
4283
|
+
});
|
|
4284
|
+
if (!open.length) return apClose();
|
|
4285
|
+
var cur =
|
|
4286
|
+
(ap.shown &&
|
|
4287
|
+
open.filter(function (a) {
|
|
4288
|
+
return a.id === ap.shown.id;
|
|
4289
|
+
})[0]) ||
|
|
4290
|
+
open[0];
|
|
4291
|
+
apShow(cur, open.length - 1);
|
|
4292
|
+
},
|
|
4293
|
+
function () {},
|
|
4294
|
+
);
|
|
4295
|
+
}
|
|
4296
|
+
if (!apiOffPage && !configBad && rawFetch) {
|
|
4297
|
+
setInterval(apPoll, 3000);
|
|
4298
|
+
document.addEventListener("visibilitychange", function () {
|
|
4299
|
+
if (!document.hidden) apPoll();
|
|
4300
|
+
});
|
|
4301
|
+
setTimeout(apPoll, 300);
|
|
4302
|
+
}
|
|
4303
|
+
|
|
3961
4304
|
// ── the UI language (R19): a user pref ───────────────────────────────
|
|
3962
4305
|
// /api/settings `language` (user.yml), then a reload: the server serves
|
|
3963
4306
|
// every page with that language's catalog from its first paint.
|
|
@@ -110,6 +110,8 @@ export interface GuardSession {
|
|
|
110
110
|
key: string;
|
|
111
111
|
/** The session's own CSRF token. */
|
|
112
112
|
csrf: string;
|
|
113
|
+
/** sha256 of its approval key (approvals.ts); unset before 0.4.15. */
|
|
114
|
+
approveKeyHash?: string;
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
/** How a request reached Den. `edge` and `lan` are beyond loopback. */
|
|
@@ -95,7 +95,8 @@ export const ROUTES: readonly RouteEntry[] = [
|
|
|
95
95
|
"/theme-favicon",
|
|
96
96
|
"/theme-favicon/*",
|
|
97
97
|
"/vendor/*",
|
|
98
|
-
"/report
|
|
98
|
+
"/report",
|
|
99
|
+
"/report/*",
|
|
99
100
|
"/reports/security/*",
|
|
100
101
|
].map((p) => R(p)),
|
|
101
102
|
R("/_den/throw", "a test-only route (DEN_DEBUG_ROUTES): it throws"),
|
|
@@ -174,6 +175,30 @@ export const ROUTES: readonly RouteEntry[] = [
|
|
|
174
175
|
R("/api/health", "runs the manifests' health probes: read-only checks"),
|
|
175
176
|
R("/api/test-sources", "git rev-parse in each repo: read-only"),
|
|
176
177
|
R("/api/manifest/validate", "GET validates a file at a path: read-only"),
|
|
178
|
+
// ON-3 (onboard-routes.ts): reads only; nothing written, no repo tool run.
|
|
179
|
+
R("/api/onboard/status", "what's set up and the next step (file reads)"),
|
|
180
|
+
R(
|
|
181
|
+
"/api/onboard/repos",
|
|
182
|
+
"lists the git repos in a folder in home; never follows a symlink",
|
|
183
|
+
),
|
|
184
|
+
R(
|
|
185
|
+
"/api/onboard/manifest",
|
|
186
|
+
"drafts and validates a side-car manifest; writes nothing",
|
|
187
|
+
),
|
|
188
|
+
// ON-3's held writes (approval-routes.ts): asking runs nothing; Allow is
|
|
189
|
+
// the person's signed-in browser only (approvals.ts approverAllowed).
|
|
190
|
+
R("/api/approvals", "what waits for the person (never an agent's token)"),
|
|
191
|
+
R("/api/approvals/:id", "one held request: the asker's own, or the person's"),
|
|
192
|
+
M(
|
|
193
|
+
["POST"],
|
|
194
|
+
"/api/approvals",
|
|
195
|
+
"holds an agent's write for the person; nothing runs until they allow it",
|
|
196
|
+
),
|
|
197
|
+
M(
|
|
198
|
+
["POST"],
|
|
199
|
+
"/api/approvals/:id/decide",
|
|
200
|
+
"the person's Allow runs the held write once; Deny runs nothing",
|
|
201
|
+
),
|
|
177
202
|
R("/api/service/logs", "reads (and follows) a service's log"),
|
|
178
203
|
R("/api/service/taillog", "tails a service's log (tail, docker logs)"),
|
|
179
204
|
R(
|
|
@@ -342,6 +367,11 @@ export const ROUTES: readonly RouteEntry[] = [
|
|
|
342
367
|
"/api/workspaces/:ws/agents/:id/fresh",
|
|
343
368
|
"starts an agent afresh: a runtime in a PTY",
|
|
344
369
|
),
|
|
370
|
+
S(
|
|
371
|
+
["POST"],
|
|
372
|
+
"/api/workspaces/:ws/agents/:id/persona",
|
|
373
|
+
"changes an agent's persona (AG-18): its session stops and the same conversation resumes, a runtime in a PTY; a person's only",
|
|
374
|
+
),
|
|
345
375
|
S(
|
|
346
376
|
["POST"],
|
|
347
377
|
"/api/workspaces/:ws/agents/:id/relaunch",
|
|
@@ -443,6 +473,7 @@ export const REGEX_ROUTES: Readonly<Record<string, readonly string[]>> = {
|
|
|
443
473
|
"/api/workspaces/:ws/agents/:id/remove",
|
|
444
474
|
"/api/workspaces/:ws/agents/:id/name",
|
|
445
475
|
"/api/workspaces/:ws/agents/:id/paths",
|
|
476
|
+
"/api/workspaces/:ws/agents/:id/persona",
|
|
446
477
|
],
|
|
447
478
|
"agent-routes.ts:RUNTIME_PATH": ["/api/agents/runtimes/:id/path"],
|
|
448
479
|
"agent-routes.ts:PERSONA_PATH": ["/api/personas/:id"],
|
|
@@ -456,6 +487,8 @@ export const REGEX_ROUTES: Readonly<Record<string, readonly string[]>> = {
|
|
|
456
487
|
"github-routes.ts:LINK_PATH": ["/api/web/owner/link/:id"],
|
|
457
488
|
"pair-routes.ts:POLL_PATH": ["/api/signin/code/:id"],
|
|
458
489
|
"index.ts:LOGIN_WAIT_PATH": ["/api/web/logins/:id"],
|
|
490
|
+
"approval-routes.ts:ONE_PATH": ["/api/approvals/:id"],
|
|
491
|
+
"approval-routes.ts:DECIDE_PATH": ["/api/approvals/:id/decide"],
|
|
459
492
|
};
|
|
460
493
|
|
|
461
494
|
/** Whether `pathname` matches a table path. */
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* `<cli> agents list` does (agent-ops.ts agentRows), and adds the plain
|
|
10
10
|
* sessions (`sessions start`), labelled kind "session". An agent whose
|
|
11
11
|
* actor token `web logout --all` revoked is marked (tokenRevoked). The
|
|
12
|
-
*
|
|
13
|
-
* (AG-6a
|
|
12
|
+
* agent with one of Den's held writes waiting for the person (ON-3,
|
|
13
|
+
* approval-routes.ts) is `waiting` (AG-6a); a runtime's own permission
|
|
14
|
+
* prompts are not seen here yet. Asking never starts a session host.
|
|
14
15
|
*
|
|
15
16
|
* The io route bridges one WebSocket to one attachment on the session host
|
|
16
17
|
* (its own connection to the host, so a browser and the CLI, or two
|
|
@@ -78,6 +79,8 @@ export interface SessionRouteDeps {
|
|
|
78
79
|
/** Whether workspaces.yml names `ws` (active or not). */
|
|
79
80
|
workspaceExists?: (ws: string) => boolean;
|
|
80
81
|
pty?: () => PtyLoad;
|
|
82
|
+
/** The agent ids with an approval waiting for the person (AG-6a, ON-3). */
|
|
83
|
+
waitingAgents?: () => ReadonlySet<string>;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
interface Resolved {
|
|
@@ -87,6 +90,7 @@ interface Resolved {
|
|
|
87
90
|
activeWorkspaces: () => string[];
|
|
88
91
|
workspaceExists: (ws: string) => boolean;
|
|
89
92
|
pty: () => PtyLoad;
|
|
93
|
+
waitingAgents: () => ReadonlySet<string>;
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
/** A workspace name as `?ws=` may give it. */
|
|
@@ -116,6 +120,7 @@ function resolve(d: SessionRouteDeps = {}): Resolved {
|
|
|
116
120
|
ws,
|
|
117
121
|
)),
|
|
118
122
|
pty: d.pty ?? (() => loadPty()),
|
|
123
|
+
waitingAgents: d.waitingAgents ?? (() => new Set<string>()),
|
|
119
124
|
};
|
|
120
125
|
}
|
|
121
126
|
|
|
@@ -169,6 +174,8 @@ export interface ListedSession {
|
|
|
169
174
|
/** The host has it (running, or ended with its scrollback): a pane can attach. */
|
|
170
175
|
attachable: boolean;
|
|
171
176
|
resumable?: boolean;
|
|
177
|
+
/** AG-18: Change persona… applies (its runtime can resume). */
|
|
178
|
+
personaChange?: boolean;
|
|
172
179
|
/** Why only Start fresh applies (AG-14.2): "<runtime> can't resume a conversation". */
|
|
173
180
|
noResume?: string;
|
|
174
181
|
/** When it was stopped, seen to end, or found interrupted (AG-14.1). */
|
|
@@ -189,7 +196,8 @@ export interface ListedSession {
|
|
|
189
196
|
/** The list, from the host's sessions and the agent records. */
|
|
190
197
|
export function listedSessions(
|
|
191
198
|
sessions: SessionInfo[],
|
|
192
|
-
d: Pick<Resolved, "stateDir" | "dist" | "activeWorkspaces"
|
|
199
|
+
d: Pick<Resolved, "stateDir" | "dist" | "activeWorkspaces"> &
|
|
200
|
+
Partial<Pick<Resolved, "waitingAgents">>,
|
|
193
201
|
/** A workspace asked for by name (`?ws=`), listed even when inactive. */
|
|
194
202
|
also?: string,
|
|
195
203
|
): ListedSession[] {
|
|
@@ -204,6 +212,7 @@ export function listedSessions(
|
|
|
204
212
|
active.has(r.workspace),
|
|
205
213
|
);
|
|
206
214
|
const revoked = revokedAgentIds(d.stateDir());
|
|
215
|
+
const waiting = d.waitingAgents?.() ?? new Set<string>();
|
|
207
216
|
const out: ListedSession[] = [];
|
|
208
217
|
const agentSessions = new Set<string>();
|
|
209
218
|
noteEnded(d.stateDir(), mine, records);
|
|
@@ -236,12 +245,14 @@ export function listedSessions(
|
|
|
236
245
|
...(r.startedAt ? { startedAt: r.startedAt } : {}),
|
|
237
246
|
attachable: Boolean(s),
|
|
238
247
|
resumable: r.resumable,
|
|
248
|
+
...(r.personaChange ? { personaChange: true } : {}),
|
|
239
249
|
...(r.noResume ? { noResume: r.noResume } : {}),
|
|
240
250
|
...(r.stoppedAt ? { stoppedAt: r.stoppedAt } : {}),
|
|
241
251
|
...(r.endedAt ? { endedAt: r.endedAt } : {}),
|
|
242
252
|
...(r.interruptedAt ? { interruptedAt: r.interruptedAt } : {}),
|
|
243
253
|
...(r.rotation?.due ? { rotation: r.rotation } : {}),
|
|
244
254
|
...(revoked.has(rec.id) && s && !s.exited ? { tokenRevoked: true } : {}),
|
|
255
|
+
...(waiting.has(rec.id) && s && !s.exited ? { waiting: true } : {}),
|
|
245
256
|
});
|
|
246
257
|
}
|
|
247
258
|
for (const s of mine) {
|
|
@@ -344,6 +344,25 @@ const availableReposCache = singleFlight<AvailableRepos>(2_000);
|
|
|
344
344
|
* Same contract as the original inline handlers: uses return sendJson/send400
|
|
345
345
|
* to end the response. Caller checks res.writableEnded to know if handled.
|
|
346
346
|
*/
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Why a workspace can't be created with this name, or null: the Workspaces
|
|
350
|
+
* page's create and ON-3's held workspace_create (approval-routes.ts) both
|
|
351
|
+
* ask this.
|
|
352
|
+
*/
|
|
353
|
+
export function workspaceCreateRefusal(
|
|
354
|
+
name: string | undefined,
|
|
355
|
+
): string | null {
|
|
356
|
+
if (!name) return "name required";
|
|
357
|
+
// eslint-disable-next-line no-control-regex
|
|
358
|
+
if (/["'\\/<>\x00-\x1f]/.test(name))
|
|
359
|
+
return "name must not contain quotes, slashes, or control characters";
|
|
360
|
+
if (name.length > 64) return "name must be 64 characters or fewer";
|
|
361
|
+
if (readWorkspacesFile().workspaces[name])
|
|
362
|
+
return `workspace "${name}" already exists`;
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
347
366
|
export async function handleWorkspaceRoutes(
|
|
348
367
|
req: http.IncomingMessage,
|
|
349
368
|
res: http.ServerResponse,
|
|
@@ -639,16 +658,8 @@ export async function handleWorkspaceRoutes(
|
|
|
639
658
|
try {
|
|
640
659
|
const body = await readBody(req);
|
|
641
660
|
const { name } = JSON.parse(body) as { name?: string };
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if (/["'\\/<>\x00-\x1f]/.test(name))
|
|
645
|
-
return send400(
|
|
646
|
-
res,
|
|
647
|
-
"name must not contain quotes, slashes, or control characters",
|
|
648
|
-
);
|
|
649
|
-
const file = readWorkspacesFile();
|
|
650
|
-
if (file.workspaces[name])
|
|
651
|
-
return send400(res, `workspace "${name}" already exists`);
|
|
661
|
+
const refusal = workspaceCreateRefusal(name);
|
|
662
|
+
if (refusal || !name) return send400(res, refusal ?? "name required");
|
|
652
663
|
createWorkspace(name, { services: {} });
|
|
653
664
|
return sendJson(res, { ok: true, name });
|
|
654
665
|
} catch (e) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"schema": "den.theme/v1",
|
|
4
4
|
"id": "den",
|
|
5
5
|
"name": "Den",
|
|
6
|
-
"version": "2.0.
|
|
6
|
+
"version": "2.0.1",
|
|
7
7
|
"description": "Den's default: a sheltered workspace in walnut and umber neutrals with a moss-green accent. System fonts.",
|
|
8
8
|
"author": "Monster Orange",
|
|
9
9
|
"license": "MIT",
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
"border": "#dcd3c7",
|
|
38
38
|
"border-strong": "#8d7f6d",
|
|
39
39
|
"accent": "#557a2a",
|
|
40
|
-
"accent-text": "#
|
|
40
|
+
"accent-text": "#415e1f",
|
|
41
41
|
"accent-fill": "#4a6b23",
|
|
42
42
|
"on-accent": "#ffffff",
|
|
43
|
-
"ok": "#
|
|
44
|
-
"warn": "#
|
|
45
|
-
"bad": "#
|
|
46
|
-
"remote": "#
|
|
43
|
+
"ok": "#27613b",
|
|
44
|
+
"warn": "#774c0e",
|
|
45
|
+
"bad": "#983124",
|
|
46
|
+
"remote": "#205d6e",
|
|
47
47
|
"shadow": "0 8px 24px rgb(48 36 24 / 0.14)",
|
|
48
48
|
"bar": "linear-gradient(90deg, #557a2a 0%, #8a6a48 100%)"
|
|
49
49
|
},
|