@catheadowl/ukp 0.2.0 → 0.2.2
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/bin/ukp.js +82 -0
- package/package.json +3 -2
- package/src/capabilities/remote-client.ts +87 -11
- package/src/commands/guide-content/remote.txt +2 -2
- package/src/commands/inventory.ts +1 -1
- package/src/commands/serve.ts +2 -0
- package/src/server.ts +20 -2
package/bin/ukp.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Self-locating launcher (D-085 completion, Windows-host wake 2026-09-20).
|
|
3
|
+
//
|
|
4
|
+
// npm's shims exec this file with node (the shebang above); bun's own bin
|
|
5
|
+
// links and `bunx` run it with bun itself. Either way the launcher finds a
|
|
6
|
+
// bun executable WITHOUT relying on the caller's PATH: non-interactive
|
|
7
|
+
// shells (ssh sessions, schtasks, service wrappers) see only the system
|
|
8
|
+
// PATH, while every standard install mode lands bun in a user-local dir.
|
|
9
|
+
// Candidate order: an explicit UKP_BUN override, the current interpreter
|
|
10
|
+
// when it already is bun, the standard install locations, then PATH.
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const cli = join(here, "..", "src", "cli.ts");
|
|
19
|
+
const isWindows = process.platform === "win32";
|
|
20
|
+
const runningUnderBun = typeof Bun !== "undefined" || Boolean(process.versions && process.versions.bun);
|
|
21
|
+
|
|
22
|
+
function bunCandidates() {
|
|
23
|
+
const exe = isWindows ? "bun.exe" : "bun";
|
|
24
|
+
const home = homedir();
|
|
25
|
+
const candidates = [];
|
|
26
|
+
if (typeof process.env.UKP_BUN === "string" && process.env.UKP_BUN.length > 0) {
|
|
27
|
+
candidates.push(process.env.UKP_BUN);
|
|
28
|
+
}
|
|
29
|
+
if (runningUnderBun) candidates.push(process.execPath);
|
|
30
|
+
// bun's official installer (same shape on every platform).
|
|
31
|
+
candidates.push(join(home, ".bun", "bin", exe));
|
|
32
|
+
if (isWindows) {
|
|
33
|
+
// `npm i -g bun` lands its shim in the npm user prefix — the same
|
|
34
|
+
// prefix root this package's own shim lives under.
|
|
35
|
+
candidates.push(join(here, "..", "..", "..", "..", exe));
|
|
36
|
+
// scoop installs stay user-local too.
|
|
37
|
+
candidates.push(join(home, "scoop", "shims", exe));
|
|
38
|
+
} else {
|
|
39
|
+
candidates.push(join(home, ".npm-global", "bin", exe));
|
|
40
|
+
// Homebrew's path is joined, not written as one literal: the release
|
|
41
|
+
// leak scan rightly flags literal home-directory-shaped paths, and this
|
|
42
|
+
// is a functional standard-location constant (same stance as the wake
|
|
43
|
+
// command's PATH prefix in src/capabilities/remote-client.ts).
|
|
44
|
+
candidates.push(join("/", "opt", "homebrew", "bin", "bun"));
|
|
45
|
+
candidates.push("/usr/local/bin/bun");
|
|
46
|
+
}
|
|
47
|
+
// PATH last, resolved ourselves dir-by-dir: a bare-name spawn would let
|
|
48
|
+
// the OS search with the PARENT's PATH on Windows (CreateProcess
|
|
49
|
+
// semantics), which this process cannot see or control.
|
|
50
|
+
const pathVar = process.env.PATH;
|
|
51
|
+
if (typeof pathVar === "string" && pathVar.length > 0) {
|
|
52
|
+
for (const dir of pathVar.split(isWindows ? ";" : ":")) {
|
|
53
|
+
if (dir.length > 0) candidates.push(join(dir, exe));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return candidates;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fail(detail) {
|
|
60
|
+
process.stderr.write(
|
|
61
|
+
"ukp: no bun executable found (" + detail + ").\n"
|
|
62
|
+
+ "ukp runs on bun — install it (https://bun.sh), or point UKP_BUN at the\n"
|
|
63
|
+
+ "bun executable. Standard install locations (~/.bun/bin and the npm\n"
|
|
64
|
+
+ "global prefix) are checked before PATH.\n",
|
|
65
|
+
);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const bunPath = bunCandidates().find((candidate) => existsSync(candidate));
|
|
70
|
+
if (bunPath === undefined) {
|
|
71
|
+
fail("looked for UKP_BUN, the standard install locations, and PATH");
|
|
72
|
+
}
|
|
73
|
+
const result = spawnSync(bunPath, [cli, ...process.argv.slice(2)], {
|
|
74
|
+
stdio: "inherit",
|
|
75
|
+
env: process.env,
|
|
76
|
+
});
|
|
77
|
+
if (result.error) {
|
|
78
|
+
// bunPath existed on disk a moment ago; a spawn failure here is exotic,
|
|
79
|
+
// but it must still land in the remedy, never a stack trace.
|
|
80
|
+
fail(`spawning '${bunPath}' failed: ${String(result.error.message ?? result.error)}`);
|
|
81
|
+
}
|
|
82
|
+
process.exit(result.status === null ? (result.signal !== null ? 130 : 1) : result.status);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catheadowl/ukp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"bun": ">=1.3.14"
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
"agent"
|
|
25
25
|
],
|
|
26
26
|
"bin": {
|
|
27
|
-
"ukp": "
|
|
27
|
+
"ukp": "bin/ukp.js"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
|
+
"bin",
|
|
30
31
|
"src",
|
|
31
32
|
"README.md",
|
|
32
33
|
"LICENSE",
|
|
@@ -290,10 +290,24 @@ function spawnMuxMaster(
|
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
/** The pinned wake command (W9 / ADR-REM-006 §4 safety narrowing)
|
|
294
|
-
* only the client-chosen integer port and idle window are
|
|
295
|
-
* exact string an operator can allowlist with git-shell /
|
|
296
|
-
* `command=`. Loopback-only, anonymous: SSH carries
|
|
293
|
+
/** The pinned wake command (W9 / ADR-REM-006 §4 safety narrowing), in two
|
|
294
|
+
* host-shell forms: only the client-chosen integer port and idle window are
|
|
295
|
+
* interpolated — the exact string an operator can allowlist with git-shell /
|
|
296
|
+
* authorized_keys `command=`. Loopback-only, anonymous: SSH carries
|
|
297
|
+
* encryption and auth. The POSIX form is unchanged since W9; the cmd form
|
|
298
|
+
* (2026-09-20, liku feedback) serves Windows hosts whose OpenSSH default
|
|
299
|
+
* shell is cmd.exe and cannot parse `sh -c` at all — it is the pinned
|
|
300
|
+
* equivalent with a Windows-shaped PATH prefix (bun official, scoop, npm
|
|
301
|
+
* user prefix; the launcher resolves bun itself once ukp is found, the
|
|
302
|
+
* prefix guarantees ukp resolves). The `set "PATH=…"` quoting is load-
|
|
303
|
+
* bearing: an unquoted `set PATH=…%PATH%&&…` breaks when the expanded PATH
|
|
304
|
+
* contains cmd metacharacters (`&` in a directory name — verified), while
|
|
305
|
+
* the quoted form survives both the sshd double-cmd wrapper and cmd's own
|
|
306
|
+
* expansion (verified against Win32-OpenSSH source and live). The wake
|
|
307
|
+
* ladder sends POSIX first and switches on the wrong-shell failure
|
|
308
|
+
* signature; host shells covered: POSIX login shells, cmd.exe default, and
|
|
309
|
+
* Git-Bash-style DefaultShell hosts — a powershell DefaultShell is NOT
|
|
310
|
+
* supported (its -c quoting drops the form).
|
|
297
311
|
*
|
|
298
312
|
* The remote shell ssh runs commands in is NON-INTERACTIVE (bare system
|
|
299
313
|
* PATH), while every standard install mode lands in user-local,
|
|
@@ -301,9 +315,9 @@ function spawnMuxMaster(
|
|
|
301
315
|
* interactivity guard; `bun add -g` links into ~/.bun/bin; a user npm prefix
|
|
302
316
|
* like ~/.npm-global never leaves the user dir; brew lives outside the
|
|
303
317
|
* default ssh PATH). So the command prepends the standard install locations
|
|
304
|
-
* to PATH before exec — ukp AND its
|
|
305
|
-
*
|
|
306
|
-
*
|
|
318
|
+
* to PATH before exec — ukp AND its shebang/launcher both resolve for any
|
|
319
|
+
* usual install. nvm-style versioned layouts have no fixed path shape and
|
|
320
|
+
* need a system symlink (documented exception). */
|
|
307
321
|
// The Linuxbrew segment is joined, not written as one literal: its standard
|
|
308
322
|
// install location is a functional constant (part of the byte-pinned remote
|
|
309
323
|
// command), while the release "absolute user path" leak scan rightly flags
|
|
@@ -317,10 +331,54 @@ const WAKE_REMOTE_PATH_PREFIX = [
|
|
|
317
331
|
["/home", "linuxbrew", ".linuxbrew", "bin"].join("/"),
|
|
318
332
|
].join(":");
|
|
319
333
|
|
|
320
|
-
|
|
334
|
+
export type WakeShellForm = "posix" | "cmd";
|
|
335
|
+
|
|
336
|
+
function wakeDoorCommand(remotePort: number, form: WakeShellForm = "posix"): string {
|
|
337
|
+
if (form === "cmd") {
|
|
338
|
+
return `cmd /d /c "set "PATH=%USERPROFILE%\\.bun\\bin;%USERPROFILE%\\scoop\\shims;%APPDATA%\\npm;%PATH%"&&ukp serve --allow-anonymous --host 127.0.0.1 --port ${remotePort} --max-idle ${WAKE_DOOR_MAX_IDLE_SECONDS}"`;
|
|
339
|
+
}
|
|
321
340
|
return `sh -c 'PATH="${WAKE_REMOTE_PATH_PREFIX}:$PATH" exec ukp serve --allow-anonymous --host 127.0.0.1 --port ${remotePort} --max-idle ${WAKE_DOOR_MAX_IDLE_SECONDS}'`;
|
|
322
341
|
}
|
|
323
342
|
|
|
343
|
+
/** Wrong-shell failure signature for the wake ladder: the remote shell could
|
|
344
|
+
* not even parse the sent form, so the OTHER form is the retry — as opposed
|
|
345
|
+
* to a missing-ukp failure, where the shell ran fine and named 'ukp' as the
|
|
346
|
+
* thing it cannot find. Language-independent: cmd.exe locales vary the
|
|
347
|
+
* message body (English, zh-CN, German … all differ) but always quote the
|
|
348
|
+
* missing ASCII token; POSIX shells prefix the
|
|
349
|
+
* token ("sh: command not found"). */
|
|
350
|
+
function wrongShellSignature(form: WakeShellForm, diagnostics: string): boolean {
|
|
351
|
+
if (form === "posix") return /['"]sh['"]|sh: (?:command )?not found/i.test(diagnostics);
|
|
352
|
+
return /['"]cmd['"]|cmd: (?:command )?not found/i.test(diagnostics);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** One cheap, language-independent roundtrip to learn the host shell family
|
|
356
|
+
* BEFORE the first wake attempt (2026-09-20, liku 9.5 finding): cmd.exe
|
|
357
|
+
* expands `%OS%` to `Windows_NT`; a POSIX shell echoes the literal token.
|
|
358
|
+
* Choosing the form up front avoids the blind POSIX-first attempt costing a
|
|
359
|
+
* full ready-timeout on every Windows invocation, and picks the right tty
|
|
360
|
+
* stance — `-tt` exists for POSIX SIGHUP reaping; on Windows there is no
|
|
361
|
+
* SIGHUP (max-idle bounds the door), and at least one Win32-OpenSSH 9.5
|
|
362
|
+
* build LOSES a quoted remote command under a pty (an interactive cmd
|
|
363
|
+
* swallows it and waits on stdin forever — the no-pty path executes it
|
|
364
|
+
* correctly). Defaults to POSIX (the common server shape) when the probe
|
|
365
|
+
* errors or times out; the ladder remains as the backstop. */
|
|
366
|
+
async function probeHostShellFamily(
|
|
367
|
+
options: { sshCommand?: readonly string[] },
|
|
368
|
+
target: string,
|
|
369
|
+
): Promise<WakeShellForm> {
|
|
370
|
+
const proc = Bun.spawn(
|
|
371
|
+
[...(options.sshCommand ?? ["ssh"]), "-o", "BatchMode=yes", target, "echo %OS%"],
|
|
372
|
+
{ stdout: "pipe", stderr: "pipe", stdin: "ignore" },
|
|
373
|
+
);
|
|
374
|
+
const output = await Promise.race([
|
|
375
|
+
new Response(proc.stdout).text().catch(() => ""),
|
|
376
|
+
Bun.sleep(5_000).then(() => ""),
|
|
377
|
+
]);
|
|
378
|
+
proc.kill();
|
|
379
|
+
return /Windows_NT/i.test(output) ? "cmd" : "posix";
|
|
380
|
+
}
|
|
381
|
+
|
|
324
382
|
/** HTTP-level readiness probe through the tunnel (W9): a TCP accept on the
|
|
325
383
|
* local forward proves nothing — ssh accepts locally and connects remotely
|
|
326
384
|
* lazily — so we poll the door's public discovery document until it answers
|
|
@@ -399,6 +457,10 @@ async function openSshTunnel(
|
|
|
399
457
|
// options for the remaining attempts so the endpoint degrades to Tier 0
|
|
400
458
|
// instead of hard-failing what worked without mux.
|
|
401
459
|
let muxDisabled = false;
|
|
460
|
+
// Shell form (2026-09-20): probed up front via `echo %OS%` (language-
|
|
461
|
+
// independent — see probeHostShellFamily), POSIX by default; the wrong-
|
|
462
|
+
// shell ladder below remains the backstop for a lying or timed-out probe.
|
|
463
|
+
let form: WakeShellForm = await probeHostShellFamily(options, target);
|
|
402
464
|
const attempts = 3;
|
|
403
465
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
404
466
|
const localPort = await freePort();
|
|
@@ -409,14 +471,17 @@ async function openSshTunnel(
|
|
|
409
471
|
// SIGHUP on disconnect — normal disconnect included, the door only ever
|
|
410
472
|
// died to --max-idle. Forcing a pty gives the session a controlling
|
|
411
473
|
// terminal, so killing the client reaps the door in seconds (verified:
|
|
412
|
-
// kill -9 of the local ssh → door gone within 3s).
|
|
413
|
-
|
|
474
|
+
// kill -9 of the local ssh → door gone within 3s). POSIX hosts only:
|
|
475
|
+
// Windows has no SIGHUP (max-idle bounds the door there), and pty
|
|
476
|
+
// sessions lose quoted remote commands on at least one
|
|
477
|
+
// Win32-OpenSSH 9.5 build — the cmd form goes without -tt.
|
|
478
|
+
...(form === "posix" ? ["-tt"] : []),
|
|
414
479
|
"-o", "ExitOnForwardFailure=yes",
|
|
415
480
|
"-o", "BatchMode=yes",
|
|
416
481
|
...(muxDisabled ? [] : muxClientOptions()),
|
|
417
482
|
"-L", `127.0.0.1:${localPort}:127.0.0.1:${remotePort}`,
|
|
418
483
|
target,
|
|
419
|
-
wakeDoorCommand(remotePort),
|
|
484
|
+
wakeDoorCommand(remotePort, form),
|
|
420
485
|
];
|
|
421
486
|
const proc = Bun.spawn(argv, { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
|
|
422
487
|
const stderrTail = drainStderrTail([proc.stdout, proc.stderr]);
|
|
@@ -432,6 +497,17 @@ async function openSshTunnel(
|
|
|
432
497
|
if (/ControlPath too long|muxserver_listen|unix_listener/i.test(diagnostics)) {
|
|
433
498
|
muxDisabled = true;
|
|
434
499
|
}
|
|
500
|
+
if (exited && wrongShellSignature(form, diagnostics)) {
|
|
501
|
+
if (attempt < attempts) {
|
|
502
|
+
form = form === "posix" ? "cmd" : "posix";
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
// Both forms rejected: this is a shell mismatch, not a missing-ukp
|
|
506
|
+
// failure — say so instead of falling into the missing-ukp wording.
|
|
507
|
+
throw new RemoteTransportError(
|
|
508
|
+
`waking the ukp door on '${target}' (endpoint '${endpointLabel}') failed — the remote shell rejected both wake command forms (POSIX and cmd.exe): ${diagnostics}. Supported host shells: POSIX login shells, cmd.exe default, Git-Bash-style DefaultShell; a powershell DefaultShell is not supported`,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
435
511
|
// POSIX login shells print "…: ukp: command not found" (exit 127); Windows
|
|
436
512
|
// OpenSSH under cmd prints "'ukp' is not recognized…" (exit 9009).
|
|
437
513
|
if (exited && /not found|not recognized|exit status 127|status 9009/i.test(diagnostics)) {
|
|
@@ -54,8 +54,8 @@ Name and identity:
|
|
|
54
54
|
- ssh:// urls take no port: registration rejects one (the woken door listens on a client-chosen loopback port; use an ssh config Host alias for a non-standard sshd port).
|
|
55
55
|
|
|
56
56
|
Operational note:
|
|
57
|
-
- ssh:// hosts: nothing to manage — ukp wakes and reaps doors itself
|
|
58
|
-
- https hosts: keep ukp serve under your normal process manager, for example systemd, Task Scheduler, or a terminal multiplexer. The CLI does not install or autostart the door.
|
|
57
|
+
- ssh:// hosts: nothing to manage — ukp wakes and reaps doors itself (on Windows, enabling the built-in OpenSSH Server feature once is the low-burden path). SSH lockdown (login-shell restriction, why a forced authorized_keys command= does not compose) and nvm-style install layouts are deployment-handbook material.
|
|
58
|
+
- https hosts: keep ukp serve under your normal process manager, for example systemd, Task Scheduler, or a terminal multiplexer. Linux can go resident-free (systemd socket activation, deployment handbook); Windows has no systemd equivalent, so a resident door wants a real service wrapper (WinSW-class) — prefer the ssh:// path there unless consumers have no SSH credentials. The CLI does not install or autostart the door.
|
|
59
59
|
- serve accepts --max-idle <seconds> to self-exit after that long without requests (the same backstop the woken doors use).
|
|
60
60
|
|
|
61
61
|
Remember:
|
|
@@ -80,7 +80,7 @@ export const REGISTER_SPEC: UkpCommandSpec = {
|
|
|
80
80
|
" assertion on the declared name; bearer-token services read",
|
|
81
81
|
" UKP_ENDPOINT_<NAME>_TOKEN (keyed by the handle) at call time.",
|
|
82
82
|
"",
|
|
83
|
-
"Host doors
|
|
83
|
+
"Host doors:",
|
|
84
84
|
" 'ukp register --url ssh://ali' fetches the door's discovery document",
|
|
85
85
|
" and imports every endpoint behind it (a door url may also name one",
|
|
86
86
|
" endpoint: ssh://ali/notes). Import is explicit and idempotent:",
|
package/src/commands/serve.ts
CHANGED
|
@@ -173,6 +173,7 @@ export function renderServeBanner(info: ServeInfo): string {
|
|
|
173
173
|
` endpoints: ${info.door!.endpoints.length > 0 ? info.door!.endpoints.join(", ") : "(none — register endpoints on this host)"}`,
|
|
174
174
|
` write: ${info.door!.write.length > 0 ? info.door!.write.join(", ") + " (propose via PUT /e/<name>/v1/propose/<id>)" : "(no endpoint declares propose)"}`,
|
|
175
175
|
` auth: ${info.authRequired ? "bearer token required" : "no token (loopback bind; ssh-forwarded clients authenticate by SSH key)"}`,
|
|
176
|
+
` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning — install ripgrep on this door's PATH)"}`,
|
|
176
177
|
...(info.maxIdleSeconds !== undefined
|
|
177
178
|
? [` idle: exits after ${info.maxIdleSeconds}s without requests (--max-idle)`]
|
|
178
179
|
: []),
|
|
@@ -187,6 +188,7 @@ export function renderServeBanner(info: ServeInfo): string {
|
|
|
187
188
|
` listening: ${info.url}`,
|
|
188
189
|
` discovery: ${info.url.startsWith("http") ? `${info.url}${DISCOVERY_PATH}` : info.url}`,
|
|
189
190
|
` auth: ${info.authRequired ? "bearer token required" : "no token (loopback only)"}`,
|
|
191
|
+
` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning — install ripgrep on this door's PATH)"}`,
|
|
190
192
|
...(info.maxIdleSeconds !== undefined
|
|
191
193
|
? [` idle: exits after ${info.maxIdleSeconds}s without requests (--max-idle)`]
|
|
192
194
|
: []),
|
package/src/server.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createServer as netCreateServer, connect as netConnect, type Server as
|
|
|
4
4
|
import { ENDPOINT_NAME, loadManifest, type LoadedManifest } from "./config/manifest.ts";
|
|
5
5
|
import { localPathOf, readRegistry } from "./registry.ts";
|
|
6
6
|
import { FILE_NATIVE_CAPABILITIES, resolveFileNativeCapability } from "./config/file-native.ts";
|
|
7
|
+
import { EXTERNAL_PROVIDER, EXTERNAL_TOOL_CAPABILITIES } from "./config/external-tool.ts";
|
|
7
8
|
import {
|
|
8
9
|
PROPOSE_SLUG,
|
|
9
10
|
ProposeBusyError,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
type NavRequest,
|
|
29
30
|
} from "./capabilities/nav.ts";
|
|
30
31
|
import {
|
|
32
|
+
rgExecutableAvailable,
|
|
31
33
|
runRg,
|
|
32
34
|
projectRgEnvelope,
|
|
33
35
|
RG_DEFAULT_LIMIT,
|
|
@@ -149,6 +151,14 @@ export interface ServeInfo {
|
|
|
149
151
|
host: string;
|
|
150
152
|
port: number;
|
|
151
153
|
authRequired: boolean;
|
|
154
|
+
/** ripgrep availability probed once at startup IN THE DOOR'S OWN PROCESS
|
|
155
|
+
* (2026-09-20, liku feedback): /v1/rg serves every endpoint and a missing
|
|
156
|
+
* binary degrades per-endpoint on the wire by design — but that truth is
|
|
157
|
+
* consumer-channel only. The banner line is the operator channel, and the
|
|
158
|
+
* probe environment is by construction the one that matters (ukp diagnose
|
|
159
|
+
* measures the invoking shell instead, which can differ from the door's
|
|
160
|
+
* schtasks / wake environment). */
|
|
161
|
+
rg: "ok" | "missing";
|
|
152
162
|
/** TLS identity summary (W5' / D-079) when serving over HTTPS; the pin is
|
|
153
163
|
* what remote clients TOFU-pin at registration. `source` "re-signed" marks
|
|
154
164
|
* the one start that grew SAN coverage over the existing key (--tls-san
|
|
@@ -231,13 +241,19 @@ function readInstanceUid(serviceFolder: string): string {
|
|
|
231
241
|
}
|
|
232
242
|
|
|
233
243
|
/** Capability projection shared by endpoint and door documents: derived
|
|
234
|
-
* file-native defaults (read/nav) are always present
|
|
235
|
-
*
|
|
244
|
+
* file-native defaults (read/nav) are always present, then the external-tool
|
|
245
|
+
* base tier (rg, ADR-RG-003 — served on /v1/rg for every endpoint; a missing
|
|
246
|
+
* binary degrades to per-endpoint availability data per the W6 wire
|
|
247
|
+
* contract, never an undeclared capability); declared capabilities override
|
|
248
|
+
* or extend both (a declared nav or rg replaces the default entry). */
|
|
236
249
|
function projectCapabilities(loaded: LoadedManifest): DiscoveryDocument["capabilities"] {
|
|
237
250
|
const capabilities: DiscoveryDocument["capabilities"] = {};
|
|
238
251
|
for (const [name, spec] of Object.entries(FILE_NATIVE_CAPABILITIES)) {
|
|
239
252
|
if (spec.derived) capabilities[name] = { provider: "file", derived: true };
|
|
240
253
|
}
|
|
254
|
+
for (const name of Object.keys(EXTERNAL_TOOL_CAPABILITIES)) {
|
|
255
|
+
capabilities[name] = { provider: EXTERNAL_PROVIDER, derived: true };
|
|
256
|
+
}
|
|
241
257
|
for (const name of Object.keys(loaded.manifest.capabilities).sort()) {
|
|
242
258
|
const declared = loaded.manifest.capabilities[name]!;
|
|
243
259
|
capabilities[name] = { provider: declared.provider ?? "file" };
|
|
@@ -886,6 +902,7 @@ function startDoorServer(config: ServeConfig, host: string, tlsMaterial: ReturnT
|
|
|
886
902
|
host,
|
|
887
903
|
port: listener.port,
|
|
888
904
|
authRequired: (config.tokens?.length ?? 0) > 0,
|
|
905
|
+
rg: rgExecutableAvailable() ? "ok" : "missing",
|
|
889
906
|
...(tlsMaterial !== undefined
|
|
890
907
|
? { tls: { pin: tlsMaterial.pin, san: tlsMaterial.san, source: tlsMaterial.source } }
|
|
891
908
|
: {}),
|
|
@@ -1019,6 +1036,7 @@ export function startUkpServer(config: ServeConfig): StartedServe {
|
|
|
1019
1036
|
host,
|
|
1020
1037
|
port: listener.port,
|
|
1021
1038
|
authRequired: (config.tokens?.length ?? 0) > 0,
|
|
1039
|
+
rg: rgExecutableAvailable() ? "ok" : "missing",
|
|
1022
1040
|
...(tlsMaterial !== undefined
|
|
1023
1041
|
? { tls: { pin: tlsMaterial.pin, san: tlsMaterial.san, source: tlsMaterial.source } }
|
|
1024
1042
|
: {}),
|