@monster-orange/den 0.4.25 → 0.4.27
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 +55 -0
- package/docs/distributions.md +74 -20
- package/package.json +1 -1
- package/src/shared/agents/runtime.ts +2 -2
- package/src/shared/hub.ts +304 -82
- package/src/shared/secrets/onepassword.ts +135 -20
- package/src/ui/cli/agent-ops.ts +17 -3
- package/src/ui/cli/doctor-command.ts +11 -2
- package/src/ui/server/agent-routes.ts +79 -1
- package/src/ui/server/hub-connect.ts +433 -0
- package/src/ui/server/public/i18n/es.js +2 -0
- package/src/ui/server/public/i18n/ja.js +2 -0
- package/src/ui/server/public/settings-registry.js +196 -33
- package/src/ui/server/public/settings.html +23 -0
- package/src/ui/server/public/shell.js +1 -0
- package/src/ui/server/public/topbar.js +5 -1
- package/src/ui/server/route-classes.ts +17 -0
|
@@ -917,8 +917,19 @@ export const DEN_SETTINGS = [
|
|
|
917
917
|
category: "agents",
|
|
918
918
|
label: "Hub",
|
|
919
919
|
description:
|
|
920
|
-
"Every agent gets Hub's tools: search Hub, publish docs and mocks there. Its team
|
|
921
|
-
keywords: [
|
|
920
|
+
"Connect this install to your company's Hub: sign in there and approve it. Every agent then gets Hub's tools: search Hub, publish docs and mocks there. Its team comes from this install's token, its project from the workspace's repos.",
|
|
921
|
+
keywords: [
|
|
922
|
+
"hub",
|
|
923
|
+
"den hub",
|
|
924
|
+
"connect",
|
|
925
|
+
"disconnect",
|
|
926
|
+
"mcp",
|
|
927
|
+
"team",
|
|
928
|
+
"company",
|
|
929
|
+
"token",
|
|
930
|
+
"docs",
|
|
931
|
+
"mocks",
|
|
932
|
+
],
|
|
922
933
|
scope: "user",
|
|
923
934
|
control: { type: "custom", render: renderHub },
|
|
924
935
|
keys: ["hub_allow_tools"],
|
|
@@ -1459,20 +1470,82 @@ export function rotationHtml(b) {
|
|
|
1459
1470
|
);
|
|
1460
1471
|
}
|
|
1461
1472
|
|
|
1462
|
-
/** HUB-1.
|
|
1463
|
-
|
|
1464
|
-
|
|
1473
|
+
/** HUB-1.7: what came back from Hub's redirect (?hub=…), when the server has no words for it. */
|
|
1474
|
+
const HUB_FLASH = {
|
|
1475
|
+
connected: [true, "Connected to Hub."],
|
|
1476
|
+
denied: [false, "Not connected: the request was declined on Hub."],
|
|
1477
|
+
expired: [
|
|
1478
|
+
false,
|
|
1479
|
+
"That connect link was used or too old: press Connect again.",
|
|
1480
|
+
],
|
|
1481
|
+
failed: [false, "Connect didn't finish: press Connect again."],
|
|
1482
|
+
};
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* HUB-1.5/1.7: Settings › Agents › Hub (b: GET /api/hub; flash: what the
|
|
1486
|
+
* last Connect came to). Not connected: Hub's address and Connect.
|
|
1487
|
+
* Connected: the company (a link), role, who signed in, this install,
|
|
1488
|
+
* Disconnect. A token Hub refuses offers Reconnect; a role that no longer
|
|
1489
|
+
* may connect says to ask an admin (never a retry).
|
|
1490
|
+
*/
|
|
1491
|
+
export function hubHtml(b, flash) {
|
|
1492
|
+
b = b || { configured: false };
|
|
1493
|
+
const state = b.configured
|
|
1494
|
+
? b.state || (b.connected ? "connected" : "no_token")
|
|
1495
|
+
: "no_token";
|
|
1496
|
+
const note = (t, attr) => `<p class="set-note" ${attr}>${escR(t)}</p>`;
|
|
1497
|
+
const flashHtml = flash
|
|
1498
|
+
? `<p class="set-note hub-flash ${flash.ok ? "ok" : "warn"}" role="status" data-hub-flash="${flash.ok ? "ok" : "failed"}">${escR(flash.message)}</p>`
|
|
1499
|
+
: "";
|
|
1500
|
+
if (state === "bad_config")
|
|
1501
|
+
return (
|
|
1502
|
+
flashHtml +
|
|
1503
|
+
note(b.problem || "hub in user.yml is not valid", "data-hub-problem")
|
|
1504
|
+
);
|
|
1505
|
+
if (state === "no_token")
|
|
1506
|
+
return (
|
|
1507
|
+
flashHtml +
|
|
1508
|
+
`<form class="hub-connect" data-hub-connect>` +
|
|
1509
|
+
`<label for="hub-url">Hub's address</label> ` +
|
|
1510
|
+
`<input type="url" id="hub-url" class="den-mono" spellcheck="false" autocomplete="url" placeholder="https://hub.example.com" value="${escR(b.url || "")}">` +
|
|
1511
|
+
` <button type="submit" class="btn sm primary" data-hub-action="connect">Connect</button>` +
|
|
1512
|
+
`</form>` +
|
|
1513
|
+
`<p class="set-note">Connect opens Hub. Sign in there and approve this install; you come back here connected. The token is kept in this install's secrets store, never shown.</p>` +
|
|
1514
|
+
(b.problem && b.url && !/^not connected yet/.test(b.problem)
|
|
1515
|
+
? note(b.problem, "data-hub-problem")
|
|
1516
|
+
: "") +
|
|
1517
|
+
`<div class="rt-err" role="alert" hidden></div>`
|
|
1518
|
+
);
|
|
1519
|
+
const status = {
|
|
1520
|
+
connected: ["ok", "Connected"],
|
|
1521
|
+
rejected: ["warn", "Token not accepted"],
|
|
1522
|
+
forbidden: ["warn", "Not allowed"],
|
|
1523
|
+
unreachable: ["warn", "Can't reach Hub"],
|
|
1524
|
+
}[state] || ["warn", "Not connected"];
|
|
1525
|
+
const team = b.team
|
|
1526
|
+
? b.teamUrl && /^https?:\/\//i.test(b.teamUrl)
|
|
1527
|
+
? `<a href="${escR(b.teamUrl)}" target="_blank" rel="noopener noreferrer">${escR(b.team)}</a>`
|
|
1528
|
+
: escR(b.team)
|
|
1529
|
+
: "";
|
|
1465
1530
|
const rows = [
|
|
1466
|
-
["
|
|
1531
|
+
["Hub", `<span class="den-mono">${escR(b.url || "")}</span>`],
|
|
1467
1532
|
[
|
|
1468
1533
|
"Status",
|
|
1469
|
-
|
|
1470
|
-
? `<span class="set-tag ok" data-hub-state="connected">Connected</span>`
|
|
1471
|
-
: `<span class="set-tag warn" data-hub-state="down">Not connected</span>`,
|
|
1534
|
+
`<span class="set-tag ${status[0]}" data-hub-state="${escR(state)}">${status[1]}</span>`,
|
|
1472
1535
|
],
|
|
1473
|
-
...(
|
|
1536
|
+
...(team ? [["Company", team]] : []),
|
|
1537
|
+
...(b.role ? [["Role", escR(b.role)]] : []),
|
|
1538
|
+
...(b.login ? [["Signed in as", escR(b.login)]] : []),
|
|
1474
1539
|
...(b.label ? [["This install", escR(b.label)]] : []),
|
|
1475
1540
|
];
|
|
1541
|
+
const why =
|
|
1542
|
+
state === "rejected"
|
|
1543
|
+
? "Hub no longer accepts this install's token (revoked or expired). Reconnect signs you in again."
|
|
1544
|
+
: state === "forbidden"
|
|
1545
|
+
? "Your role on Hub no longer allows Den installs. Ask a Hub admin to change your role, then Reconnect."
|
|
1546
|
+
: state === "unreachable"
|
|
1547
|
+
? b.problem || "Hub didn't answer."
|
|
1548
|
+
: "";
|
|
1476
1549
|
const tools =
|
|
1477
1550
|
`<label class="hub-tools"><input type="checkbox" id="hub-allow-tools"${b.allowTools !== false ? " checked" : ""}> Agents use Hub's tools without asking</label>` +
|
|
1478
1551
|
`<p class="set-note" data-hub-tools-note>${
|
|
@@ -1481,46 +1554,136 @@ export function hubHtml(b) {
|
|
|
1481
1554
|
: "Agents ask before each Hub tool runs."
|
|
1482
1555
|
}</p>`;
|
|
1483
1556
|
return (
|
|
1557
|
+
flashHtml +
|
|
1484
1558
|
`<dl class="hub-st" data-hub>` +
|
|
1485
1559
|
rows.map(([k, v]) => `<dt>${escR(k)}</dt><dd>${v}</dd>`).join("") +
|
|
1486
1560
|
`</dl>` +
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1561
|
+
(why ? note(why, "data-hub-problem") : "") +
|
|
1562
|
+
`<div class="hub-acts">` +
|
|
1563
|
+
(state === "rejected"
|
|
1564
|
+
? `<button type="button" class="btn sm primary" data-hub-action="connect">Reconnect</button> `
|
|
1565
|
+
: "") +
|
|
1566
|
+
`<button type="button" class="btn sm" data-hub-action="disconnect">Disconnect</button>` +
|
|
1567
|
+
`</div>` +
|
|
1568
|
+
`<div class="rt-err" role="alert" hidden></div>` +
|
|
1569
|
+
tools
|
|
1491
1570
|
);
|
|
1492
1571
|
}
|
|
1493
1572
|
|
|
1494
|
-
/**
|
|
1573
|
+
/** Read and clear ?hub=… (set by Hub's redirect back through /hub/callback). */
|
|
1574
|
+
function takeHubFlash() {
|
|
1575
|
+
try {
|
|
1576
|
+
const u = new URL(location.href);
|
|
1577
|
+
const v = u.searchParams.get("hub");
|
|
1578
|
+
if (!v) return null;
|
|
1579
|
+
u.searchParams.delete("hub");
|
|
1580
|
+
history.replaceState(history.state, "", u.pathname + u.search + u.hash);
|
|
1581
|
+
return v;
|
|
1582
|
+
} catch {
|
|
1583
|
+
return null;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/** HUB-1.5/1.7: the Hub row: always shown, so a person can Connect. */
|
|
1495
1588
|
function renderHub(host, ctx) {
|
|
1496
|
-
|
|
1497
|
-
|
|
1589
|
+
return drawHub(host, ctx, takeHubFlash(), null);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/** Draw the row; `back` is ?hub=… from Hub's redirect, `flash` words to show. */
|
|
1593
|
+
function drawHub(host, ctx, back, flash) {
|
|
1498
1594
|
// Under an invalid config /api/hub answers 422: ask nothing, show nothing.
|
|
1499
|
-
if (ctx.shell?.configInvalid?.() === true)
|
|
1595
|
+
if (ctx.shell?.configInvalid?.() === true) {
|
|
1596
|
+
host.innerHTML = "";
|
|
1597
|
+
return Promise.resolve();
|
|
1598
|
+
}
|
|
1500
1599
|
return ctx.shell
|
|
1501
1600
|
.api("GET", "/api/hub")
|
|
1502
1601
|
.then((b) => {
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
if (
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
box.disabled = true;
|
|
1510
|
-
ctx.shell
|
|
1511
|
-
.api("POST", "/api/settings", { hub_allow_tools: box.checked })
|
|
1512
|
-
.then(() => renderHub(host, ctx))
|
|
1513
|
-
.catch(() => {
|
|
1514
|
-
box.checked = !box.checked;
|
|
1515
|
-
box.disabled = false;
|
|
1516
|
-
});
|
|
1517
|
-
});
|
|
1602
|
+
let f = flash;
|
|
1603
|
+
if (!f && b && b.connect) f = b.connect;
|
|
1604
|
+
else if (!f && back && HUB_FLASH[back])
|
|
1605
|
+
f = { ok: HUB_FLASH[back][0], message: HUB_FLASH[back][1] };
|
|
1606
|
+
host.innerHTML = hubHtml(b, f);
|
|
1607
|
+
wireHub(host, ctx);
|
|
1518
1608
|
})
|
|
1519
1609
|
.catch(() => {
|
|
1520
1610
|
host.innerHTML = "";
|
|
1521
1611
|
});
|
|
1522
1612
|
}
|
|
1523
1613
|
|
|
1614
|
+
function wireHub(host, ctx) {
|
|
1615
|
+
const err = (msg, retry) => {
|
|
1616
|
+
const e = host.querySelector(".rt-err");
|
|
1617
|
+
if (!e) return;
|
|
1618
|
+
e.hidden = !msg;
|
|
1619
|
+
e.textContent = msg || "";
|
|
1620
|
+
if (retry) {
|
|
1621
|
+
const btn = document.createElement("button");
|
|
1622
|
+
btn.type = "button";
|
|
1623
|
+
btn.className = "btn sm";
|
|
1624
|
+
btn.dataset.hubAction = "force";
|
|
1625
|
+
btn.textContent = "Remove it here anyway";
|
|
1626
|
+
btn.addEventListener("click", () => disconnect(true, btn));
|
|
1627
|
+
e.append(" ", btn);
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
const busy = (on) =>
|
|
1631
|
+
host.querySelectorAll("button, input").forEach((x) => (x.disabled = on));
|
|
1632
|
+
const connect = () => {
|
|
1633
|
+
const typed = host.querySelector("#hub-url");
|
|
1634
|
+
busy(true);
|
|
1635
|
+
err("");
|
|
1636
|
+
ctx.shell
|
|
1637
|
+
.api("POST", "/api/hub/connect", typed ? { url: typed.value } : {})
|
|
1638
|
+
.then((r) => {
|
|
1639
|
+
// Off to Hub: sign in and approve there; Hub sends the person back.
|
|
1640
|
+
location.assign(r.location);
|
|
1641
|
+
})
|
|
1642
|
+
.catch((e) => {
|
|
1643
|
+
busy(false);
|
|
1644
|
+
err(e?.message || "Connect didn't start");
|
|
1645
|
+
});
|
|
1646
|
+
};
|
|
1647
|
+
const disconnect = (force, btn) => {
|
|
1648
|
+
busy(true);
|
|
1649
|
+
err("");
|
|
1650
|
+
ctx.shell
|
|
1651
|
+
.api("POST", "/api/hub/disconnect", force ? { force: true } : {})
|
|
1652
|
+
.then((r) => drawHub(host, ctx, null, { ok: true, message: r.message }))
|
|
1653
|
+
.catch((e) => {
|
|
1654
|
+
busy(false);
|
|
1655
|
+
if (btn) btn.remove();
|
|
1656
|
+
err(e?.message || "Not disconnected", !force);
|
|
1657
|
+
});
|
|
1658
|
+
};
|
|
1659
|
+
const form = host.querySelector("[data-hub-connect]");
|
|
1660
|
+
if (form)
|
|
1661
|
+
form.addEventListener("submit", (ev) => {
|
|
1662
|
+
ev.preventDefault();
|
|
1663
|
+
connect();
|
|
1664
|
+
});
|
|
1665
|
+
host
|
|
1666
|
+
.querySelectorAll('button[data-hub-action="connect"]')
|
|
1667
|
+
.forEach(
|
|
1668
|
+
(x) => x.type === "button" && x.addEventListener("click", connect),
|
|
1669
|
+
);
|
|
1670
|
+
host
|
|
1671
|
+
.querySelector('[data-hub-action="disconnect"]')
|
|
1672
|
+
?.addEventListener("click", () => disconnect(false));
|
|
1673
|
+
const box = host.querySelector("#hub-allow-tools");
|
|
1674
|
+
if (box)
|
|
1675
|
+
box.addEventListener("change", () => {
|
|
1676
|
+
box.disabled = true;
|
|
1677
|
+
ctx.shell
|
|
1678
|
+
.api("POST", "/api/settings", { hub_allow_tools: box.checked })
|
|
1679
|
+
.then(() => drawHub(host, ctx, null, null))
|
|
1680
|
+
.catch(() => {
|
|
1681
|
+
box.checked = !box.checked;
|
|
1682
|
+
box.disabled = false;
|
|
1683
|
+
});
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1524
1687
|
/** The Rotation row: this install's values, the overrides, + Add override. */
|
|
1525
1688
|
function renderRotation(host, ctx) {
|
|
1526
1689
|
const err = () => host.querySelector(".rt-err");
|
|
@@ -473,6 +473,29 @@
|
|
|
473
473
|
min-width: 0;
|
|
474
474
|
overflow-wrap: anywhere;
|
|
475
475
|
}
|
|
476
|
+
/* HUB-1.7: Connect / Disconnect. */
|
|
477
|
+
.hub-connect {
|
|
478
|
+
display: flex;
|
|
479
|
+
flex-wrap: wrap;
|
|
480
|
+
gap: 6px 8px;
|
|
481
|
+
align-items: center;
|
|
482
|
+
}
|
|
483
|
+
.hub-connect input {
|
|
484
|
+
flex: 1 1 240px;
|
|
485
|
+
min-width: 0;
|
|
486
|
+
}
|
|
487
|
+
.hub-acts {
|
|
488
|
+
display: flex;
|
|
489
|
+
flex-wrap: wrap;
|
|
490
|
+
gap: 8px;
|
|
491
|
+
margin-top: 10px;
|
|
492
|
+
}
|
|
493
|
+
.hub-flash.ok {
|
|
494
|
+
color: var(--ok, inherit);
|
|
495
|
+
}
|
|
496
|
+
.hub-flash.warn {
|
|
497
|
+
color: var(--warn, inherit);
|
|
498
|
+
}
|
|
476
499
|
.hub-tools {
|
|
477
500
|
display: flex;
|
|
478
501
|
gap: 8px;
|
|
@@ -309,6 +309,7 @@
|
|
|
309
309
|
"lock.checking": "Checking…",
|
|
310
310
|
"lock.note": "Den uses it only for sudo. Agents can't enter it.",
|
|
311
311
|
"lock.saved": "Admin password kept for this session",
|
|
312
|
+
"lock.savedTo": "Admin password kept for this session and saved to {s}",
|
|
312
313
|
"lock.saveFailed": "Kept for this session, but saving to {s} failed: {err}",
|
|
313
314
|
"lock.wrong": "Incorrect password — try again",
|
|
314
315
|
"lock.reqFailed": "Request failed — is the server running?",
|
|
@@ -1459,7 +1459,11 @@ function openLock() {
|
|
|
1459
1459
|
s: sudo.vault.label || sudo.vault.backend,
|
|
1460
1460
|
err: res.vaultError,
|
|
1461
1461
|
})
|
|
1462
|
-
:
|
|
1462
|
+
: saveToVault
|
|
1463
|
+
? t("lock.savedTo", {
|
|
1464
|
+
s: sudo.vault.label || sudo.vault.backend,
|
|
1465
|
+
})
|
|
1466
|
+
: t("lock.saved"),
|
|
1463
1467
|
);
|
|
1464
1468
|
settleSudo(true);
|
|
1465
1469
|
return;
|
|
@@ -392,6 +392,20 @@ export const ROUTES: readonly RouteEntry[] = [
|
|
|
392
392
|
"/api/hub",
|
|
393
393
|
"HUB-1.5: Den Hub: its URL, connected or not, the team and the install's label (never the token)",
|
|
394
394
|
),
|
|
395
|
+
M(
|
|
396
|
+
["POST"],
|
|
397
|
+
"/api/hub/connect",
|
|
398
|
+
"HUB-1.7: start Connect to Hub: a PKCE verifier and single-use state kept here; answers Hub's /connect URL",
|
|
399
|
+
),
|
|
400
|
+
M(
|
|
401
|
+
["POST"],
|
|
402
|
+
"/api/hub/disconnect",
|
|
403
|
+
"HUB-1.7: revoke this install's Hub token on Hub, then remove it here",
|
|
404
|
+
),
|
|
405
|
+
R(
|
|
406
|
+
"/hub/callback",
|
|
407
|
+
"HUB-1.7: Hub sends the person back here. Sign-in free: the session cookie is SameSite=Strict, so Hub's redirect doesn't carry it. It acts only for a state this server issued to a signed-in Connect (single-use, 10 min), with the PKCE verifier only this server holds; anything else changes nothing",
|
|
408
|
+
),
|
|
395
409
|
M(
|
|
396
410
|
["PUT"],
|
|
397
411
|
"/api/agents/rotation",
|
|
@@ -488,6 +502,9 @@ export const SIGNIN_FREE: readonly string[] = [
|
|
|
488
502
|
// SEC-2a: approve this computer with a code, from the signed-out page.
|
|
489
503
|
"/api/signin/code",
|
|
490
504
|
"/api/signin/code/:id",
|
|
505
|
+
// HUB-1.7: Hub's redirect back (no session cookie on it): the state
|
|
506
|
+
// this server issued is the proof (hub-connect.ts).
|
|
507
|
+
"/hub/callback",
|
|
491
508
|
];
|
|
492
509
|
|
|
493
510
|
/**
|