@monster-orange/den 0.4.3 → 0.4.4
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 +179 -0
- package/README.md +9 -9
- package/docs/distributions.md +215 -65
- package/package.json +1 -1
- package/src/shared/agents/catalog-runtime.ts +178 -0
- package/src/shared/agents/catalog.ts +343 -0
- package/src/shared/agents/den-claude.ts +14 -18
- package/src/shared/agents/runtime-search.ts +142 -15
- package/src/shared/agents/runtime.ts +18 -1
- package/src/shared/agents/runtimes.ts +23 -1
- package/src/shared/auth/sudo-vault.ts +16 -0
- package/src/shared/den-config.ts +57 -3
- package/src/shared/edge/status.ts +134 -0
- package/src/shared/environment-manager/health.ts +82 -8
- package/src/shared/environment-manager/workspaces.ts +54 -0
- package/src/shared/health/not-checked.ts +42 -0
- package/src/shared/health/rollup.ts +4 -0
- package/src/shared/process/spawn-streaming.ts +8 -3
- package/src/shared/sessions/client.ts +5 -1
- package/src/shared/sessions/host.ts +29 -1
- package/src/shared/sessions/protocol.ts +6 -1
- package/src/shared/sessions/types.ts +6 -0
- package/src/shared/stacks/status.ts +24 -0
- package/src/shared/strategies/registry.ts +5 -0
- package/src/shared/types/index.ts +6 -0
- package/src/ui/cli/agent-ops.ts +210 -19
- package/src/ui/cli/agents-command.ts +50 -21
- package/src/ui/cli/edge-command.ts +16 -3
- package/src/ui/cli/health-display.ts +4 -1
- package/src/ui/mcp/scope.ts +23 -3
- package/src/ui/mcp/server.ts +31 -7
- package/src/ui/mcp/tools.ts +327 -20
- package/src/ui/mcp/views.ts +450 -0
- package/src/ui/server/agent-routes.ts +118 -28
- package/src/ui/server/index.ts +77 -6
- package/src/ui/server/public/agent-start.js +46 -7
- package/src/ui/server/public/agents-page.js +232 -97
- package/src/ui/server/public/agents-section.js +3 -5
- package/src/ui/server/public/agents.html +12 -28
- package/src/ui/server/public/graph-model.js +2 -0
- package/src/ui/server/public/header.js +2 -3
- package/src/ui/server/public/health-rollup.js +20 -8
- package/src/ui/server/public/i18n/es.js +15 -13
- package/src/ui/server/public/i18n/ja.js +14 -13
- package/src/ui/server/public/index.html +17 -4
- package/src/ui/server/public/local-dev.html +28 -17
- package/src/ui/server/public/services.html +21 -5
- package/src/ui/server/public/settings-page.js +3 -2
- package/src/ui/server/public/settings-registry.js +225 -0
- package/src/ui/server/public/settings.html +90 -1
- package/src/ui/server/public/shell.js +28 -23
- package/src/ui/server/public/topbar.js +166 -206
- package/src/ui/server/readonly-routes.ts +29 -5
- package/src/ui/server/request-guard.ts +1 -1
- package/src/ui/server/route-classes.ts +14 -2
- package/src/ui/server/session-routes.ts +9 -1
- package/src/ui/server/sudo-store.ts +170 -6
- package/src/ui/server/workspace-routes.ts +10 -3
package/src/ui/server/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type ContainerHealthTarget,
|
|
18
18
|
} from "../../shared/stacks/compose-health.js";
|
|
19
19
|
import { healthCheckFields, type HealthCheckType } from "./health-row.js";
|
|
20
|
+
import { notCheckedReason } from "../../shared/health/not-checked.js";
|
|
20
21
|
import { hasRuntime, repoKind } from "../../shared/repo-kind.js";
|
|
21
22
|
import * as http from "http";
|
|
22
23
|
import * as net from "net";
|
|
@@ -162,6 +163,7 @@ import {
|
|
|
162
163
|
saveSudoToVault,
|
|
163
164
|
sudoRef,
|
|
164
165
|
sudoVaultStatus,
|
|
166
|
+
sudoVaultWhere,
|
|
165
167
|
SUDO_REF_NAME,
|
|
166
168
|
} from "../../shared/auth/sudo-vault.js";
|
|
167
169
|
import {
|
|
@@ -275,6 +277,10 @@ interface HealthResult {
|
|
|
275
277
|
pending?: boolean;
|
|
276
278
|
status?: number;
|
|
277
279
|
error?: string;
|
|
280
|
+
/** A 3xx answer's target (MCP-2.4): ok null, not down. */
|
|
281
|
+
redirect?: string;
|
|
282
|
+
/** With ok null and nothing checked: why not (MCP-2.4). */
|
|
283
|
+
why?: string;
|
|
278
284
|
/** Where the app opens for this env (open-in-new-tab), or null. */
|
|
279
285
|
openUrl?: string | null;
|
|
280
286
|
/** Extra named links for this env. */
|
|
@@ -716,12 +722,14 @@ const CONFIG_FREE_PAGES = new Set(["/settings", "/settings.html", "/logs"]);
|
|
|
716
722
|
import {
|
|
717
723
|
setSudoPassword,
|
|
718
724
|
clearSudoPassword,
|
|
725
|
+
clearSudoShims,
|
|
719
726
|
closeLockForThisRequest,
|
|
720
727
|
inRequestLockScope,
|
|
721
728
|
getSudoEnv,
|
|
722
729
|
prewarmSudo,
|
|
723
730
|
hasSudoPassword,
|
|
724
731
|
hasPasswordlessSudo,
|
|
732
|
+
watchSudo,
|
|
725
733
|
} from "./sudo-store.js";
|
|
726
734
|
import {
|
|
727
735
|
guardStdio,
|
|
@@ -730,6 +738,9 @@ import {
|
|
|
730
738
|
requestShutdown,
|
|
731
739
|
} from "./shutdown-guard.js";
|
|
732
740
|
|
|
741
|
+
/** This server saved the admin password to the vault or loaded it from there. */
|
|
742
|
+
let sudoInVault = false;
|
|
743
|
+
|
|
733
744
|
// ── HTTP helpers ─────────────────────────────────────────────────────────────
|
|
734
745
|
import { sweepLogs } from "../../shared/process/log-rotation.js";
|
|
735
746
|
import {
|
|
@@ -1843,6 +1854,8 @@ async function handleRequest(
|
|
|
1843
1854
|
compose: ComposeHealthTarget | null;
|
|
1844
1855
|
/** A `type: container` check: answered from the shared listing. */
|
|
1845
1856
|
container: ContainerHealthTarget | null;
|
|
1857
|
+
/** The repo has no manifest (the row is kept, unchecked). */
|
|
1858
|
+
noManifest?: boolean;
|
|
1846
1859
|
meta: Pick<HealthResult, "runs" | "target" | "focused" | "workspaces">;
|
|
1847
1860
|
}> = [];
|
|
1848
1861
|
const focusName = getFocusedWorkspaceName();
|
|
@@ -1885,6 +1898,7 @@ async function handleRequest(
|
|
|
1885
1898
|
org: orgOf(resolved.repoName, catalog),
|
|
1886
1899
|
compose: null,
|
|
1887
1900
|
container: null,
|
|
1901
|
+
noManifest: true,
|
|
1888
1902
|
meta: {
|
|
1889
1903
|
runs: "none",
|
|
1890
1904
|
target: null,
|
|
@@ -2012,6 +2026,7 @@ async function handleRequest(
|
|
|
2012
2026
|
expect,
|
|
2013
2027
|
compose,
|
|
2014
2028
|
container,
|
|
2029
|
+
noManifest,
|
|
2015
2030
|
}) => {
|
|
2016
2031
|
if (checkType !== "none") {
|
|
2017
2032
|
const started = Date.now();
|
|
@@ -2078,6 +2093,13 @@ async function handleRequest(
|
|
|
2078
2093
|
isEntrypoint,
|
|
2079
2094
|
...meta,
|
|
2080
2095
|
ok: null,
|
|
2096
|
+
why: notCheckedReason({
|
|
2097
|
+
env,
|
|
2098
|
+
runs: meta.runs,
|
|
2099
|
+
target: meta.target,
|
|
2100
|
+
manifest: !noManifest,
|
|
2101
|
+
conventional,
|
|
2102
|
+
}),
|
|
2081
2103
|
};
|
|
2082
2104
|
};
|
|
2083
2105
|
// Stream each result the moment it lands. The checks run concurrently but
|
|
@@ -2244,9 +2266,10 @@ async function handleRequest(
|
|
|
2244
2266
|
Connection: "keep-alive",
|
|
2245
2267
|
"X-Accel-Buffering": "no",
|
|
2246
2268
|
});
|
|
2247
|
-
|
|
2269
|
+
// LK-0: the command's sudo without the password is a needs-sudo event.
|
|
2270
|
+
const emit = watchSudo((obj: unknown): void => {
|
|
2248
2271
|
res.write(`data: ${JSON.stringify(obj)}\n\n`);
|
|
2249
|
-
};
|
|
2272
|
+
}, `${repoName}/${svcName} ${cmdName}`);
|
|
2250
2273
|
// Every fail here is before anything ran: a refusal or a lookup miss.
|
|
2251
2274
|
// Recorded, so failed attempts show in the activity trail (F2).
|
|
2252
2275
|
const fail = (msg: string): void => {
|
|
@@ -3864,8 +3887,13 @@ async function handleRequest(
|
|
|
3864
3887
|
}
|
|
3865
3888
|
|
|
3866
3889
|
// ── API: sudo password ────────────────────────────────────────────────────
|
|
3890
|
+
// LK-0: the admin password is for sudo, and nothing else: no route asks
|
|
3891
|
+
// for it as an access check. A run that is about to run sudo without it
|
|
3892
|
+
// streams a needs-sudo event (watchSudo); the page asks, then runs again.
|
|
3867
3893
|
// POST /api/sudo-password body: { password: string, saveToVault?: boolean }
|
|
3868
|
-
//
|
|
3894
|
+
// POST /api/sudo-password/forget → { ok, vaultCleared, vaultError? }
|
|
3895
|
+
// GET /api/sudo-password/status → { set, passwordless,
|
|
3896
|
+
// saved: "vault" | "session" | null, vault: { configured, label } }
|
|
3869
3897
|
// GET /api/sudo-password/vault-status → { backend, label, configured,
|
|
3870
3898
|
// backendAvailable, reason?, hasStored, opAvailable (deprecated) }
|
|
3871
3899
|
// POST /api/sudo-password/load-from-vault → { ok, reason?, message? }
|
|
@@ -3901,6 +3929,7 @@ async function handleRequest(
|
|
|
3901
3929
|
try {
|
|
3902
3930
|
await saveSudoToVault(password);
|
|
3903
3931
|
savedToVault = true;
|
|
3932
|
+
sudoInVault = true;
|
|
3904
3933
|
} catch (err) {
|
|
3905
3934
|
vaultError = (err as Error).message;
|
|
3906
3935
|
}
|
|
@@ -3912,10 +3941,51 @@ async function handleRequest(
|
|
|
3912
3941
|
}
|
|
3913
3942
|
|
|
3914
3943
|
if (pathname === "/api/sudo-password/status") {
|
|
3915
|
-
// passwordless: NOPASSWD sudoer (or warm cache) —
|
|
3944
|
+
// passwordless: NOPASSWD sudoer (or warm cache) — sudo needs none.
|
|
3945
|
+
// saved (Settings, LK-0): "vault" once this server saved it there or
|
|
3946
|
+
// loaded it from there, "session" when it is held in memory only.
|
|
3947
|
+
const set = hasSudoPassword();
|
|
3916
3948
|
return sendJson(res, {
|
|
3917
|
-
set
|
|
3949
|
+
set,
|
|
3918
3950
|
passwordless: hasPasswordlessSudo(),
|
|
3951
|
+
saved: sudoInVault ? "vault" : set ? "session" : null,
|
|
3952
|
+
vault: sudoVaultWhere(),
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
// Forget (Settings, LK-0): the password this server holds goes, and the
|
|
3957
|
+
// vault's copy when this server knows there is one (written empty, which
|
|
3958
|
+
// every read takes as none). sudo's own cache is dropped too.
|
|
3959
|
+
if (pathname === "/api/sudo-password/forget") {
|
|
3960
|
+
if (req.method !== "POST") {
|
|
3961
|
+
res.writeHead(405);
|
|
3962
|
+
res.end();
|
|
3963
|
+
return;
|
|
3964
|
+
}
|
|
3965
|
+
clearSudoPassword();
|
|
3966
|
+
try {
|
|
3967
|
+
execSync("sudo -k", {
|
|
3968
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
3969
|
+
timeout: CLI_SPAWN_TIMEOUT_MS,
|
|
3970
|
+
});
|
|
3971
|
+
} catch {
|
|
3972
|
+
/* best-effort */
|
|
3973
|
+
}
|
|
3974
|
+
let vaultCleared = false;
|
|
3975
|
+
let vaultError: string | undefined;
|
|
3976
|
+
if (sudoInVault) {
|
|
3977
|
+
try {
|
|
3978
|
+
await saveSudoToVault("");
|
|
3979
|
+
vaultCleared = true;
|
|
3980
|
+
sudoInVault = false;
|
|
3981
|
+
} catch (err) {
|
|
3982
|
+
vaultError = (err as Error).message;
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
return sendJson(res, {
|
|
3986
|
+
ok: true,
|
|
3987
|
+
vaultCleared,
|
|
3988
|
+
...(vaultError ? { vaultError } : {}),
|
|
3919
3989
|
});
|
|
3920
3990
|
}
|
|
3921
3991
|
|
|
@@ -3960,6 +4030,7 @@ async function handleRequest(
|
|
|
3960
4030
|
return sendJson(res, { ok: false, reason: "stale" });
|
|
3961
4031
|
}
|
|
3962
4032
|
setSudoPassword(password);
|
|
4033
|
+
sudoInVault = true;
|
|
3963
4034
|
return sendJson(res, { ok: true });
|
|
3964
4035
|
}
|
|
3965
4036
|
|
|
@@ -4747,7 +4818,7 @@ async function stopDaemons(): Promise<void> {
|
|
|
4747
4818
|
}
|
|
4748
4819
|
|
|
4749
4820
|
async function handleShutdown(signal: string): Promise<void> {
|
|
4750
|
-
|
|
4821
|
+
clearSudoShims();
|
|
4751
4822
|
// The registry lives in the state dir, which is resolved from the
|
|
4752
4823
|
// distribution root on every call: if that can no longer be found (den.yml
|
|
4753
4824
|
// moved or removed while the server ran), still exit. Never product() here:
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* Start is disabled and the line says why and how to fix it (install it,
|
|
14
14
|
* or set agents.runtimes.<id>.bin in the user.yml it names, or run
|
|
15
15
|
* `<cli> agents doctor`). Nothing fails only after Start.
|
|
16
|
+
*
|
|
17
|
+
* AG-0.5: the runtimes come installed first (the server's order). One
|
|
18
|
+
* that runs as its own CLI, without Den's tools yet, says "Den tools:
|
|
19
|
+
* coming"; one not installed is disabled; generic is startable only for a
|
|
20
|
+
* persona with an argv template. Nothing is offered that can't start.
|
|
16
21
|
*/
|
|
17
22
|
|
|
18
23
|
function escHtml(s) {
|
|
@@ -53,10 +58,28 @@ export function defaultRuntimeOf(personas, runtimes, personaId) {
|
|
|
53
58
|
return (runtimes ?? []).find((r) => r.default)?.id ?? "";
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
/** A catalog runtime the search didn't find (not a path someone set). */
|
|
62
|
+
const notFound = (r) =>
|
|
63
|
+
!r.available &&
|
|
64
|
+
r.denTools !== undefined &&
|
|
65
|
+
r.source !== "setting" &&
|
|
66
|
+
r.source !== "workspace";
|
|
67
|
+
|
|
68
|
+
/** Why runtime `r` can't start for persona `personaId`, or "" when it can. */
|
|
69
|
+
export function cantStart(personas, r, personaId, t) {
|
|
70
|
+
if (!r.available) return r.why || t("ag.unavailable");
|
|
71
|
+
// generic runs the persona's own argv: none, nothing to run.
|
|
72
|
+
if (r.id === "generic") {
|
|
73
|
+
const p = (personas ?? []).find((x) => x.id === personaId);
|
|
74
|
+
if (!p?.argv?.length) return t("ag.rt.needsArgv");
|
|
75
|
+
}
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
/**
|
|
57
80
|
* The runtime select's options. The first ("" = the persona's) reads
|
|
58
81
|
* "<name> (persona default)"; one that can't run here is disabled and
|
|
59
|
-
* says why.
|
|
82
|
+
* says why; one without Den's tools yet says so.
|
|
60
83
|
*/
|
|
61
84
|
export function runtimeOptions(personas, runtimes, personaId, t) {
|
|
62
85
|
const name = runtimeName(
|
|
@@ -69,12 +92,18 @@ export function runtimeOptions(personas, runtimes, personaId, t) {
|
|
|
69
92
|
(runtimes ?? [])
|
|
70
93
|
.map((r) => {
|
|
71
94
|
const label = runtimeName(runtimes, r.id);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
const no = cantStart(personas, r, personaId, t);
|
|
96
|
+
const parts = [
|
|
97
|
+
label,
|
|
98
|
+
r.version,
|
|
99
|
+
r.denTools === false ? t("ag.rt.denTools") : "",
|
|
100
|
+
];
|
|
101
|
+
return no
|
|
102
|
+
? `<option value="${escHtml(r.id)}" disabled>${escHtml(
|
|
103
|
+
`${label} — ${notFound(r) ? t("ag.rt.notInstalled") : no}`,
|
|
75
104
|
)}</option>`
|
|
76
|
-
: `<option value="${escHtml(r.id)}"
|
|
77
|
-
|
|
105
|
+
: `<option value="${escHtml(r.id)}">${escHtml(
|
|
106
|
+
parts.filter(Boolean).join(" · "),
|
|
78
107
|
)}</option>`;
|
|
79
108
|
})
|
|
80
109
|
.join("")
|
|
@@ -103,6 +132,13 @@ export function readiness(data, personaId, runtimeId, t) {
|
|
|
103
132
|
const name = runtimeName(runtimes, r.id);
|
|
104
133
|
const mono = (x) => `<span class="mono">${escHtml(x)}</span>`;
|
|
105
134
|
if (r.available) {
|
|
135
|
+
const no = cantStart(personas, r, personaId, t);
|
|
136
|
+
if (no)
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
tone: "warn",
|
|
140
|
+
html: escHtml(t("ag.rt.refused", { name, why: no })),
|
|
141
|
+
};
|
|
106
142
|
if (!r.path) return { ok: true, tone: "none", html: "" };
|
|
107
143
|
return {
|
|
108
144
|
ok: true,
|
|
@@ -114,7 +150,10 @@ export function readiness(data, personaId, runtimeId, t) {
|
|
|
114
150
|
: t("ag.rt.readyNoVer", { name }),
|
|
115
151
|
) +
|
|
116
152
|
" " +
|
|
117
|
-
mono(r.path)
|
|
153
|
+
mono(r.path) +
|
|
154
|
+
(r.denTools === false
|
|
155
|
+
? ` <span class="tagx" data-den-tools="coming">${escHtml(t("ag.rt.denTools"))}</span> ${escHtml(t("ag.rt.ownCli"))}`
|
|
156
|
+
: ""),
|
|
118
157
|
};
|
|
119
158
|
}
|
|
120
159
|
const file = data?.userConfig || "config/local/user.yml";
|