@monster-orange/den 0.4.2 → 0.4.3
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 +94 -0
- package/README.md +7 -7
- package/docs/distributions.md +126 -13
- package/package.json +1 -1
- package/src/shared/agents/den-claude.ts +57 -12
- package/src/shared/agents/generic.ts +1 -0
- package/src/shared/agents/runtime-search.ts +416 -0
- package/src/shared/agents/runtime.ts +26 -1
- package/src/shared/agents/runtimes.ts +30 -3
- package/src/shared/den-config.ts +36 -0
- package/src/shared/edge/render.ts +31 -0
- package/src/ui/cli/agent-ops.ts +107 -19
- package/src/ui/cli/agents-command.ts +99 -3
- package/src/ui/cli/sessions-command.ts +9 -4
- package/src/ui/cli/web-command.ts +53 -8
- package/src/ui/server/agent-routes.ts +9 -2
- package/src/ui/server/edge-exposure.ts +143 -0
- package/src/ui/server/index.ts +94 -19
- package/src/ui/server/public/agent-start.js +202 -0
- package/src/ui/server/public/agents-page.js +83 -17
- package/src/ui/server/public/agents-section.js +6 -18
- package/src/ui/server/public/agents.html +70 -0
- package/src/ui/server/public/i18n/es.js +11 -0
- package/src/ui/server/public/i18n/ja.js +11 -0
- package/src/ui/server/public/local-dev.html +43 -14
- package/src/ui/server/public/settings-registry.js +1 -1
- package/src/ui/server/public/shell.js +40 -0
- package/src/ui/server/public/topbar.js +127 -28
- package/src/ui/server/request-guard.ts +122 -7
- package/src/ui/server/ui-assets.ts +2 -0
|
@@ -19,12 +19,17 @@
|
|
|
19
19
|
* session's /api/sessions/<id>/*: agent start and resume mint tokens.
|
|
20
20
|
* 403 `agent_forbidden`. WS-3a (0.4.2) adds the workspace check at
|
|
21
21
|
* agentRefusal's seam.
|
|
22
|
-
* - Sign-in (§4, route-classes.ts): beyond loopback (
|
|
23
|
-
*
|
|
24
|
-
* or a bearer. Otherwise 401
|
|
25
|
-
* does, shell-grade ones
|
|
26
|
-
*
|
|
27
|
-
*
|
|
22
|
+
* - Sign-in (§4, route-classes.ts): beyond loopback (a LAN name, a peer
|
|
23
|
+
* that isn't loopback, or the edge hostname for a client on another
|
|
24
|
+
* machine) every route needs a session or a bearer. Otherwise 401
|
|
25
|
+
* `signin_required`. On loopback no route does, shell-grade ones
|
|
26
|
+
* included (Ward, 2026-11-06: the lock, R8, is the only prompt there;
|
|
27
|
+
* Host, Origin and CSRF cover CSRF and DNS rebinding). SIGNIN_FREE
|
|
28
|
+
* routes never need one.
|
|
29
|
+
* - The edge counts as local (0.4.3, edgeClientIsLocal): the edge hostname
|
|
30
|
+
* from an edge peer gets loopback's treatment unless the edge's
|
|
31
|
+
* X-Forwarded-For names another machine, or this server is deliberately
|
|
32
|
+
* exposed (`exposed`: a non-loopback DEN_BIND_HOST bind).
|
|
28
33
|
* - Origin, on POST/PUT/PATCH/DELETE and every WebSocket upgrade: an
|
|
29
34
|
* allowed host (http or https). No Origin needs a valid bearer.
|
|
30
35
|
* Otherwise 403 `origin_rejected`.
|
|
@@ -45,6 +50,7 @@
|
|
|
45
50
|
import { randomBytes, timingSafeEqual, createHash } from "crypto";
|
|
46
51
|
import type * as http from "http";
|
|
47
52
|
import * as net from "net";
|
|
53
|
+
import * as os from "os";
|
|
48
54
|
import {
|
|
49
55
|
actorFromHeaders,
|
|
50
56
|
type Actor,
|
|
@@ -125,6 +131,18 @@ export interface GuardContext {
|
|
|
125
131
|
* server's port gets 421. Unset: taken as yes (a unit test).
|
|
126
132
|
*/
|
|
127
133
|
peerIsEdge?: (req: http.IncomingMessage) => boolean;
|
|
134
|
+
/**
|
|
135
|
+
* This machine's own addresses and container/VM bridges (localNet), for
|
|
136
|
+
* telling whether an edge request's client is this machine. Unset: none
|
|
137
|
+
* (a unit test): only loopback and the Docker ranges count as local.
|
|
138
|
+
*/
|
|
139
|
+
localNet?: () => LocalNet;
|
|
140
|
+
/**
|
|
141
|
+
* A deliberate network exposure (a non-loopback DEN_BIND_HOST bind other
|
|
142
|
+
* than the Docker bridge the edge uses on Linux): the edge hostname
|
|
143
|
+
* needs a sign-in too, as before 0.4.3.
|
|
144
|
+
*/
|
|
145
|
+
exposed?: boolean;
|
|
128
146
|
}
|
|
129
147
|
|
|
130
148
|
export type GuardResult =
|
|
@@ -263,6 +281,97 @@ export function isEdgePeer(addr: string | undefined): boolean {
|
|
|
263
281
|
);
|
|
264
282
|
}
|
|
265
283
|
|
|
284
|
+
/** An address without its zone, IPv4-mapped prefix or brackets, lowercase. */
|
|
285
|
+
function normAddress(a: string): string {
|
|
286
|
+
return a
|
|
287
|
+
.trim()
|
|
288
|
+
.toLowerCase()
|
|
289
|
+
.replace(/^\[|\]$/g, "")
|
|
290
|
+
.replace(/%.*$/, "")
|
|
291
|
+
.replace(/^::ffff:(?=\d+\.)/, "");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** This machine's own addresses, and the container/VM bridges it hosts. */
|
|
295
|
+
export interface LocalNet {
|
|
296
|
+
/** Every interface address (loopback, LAN, a VPN's such as Tailscale's 100.x). */
|
|
297
|
+
own: readonly string[];
|
|
298
|
+
/** IPv4 subnets of its container/VM bridges (Docker, OrbStack, VMs). */
|
|
299
|
+
bridges: readonly { address: string; netmask: string }[];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Interface names of container/VM bridges: their peers are this machine's. */
|
|
303
|
+
const BRIDGE_IFACE =
|
|
304
|
+
/^(docker\d*|br-|bridge\d+|vmnet|virbr|veth|cni|podman|vboxnet)/;
|
|
305
|
+
|
|
306
|
+
/** localNet from os.networkInterfaces() (or a given table, in a test). */
|
|
307
|
+
export function localNet(
|
|
308
|
+
ifaces: ReturnType<typeof os.networkInterfaces> = os.networkInterfaces(),
|
|
309
|
+
): LocalNet {
|
|
310
|
+
const own: string[] = [];
|
|
311
|
+
const bridges: { address: string; netmask: string }[] = [];
|
|
312
|
+
for (const [name, list] of Object.entries(ifaces))
|
|
313
|
+
for (const a of list ?? []) {
|
|
314
|
+
own.push(normAddress(a.address));
|
|
315
|
+
if (a.family === "IPv4" && BRIDGE_IFACE.test(name))
|
|
316
|
+
bridges.push({ address: a.address, netmask: a.netmask });
|
|
317
|
+
}
|
|
318
|
+
return { own: [...new Set(own)], bridges };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function ipv4(a: string): number {
|
|
322
|
+
return a.split(".").reduce((n, o) => n * 256 + Number(o), 0);
|
|
323
|
+
}
|
|
324
|
+
function inSubnet(a: string, net4: { address: string; netmask: string }) {
|
|
325
|
+
if (!net.isIPv4(a) || !net.isIPv4(net4.address)) return false;
|
|
326
|
+
const m = ipv4(net4.netmask);
|
|
327
|
+
// Unsigned compare of the masked halves (no 32-bit sign games).
|
|
328
|
+
const lo = (x: number) => x - (x % (2 ** 32 - m));
|
|
329
|
+
return m > 0 && lo(ipv4(a)) === lo(ipv4(net4.address));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The original client X-Forwarded-For names: its left-most entry, as an
|
|
334
|
+
* IP (a port or brackets dropped), or undefined when there is none.
|
|
335
|
+
*/
|
|
336
|
+
export function forwardedClient(xff: string | undefined): string | undefined {
|
|
337
|
+
const first = xff?.split(",")[0]?.trim();
|
|
338
|
+
if (!first) return undefined;
|
|
339
|
+
let a = first;
|
|
340
|
+
const b = /^\[([^\]]+)\](?::\d+)?$/.exec(a);
|
|
341
|
+
if (b) a = b[1];
|
|
342
|
+
else if (/^\d+\.\d+\.\d+\.\d+:\d+$/.test(a)) a = a.replace(/:\d+$/, "");
|
|
343
|
+
a = normAddress(a);
|
|
344
|
+
return net.isIP(a) ? a : undefined;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* The edge counts as local (0.4.3): whether an edge request (the edge
|
|
349
|
+
* hostname from an edge peer) comes from this machine, by the left-most
|
|
350
|
+
* X-Forwarded-For entry (the original client):
|
|
351
|
+
* - loopback, or one of this machine's own interface addresses (its LAN
|
|
352
|
+
* or VPN address, a Tailscale 100.x one included): local;
|
|
353
|
+
* - missing, unreadable, or a container/VM network's address (the Docker
|
|
354
|
+
* ranges isEdgePeer takes, a subnet of one of this machine's bridges,
|
|
355
|
+
* one of the edge's own Docker networks, or OrbStack's 0.250.250.254):
|
|
356
|
+
* we can't tell (Docker Desktop and OrbStack on macOS hand the
|
|
357
|
+
* edge their gateway's address, and a TCP passthrough in front hides the
|
|
358
|
+
* client), so local;
|
|
359
|
+
* - anything else: another machine, beyond loopback (sign-in required).
|
|
360
|
+
* The edge's own page warns when it is reachable beyond this machine
|
|
361
|
+
* (edge-exposure.ts): with no sign-in, anyone who reaches it is local.
|
|
362
|
+
*/
|
|
363
|
+
export function edgeClientIsLocal(
|
|
364
|
+
xff: string | undefined,
|
|
365
|
+
local: LocalNet = { own: [], bridges: [] },
|
|
366
|
+
): boolean {
|
|
367
|
+
const c = forwardedClient(xff);
|
|
368
|
+
if (!c || isEdgePeer(c) || local.own.includes(c)) return true;
|
|
369
|
+
// 0.0.0.0/8 ("this network") is never a client on another machine:
|
|
370
|
+
// OrbStack presents the host to its containers as 0.250.250.254.
|
|
371
|
+
if (net.isIPv4(c) && c.startsWith("0.")) return true;
|
|
372
|
+
return local.bridges.some((b) => inSubnet(c, b));
|
|
373
|
+
}
|
|
374
|
+
|
|
266
375
|
/**
|
|
267
376
|
* Whether the request came over HTTPS: the socket's own encryption, or,
|
|
268
377
|
* only for the edge hostname from an edge peer, `X-Forwarded-Proto: https`.
|
|
@@ -370,7 +479,13 @@ export function checkRequest(
|
|
|
370
479
|
"127.0.0.1, [::1] with its port, or its edge hostname).",
|
|
371
480
|
};
|
|
372
481
|
const peerLoopback = ctx.peerIsLoopback ? ctx.peerIsLoopback(req) : true;
|
|
373
|
-
|
|
482
|
+
// The edge from this machine (edgeClientIsLocal) is local, unless this
|
|
483
|
+
// server is deliberately exposed; a LAN name or peer is beyond.
|
|
484
|
+
const beyond =
|
|
485
|
+
via === "edge"
|
|
486
|
+
? !!ctx.exposed ||
|
|
487
|
+
!edgeClientIsLocal(header(req, "x-forwarded-for"), ctx.localNet?.())
|
|
488
|
+
: via !== "loopback" || !peerLoopback;
|
|
374
489
|
|
|
375
490
|
let principal: Principal | null = null;
|
|
376
491
|
const auth = header(req, "authorization");
|
|
@@ -34,6 +34,8 @@ export const UI_MODULES: readonly string[] = [
|
|
|
34
34
|
"/error-page.js",
|
|
35
35
|
"/agents-section.js",
|
|
36
36
|
"/agents-page.js",
|
|
37
|
+
// The Start agent form's choices, shared by both (AG-10.0).
|
|
38
|
+
"/agent-start.js",
|
|
37
39
|
// The UI's catalogs other than English (R19; en is in shell.js).
|
|
38
40
|
"/i18n/ja.js",
|
|
39
41
|
"/i18n/es.js",
|