@catheadowl/ukp 0.2.5 → 0.3.0
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/README.md +93 -21
- package/SECURITY.md +69 -0
- package/bin/ukp.js +31 -7
- package/docs/remote-deployment.md +175 -19
- package/package.json +11 -6
- package/skills/ukp/SKILL.md +32 -0
- package/src/capabilities/door-health.ts +480 -0
- package/src/capabilities/nav.ts +209 -444
- package/src/capabilities/qmd.ts +19 -0
- package/src/capabilities/read.ts +18 -2
- package/src/capabilities/remote-client.ts +4 -2
- package/src/capabilities/search.ts +23 -7
- package/src/capabilities/session.ts +455 -0
- package/src/capabilities/tls-identity.ts +55 -3
- package/src/cli.ts +38 -64
- package/src/commands/diagnose.ts +80 -2
- package/src/commands/guide-content/client.txt +19 -6
- package/src/commands/guide-content/remote.txt +22 -1
- package/src/commands/guide-content/rg.txt +2 -2
- package/src/commands/init.ts +243 -1
- package/src/commands/inventory.ts +277 -17
- package/src/commands/nav.ts +17 -12
- package/src/commands/read.ts +1 -1
- package/src/commands/rg.ts +2 -2
- package/src/commands/search.ts +19 -3
- package/src/commands/serve-task.ts +334 -44
- package/src/commands/serve.ts +410 -44
- package/src/commands/session.ts +541 -0
- package/src/commands/use.ts +178 -0
- package/src/config/agent-backed.ts +136 -0
- package/src/config/manifest.ts +39 -3
- package/src/manifest.yml +15 -0
- package/src/path-safety.ts +9 -4
- package/src/plumbing/token-file.ts +368 -0
- package/src/registry.ts +78 -1
- package/src/server.ts +291 -75
- package/src/spawn-relay.ts +21 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { hostname, networkInterfaces } from "node:os";
|
|
4
4
|
import { X509Certificate, createHash } from "node:crypto";
|
|
@@ -124,8 +124,13 @@ export interface SelfSignedIdentity {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/** Idempotent self-signed identity for `ukp serve --tls`: persisted under
|
|
127
|
-
*
|
|
128
|
-
*
|
|
127
|
+
* the machine home the caller resolves (`~/.ukp/tls/<endpoint-name>/` in
|
|
128
|
+
* endpoint mode, `~/.ukp/tls/` flat in door mode — the registry's
|
|
129
|
+
* directory, machine-scoped by adjudication 2026-11-01, inbox
|
|
130
|
+
* service-dir-layering item 2; the pre-2026-11-01 home was
|
|
131
|
+
* `<serviceFolder>/.ukp/tls/`, relocated by migrateLegacyServiceTlsDir).
|
|
132
|
+
* openssl signs
|
|
133
|
+
* it because node:crypto cannot issue X.509; the
|
|
129
134
|
* identity is the keypair, so losing the key loses the identity (clients
|
|
130
135
|
* re-register). `extraSanEntries` (the `--tls-san` flag, already normalized
|
|
131
136
|
* `IP:x`/`DNS:y`) merges into the SAN coverage; when a persisted certificate
|
|
@@ -227,3 +232,50 @@ function runOpenssl(args: readonly string[], outputs: readonly string[]): void {
|
|
|
227
232
|
);
|
|
228
233
|
}
|
|
229
234
|
}
|
|
235
|
+
|
|
236
|
+
/** One-time relocation of a Service folder's TLS identity into its machine
|
|
237
|
+
* home (adjudication 2026-11-01, inbox service-dir-layering item 2): the
|
|
238
|
+
* identity is machine-scoped material — its SAN coverage names THIS host's
|
|
239
|
+
* interfaces — so the keypair moves from the pre-2026-11-01
|
|
240
|
+
* `<serviceFolder>/.ukp/tls/` to the caller-resolved machine home,
|
|
241
|
+
* untouched (the SPKI pin every client holds survives; nothing re-signs —
|
|
242
|
+
* INV-04 renewal semantics by construction). The caller swaps the retired
|
|
243
|
+
* `/tls/` ignore rule in the same gesture.
|
|
244
|
+
*
|
|
245
|
+
* `occupied`: the machine home already holds a complete identity while the
|
|
246
|
+
* legacy dir also exists (a folder copied between machines rather than
|
|
247
|
+
* cloned through git). The stale duplicate stays put — silently deleting
|
|
248
|
+
* key material is not this step's call, and the legacy ignore rule keeps
|
|
249
|
+
* guarding it. */
|
|
250
|
+
export function migrateLegacyServiceTlsDir(
|
|
251
|
+
serviceFolder: string,
|
|
252
|
+
identityDir: string,
|
|
253
|
+
): "moved" | "absent" | "occupied" {
|
|
254
|
+
const legacyDir = join(serviceFolder, ".ukp", "tls");
|
|
255
|
+
if (!existsSync(legacyDir)) return "absent";
|
|
256
|
+
if (existsSync(join(identityDir, "cert.pem")) && existsSync(join(identityDir, "key.pem"))) {
|
|
257
|
+
return "occupied";
|
|
258
|
+
}
|
|
259
|
+
mkdirSync(identityDir, { recursive: true, mode: 0o700 });
|
|
260
|
+
for (const name of ["cert.pem", "key.pem"] as const) {
|
|
261
|
+
const source = join(legacyDir, name);
|
|
262
|
+
if (existsSync(source)) moveFileStrict(source, join(identityDir, name));
|
|
263
|
+
}
|
|
264
|
+
// With the material out, the whole legacy dir goes: anything left behind
|
|
265
|
+
// (.lock residue from a crashed serve) is transient state, not identity.
|
|
266
|
+
rmSync(legacyDir, { recursive: true, force: true });
|
|
267
|
+
return "moved";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** rename with a cross-device fallback (a service folder and the user home
|
|
271
|
+
* can live on different drives); the key keeps its 0600 either way. */
|
|
272
|
+
function moveFileStrict(source: string, target: string): void {
|
|
273
|
+
try {
|
|
274
|
+
renameSync(source, target);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (!(error instanceof Error) || (error as NodeJS.ErrnoException).code !== "EXDEV") throw error;
|
|
277
|
+
copyFileSync(source, target);
|
|
278
|
+
chmodSync(target, 0o600);
|
|
279
|
+
unlinkSync(source);
|
|
280
|
+
}
|
|
281
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { executeReadCommand, READ_SPEC } from "./commands/read.ts";
|
|
|
13
13
|
import { executeRgCommand, RG_SPEC } from "./commands/rg.ts";
|
|
14
14
|
import { executeGuideCommand, GUIDE_SPEC, GUIDE_TOPICS } from "./commands/guide.ts";
|
|
15
15
|
import { executeInitCommand, INIT_SPEC } from "./commands/init.ts";
|
|
16
|
+
import { executeSessionCommand } from "./commands/session.ts";
|
|
16
17
|
import { executeInspectCommand, INSPECT_SPEC } from "./commands/inspect.ts";
|
|
17
18
|
import { executeListCommand, executeRegisterCommand, executeUnregisterCommand, LIST_SPEC, REGISTER_SPEC, UNREGISTER_SPEC } from "./commands/inventory.ts";
|
|
18
19
|
import { executeNavCommand, NAV_SPEC } from "./commands/nav.ts";
|
|
@@ -20,6 +21,7 @@ import { executeProposeCommand, PROPOSE_SPEC } from "./commands/propose.ts";
|
|
|
20
21
|
import { executeUpdateCommand, UPDATE_SPEC } from "./commands/update.ts";
|
|
21
22
|
import { executeServeCommand, SERVE_SPEC } from "./commands/serve.ts";
|
|
22
23
|
import { executeSearchCommand, SEARCH_SPEC } from "./commands/search.ts";
|
|
24
|
+
import { executeUseCommand, USE_SPEC } from "./commands/use.ts";
|
|
23
25
|
|
|
24
26
|
export { renderSearchHelp } from "./commands/search.ts";
|
|
25
27
|
export { renderDiagnoseHelp } from "./commands/diagnose.ts";
|
|
@@ -54,6 +56,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
54
56
|
register: REGISTER_SPEC.summary,
|
|
55
57
|
unregister: UNREGISTER_SPEC.summary,
|
|
56
58
|
list: LIST_SPEC.summary,
|
|
59
|
+
use: USE_SPEC.summary,
|
|
57
60
|
nav: NAV_SPEC.summary,
|
|
58
61
|
propose: PROPOSE_SPEC.summary,
|
|
59
62
|
search: SEARCH_SPEC.summary,
|
|
@@ -66,7 +69,10 @@ export const COMMANDS = Object.entries(COMMAND_DESCRIPTIONS).map(
|
|
|
66
69
|
|
|
67
70
|
// Help-display groups (ADR 0022): purpose-noun headings over a flat verb
|
|
68
71
|
// surface. Every command must appear in exactly one group; a guard test
|
|
69
|
-
// locks group membership against COMMANDS.
|
|
72
|
+
// locks group membership against COMMANDS. `session` is deliberately
|
|
73
|
+
// outside the advertised surface (owner ruling 2026-11-03, extending the
|
|
74
|
+
// 2026-10-31 contract-face exit): dispatch stays live, the skill face
|
|
75
|
+
// documents it — hidden but functional, the clig.dev shape.
|
|
70
76
|
export const COMMAND_GROUPS = [
|
|
71
77
|
{
|
|
72
78
|
heading: "Endpoint commands",
|
|
@@ -74,7 +80,7 @@ export const COMMAND_GROUPS = [
|
|
|
74
80
|
},
|
|
75
81
|
{
|
|
76
82
|
heading: "Registry commands",
|
|
77
|
-
commands: ["init", "register", "unregister", "list"],
|
|
83
|
+
commands: ["init", "register", "unregister", "list", "use"],
|
|
78
84
|
},
|
|
79
85
|
{
|
|
80
86
|
heading: "Operations commands",
|
|
@@ -92,9 +98,16 @@ export interface CliContext {
|
|
|
92
98
|
resolveProvider?: ProviderResolver;
|
|
93
99
|
qmdCommand?: readonly string[];
|
|
94
100
|
rgCommand?: readonly string[];
|
|
101
|
+
/** Overrides the amux binary behind `ukp session` (wrapper mechanism,
|
|
102
|
+
* rg.ts rgCommand precedent). */
|
|
103
|
+
amuxCommand?: readonly string[];
|
|
95
104
|
artifactRoot?: string;
|
|
96
105
|
artifactRunId?: string;
|
|
97
106
|
now?: Date;
|
|
107
|
+
/** Overrides the door system snapshot behind `diagnose --door` for tests. */
|
|
108
|
+
doorProbe?: () => import("./capabilities/door-health.ts").DoorSystemSnapshot;
|
|
109
|
+
/** Overrides door-file reads behind `diagnose --door` for tests. */
|
|
110
|
+
readTextFile?: (path: string) => string;
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
interface CliCommandResult {
|
|
@@ -196,7 +209,9 @@ export function renderHelp(): string {
|
|
|
196
209
|
),
|
|
197
210
|
"",
|
|
198
211
|
]),
|
|
199
|
-
"Endpoint names for --endpoint come from 'ukp list'.",
|
|
212
|
+
"Endpoint names for --endpoint come from 'ukp list -g' (every registered endpoint); plain 'ukp list' shows the workspace scope.",
|
|
213
|
+
"",
|
|
214
|
+
"ukp:// references (ukp://<endpoint>/<path>) address registered endpoints, not web pages; open one with 'ukp read <uri>'.",
|
|
200
215
|
"",
|
|
201
216
|
"A Service's declared capabilities are backed by a provider (QMD, an external tool, backs search and update today); rg and nav work on endpoint files directly, no provider needed.",
|
|
202
217
|
"",
|
|
@@ -222,51 +237,6 @@ function writeCommandResult(
|
|
|
222
237
|
return result.exitCode;
|
|
223
238
|
}
|
|
224
239
|
|
|
225
|
-
/** Channel philosophy for `ukp list` (ukp_list W2): stdout is the data
|
|
226
|
-
* channel — byte contract, one row per line, the terminal wraps what it
|
|
227
|
-
* must; stderr to an INTERACTIVE terminal is the human channel — progress
|
|
228
|
-
* frames already live there, and the footnote lines (degradation warnings,
|
|
229
|
-
* drift notes) may exceed any terminal width (pins are ~44 chars, urls
|
|
230
|
-
* longer), so the terminal would otherwise break them mid-word or
|
|
231
|
-
* mid-hash. This wraps them at word boundaries (over-long tokens
|
|
232
|
-
* hard-split at the width) — only when the emitter knows the width;
|
|
233
|
-
* piped/CI stderr keeps raw single lines, the machine-parseable form. */
|
|
234
|
-
export function wrapLinesForTerminal(text: string, columns: number): string {
|
|
235
|
-
if (!Number.isSafeInteger(columns) || columns < 8) return text;
|
|
236
|
-
const wrapped: string[] = [];
|
|
237
|
-
for (const line of text.split("\n")) {
|
|
238
|
-
if (line.length <= columns) {
|
|
239
|
-
wrapped.push(line);
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
let current = "";
|
|
243
|
-
for (const token of line.split(" ")) {
|
|
244
|
-
// A token longer than the width hard-splits at the width (paths and
|
|
245
|
-
// SPKI pins have no spaces to break on); the remainder re-enters the
|
|
246
|
-
// same greedy fill so following words still pack.
|
|
247
|
-
const pieces: string[] = [];
|
|
248
|
-
let rest = token;
|
|
249
|
-
while (rest.length > columns) {
|
|
250
|
-
pieces.push(rest.slice(0, columns));
|
|
251
|
-
rest = rest.slice(columns);
|
|
252
|
-
}
|
|
253
|
-
pieces.push(rest);
|
|
254
|
-
for (const piece of pieces) {
|
|
255
|
-
if (current.length === 0) {
|
|
256
|
-
current = piece;
|
|
257
|
-
} else if (current.length + 1 + piece.length <= columns) {
|
|
258
|
-
current += ` ${piece}`;
|
|
259
|
-
} else {
|
|
260
|
-
wrapped.push(current);
|
|
261
|
-
current = piece;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
wrapped.push(current);
|
|
266
|
-
}
|
|
267
|
-
return wrapped.join("\n");
|
|
268
|
-
}
|
|
269
|
-
|
|
270
240
|
function executeVersionCommand(args: readonly string[]): CliCommandResult {
|
|
271
241
|
if (args.length === 0) {
|
|
272
242
|
return { exitCode: 0, stdout: renderVersion(), stderr: "" };
|
|
@@ -353,6 +323,8 @@ export function runCli(
|
|
|
353
323
|
currentDirectory,
|
|
354
324
|
registryPath,
|
|
355
325
|
resolveProvider: context.resolveProvider,
|
|
326
|
+
doorProbe: context.doorProbe,
|
|
327
|
+
readTextFile: context.readTextFile,
|
|
356
328
|
}), stdout, stderr);
|
|
357
329
|
}
|
|
358
330
|
|
|
@@ -364,6 +336,13 @@ export function runCli(
|
|
|
364
336
|
}), stdout, stderr);
|
|
365
337
|
}
|
|
366
338
|
|
|
339
|
+
if (command === "session") {
|
|
340
|
+
return writeCommandResultMaybeAsync(executeSessionCommand(args.slice(1), {
|
|
341
|
+
registryPath,
|
|
342
|
+
amuxCommand: context.amuxCommand,
|
|
343
|
+
}), stdout, stderr);
|
|
344
|
+
}
|
|
345
|
+
|
|
367
346
|
if (command === "guide") {
|
|
368
347
|
return writeCommandResult(executeGuideCommand(args.slice(1)), stdout, stderr);
|
|
369
348
|
}
|
|
@@ -413,22 +392,21 @@ export function runCli(
|
|
|
413
392
|
}), stdout, stderr);
|
|
414
393
|
}
|
|
415
394
|
|
|
395
|
+
if (command === "use") {
|
|
396
|
+
return writeCommandResult(executeUseCommand(args.slice(1), {
|
|
397
|
+
currentDirectory,
|
|
398
|
+
registryPath,
|
|
399
|
+
}), stdout, stderr);
|
|
400
|
+
}
|
|
401
|
+
|
|
416
402
|
if (command === "list") {
|
|
417
403
|
// Streaming delivery (ukp_list W2 / ADR 0026 rule 3): line-by-line
|
|
418
404
|
// through the same stdout writer every other print uses (console.log
|
|
419
405
|
// per line ≡ the single print's trailing-newline form, so piped bytes
|
|
420
406
|
// match the returned-text contract); progress frames only on an
|
|
421
|
-
// interactive stderr, so piped runs stay progress-silent.
|
|
422
|
-
// footnotes
|
|
423
|
-
//
|
|
424
|
-
const stderrColumns = process.stderr.columns ?? process.stdout.columns;
|
|
425
|
-
const interactivelyWrap = (result: CliCommandResult): CliCommandResult =>
|
|
426
|
-
process.stderr.isTTY === true && typeof stderrColumns === "number" && result.stderr.length > 0
|
|
427
|
-
? { ...result, stderr: wrapLinesForTerminal(result.stderr, stderrColumns) }
|
|
428
|
-
: result;
|
|
429
|
-
// Conditionality preserved (the W2 sync seam): an all-local listing
|
|
430
|
-
// stays a synchronous value wrapped synchronously; only the remote
|
|
431
|
-
// branch's promise goes through .then.
|
|
407
|
+
// interactive stderr, so piped runs stay progress-silent. stderr
|
|
408
|
+
// footnotes stay raw single lines in every mode — wrapping is the
|
|
409
|
+
// terminal's business (channel philosophy, docs/spec list-command).
|
|
432
410
|
const result = executeListCommand(args.slice(1), {
|
|
433
411
|
currentDirectory,
|
|
434
412
|
registryPath,
|
|
@@ -437,11 +415,7 @@ export function runCli(
|
|
|
437
415
|
: {}),
|
|
438
416
|
emitStdout: (line: string) => stdout(line),
|
|
439
417
|
});
|
|
440
|
-
return writeCommandResultMaybeAsync(
|
|
441
|
-
result instanceof Promise ? result.then(interactivelyWrap) : interactivelyWrap(result),
|
|
442
|
-
stdout,
|
|
443
|
-
stderr,
|
|
444
|
-
);
|
|
418
|
+
return writeCommandResultMaybeAsync(result, stdout, stderr);
|
|
445
419
|
}
|
|
446
420
|
|
|
447
421
|
if (command === "nav") {
|
package/src/commands/diagnose.ts
CHANGED
|
@@ -8,6 +8,15 @@ import { HelpRequestError, isHelpRequest } from "./flags.ts";
|
|
|
8
8
|
import { defaultQmdCommand } from "../capabilities/qmd.ts";
|
|
9
9
|
import { resolveProposeFolder } from "../capabilities/propose.ts";
|
|
10
10
|
import { rgExecutableAvailable } from "../capabilities/rg.ts";
|
|
11
|
+
import {
|
|
12
|
+
defaultDoorSystemProbe,
|
|
13
|
+
defaultReadDoorFile,
|
|
14
|
+
evaluateDoorSnapshot,
|
|
15
|
+
renderDoorHealthReport,
|
|
16
|
+
type DoorSystemProbe,
|
|
17
|
+
type DoorSystemSnapshot,
|
|
18
|
+
} from "../capabilities/door-health.ts";
|
|
19
|
+
import { homedir } from "node:os";
|
|
11
20
|
|
|
12
21
|
export interface ProviderCheck {
|
|
13
22
|
supported: boolean;
|
|
@@ -31,6 +40,15 @@ export interface DiagnoseCommandContext {
|
|
|
31
40
|
currentDirectory: string;
|
|
32
41
|
registryPath: string;
|
|
33
42
|
resolveProvider?: ProviderResolver;
|
|
43
|
+
/** Overrides the PowerShell door snapshot for tests (--door mode). */
|
|
44
|
+
doorProbe?: DoorSystemProbe;
|
|
45
|
+
/** Overrides file reads of the operator-authored door files for tests. */
|
|
46
|
+
readTextFile?: (path: string) => string;
|
|
47
|
+
/** Overrides the %USERPROFILE% expansion root for tests (--door mode). */
|
|
48
|
+
homeDir?: string;
|
|
49
|
+
/** Overrides the door-mode platform admission for tests (--door mode);
|
|
50
|
+
* process.platform in production. */
|
|
51
|
+
platform?: string;
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
export interface DiagnoseCommandResult {
|
|
@@ -52,8 +70,11 @@ export const DIAGNOSE_SPEC: UkpCommandSpec = {
|
|
|
52
70
|
name: "diagnose",
|
|
53
71
|
summary: "check a Service or endpoints for wiring problems (manifest, provider setup)",
|
|
54
72
|
group: "operations",
|
|
55
|
-
description: "Validate a Service folder or selected registered Service endpoints.",
|
|
56
|
-
usage: "[--endpoint <name> ... | -g]",
|
|
73
|
+
description: "Validate a Service folder or selected registered Service endpoints, or this host's resident doors (--door).",
|
|
74
|
+
usage: "[--door] [--endpoint <name> ... | -g]",
|
|
75
|
+
options: [
|
|
76
|
+
{ flags: "--door", help: "check this host's resident doors (Windows scheduled tasks running the print-task hidden launcher): task state, process tree, port holder, door.log tail - read-only" },
|
|
77
|
+
],
|
|
57
78
|
scope: {
|
|
58
79
|
endpointHelp: "validate one registered endpoint; repeat to validate multiple endpoints",
|
|
59
80
|
globalHelp: "validate every endpoint in the Host Registry; takes no value",
|
|
@@ -65,6 +86,16 @@ export const DIAGNOSE_SPEC: UkpCommandSpec = {
|
|
|
65
86
|
" Service (Manifest and provider wiring). -g validates every registered",
|
|
66
87
|
" endpoint; -g cannot be combined with --endpoint.",
|
|
67
88
|
"",
|
|
89
|
+
"Doors:",
|
|
90
|
+
" --door skips Service validation and checks this host's resident doors",
|
|
91
|
+
" (Windows): every scheduled task running the print-task hidden launcher",
|
|
92
|
+
" is reported with its task state, the wscript->cmd->bun process tree,",
|
|
93
|
+
" its port (and who holds it), and the door.log tail. Read-only -",
|
|
94
|
+
" repairs live in 'ukp serve --print-task' section 5 and the deployment",
|
|
95
|
+
" handbook. A door that is down is a finding (exit 1); a host with no",
|
|
96
|
+
" doors is healthy. Not combinable with --endpoint or -g; on Linux the",
|
|
97
|
+
" door is a systemd socket-activation unit (see the handbook).",
|
|
98
|
+
"",
|
|
68
99
|
].join("\n"),
|
|
69
100
|
};
|
|
70
101
|
|
|
@@ -208,18 +239,55 @@ export function renderDependencyRegistryWarnings(
|
|
|
208
239
|
}
|
|
209
240
|
|
|
210
241
|
function parseDiagnoseArgs(args: readonly string[]): {
|
|
242
|
+
door: boolean;
|
|
211
243
|
explicitEndpoints?: string[];
|
|
212
244
|
global: boolean;
|
|
213
245
|
warnings: string[];
|
|
214
246
|
} {
|
|
215
247
|
const parsed = parseKitArgs(DIAGNOSE_SPEC, args);
|
|
216
248
|
return {
|
|
249
|
+
door: (parsed.options as { door?: boolean }).door === true,
|
|
217
250
|
explicitEndpoints: parsed.scope.explicitEndpoints,
|
|
218
251
|
global: parsed.scope.global,
|
|
219
252
|
warnings: parsed.scope.warnings,
|
|
220
253
|
};
|
|
221
254
|
}
|
|
222
255
|
|
|
256
|
+
/** Door-mode platform admission (mirrors printTaskPreflightError: pure,
|
|
257
|
+
* `process.platform` in production). Linux doors are systemd units with
|
|
258
|
+
* their own check surface; running the Windows probe there is meaningless. */
|
|
259
|
+
export function doorModePlatformError(platform: string): string | undefined {
|
|
260
|
+
if (platform !== "win32") {
|
|
261
|
+
return "--door is Windows-only; on Linux the door is a systemd socket-activation unit - check it with systemctl --user status ukp-door.socket (the deployment handbook carries the recipes)";
|
|
262
|
+
}
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function executeDoorDiagnose(context: DiagnoseCommandContext): DiagnoseCommandResult {
|
|
267
|
+
const probe = context.doorProbe ?? defaultDoorSystemProbe;
|
|
268
|
+
const readTextFile = context.readTextFile ?? defaultReadDoorFile;
|
|
269
|
+
let snapshot: DoorSystemSnapshot;
|
|
270
|
+
try {
|
|
271
|
+
snapshot = probe();
|
|
272
|
+
} catch (error) {
|
|
273
|
+
return {
|
|
274
|
+
exitCode: 1,
|
|
275
|
+
stdout: "",
|
|
276
|
+
stderr: `ukp diagnose: door snapshot failed: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const doors = evaluateDoorSnapshot(snapshot, {
|
|
280
|
+
homeDir: context.homeDir ?? homedir(),
|
|
281
|
+
readTextFile,
|
|
282
|
+
});
|
|
283
|
+
const unhealthy = doors.some((door) => door.status !== "ok");
|
|
284
|
+
return {
|
|
285
|
+
exitCode: unhealthy ? 1 : 0,
|
|
286
|
+
stdout: renderDoorHealthReport(doors),
|
|
287
|
+
stderr: "",
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
223
291
|
function diagnoseBinding(
|
|
224
292
|
binding: RegistryBinding,
|
|
225
293
|
resolveProvider: ProviderResolver | undefined,
|
|
@@ -254,6 +322,16 @@ export function executeDiagnoseCommand(
|
|
|
254
322
|
|
|
255
323
|
try {
|
|
256
324
|
const parsed = parseDiagnoseArgs(args);
|
|
325
|
+
if (parsed.door) {
|
|
326
|
+
if (parsed.explicitEndpoints !== undefined || parsed.global) {
|
|
327
|
+
throw new KitUsageError("--door checks this host's resident doors and cannot be combined with --endpoint or -g");
|
|
328
|
+
}
|
|
329
|
+
const gate = doorModePlatformError(context.platform ?? process.platform);
|
|
330
|
+
if (gate !== undefined) {
|
|
331
|
+
throw new Error(gate);
|
|
332
|
+
}
|
|
333
|
+
return executeDoorDiagnose(context);
|
|
334
|
+
}
|
|
257
335
|
if (!parsed.explicitEndpoints && !parsed.global) {
|
|
258
336
|
try {
|
|
259
337
|
const report = diagnoseService(context.currentDirectory, context.resolveProvider);
|
|
@@ -4,16 +4,24 @@ Goal: use registered Services from a workspace by default, without naming an end
|
|
|
4
4
|
|
|
5
5
|
Model:
|
|
6
6
|
- Client path is separate from the provider path: it uses Services; it does not declare one.
|
|
7
|
-
- A folder may hold both roles at once (dual-role): a Service folder may also carry its own .ukp/client.toml to set its default scope. Each file is read independently.
|
|
7
|
+
- A folder may hold both roles at once (dual-role): a Service folder may also carry its own .ukp/client.toml to set its default scope. Each file is read independently; 'ukp use' never touches the Service face.
|
|
8
8
|
- A workspace .ukp/client.toml default_endpoints list selects which registered endpoints you mean when no selector is given.
|
|
9
9
|
- UKP walks up from the current directory to find the nearest .ukp/client.toml.
|
|
10
10
|
- Client Config stores endpoint names only; bindings live in the Host Registry.
|
|
11
11
|
- Explicit --endpoint <name> or -g overrides the client defaults. With no Client Config, most commands fall back to every registered endpoint.
|
|
12
12
|
- ukp update is stricter: it does not fall back to the Registry. It uses your Client Config defaults, or requires --endpoint <name> / -g.
|
|
13
|
-
- ukp register does not edit .ukp/client.toml.
|
|
14
|
-
|
|
15
|
-
1.
|
|
16
|
-
|
|
13
|
+
- ukp register does not edit .ukp/client.toml; 'ukp use' is the one-step writer.
|
|
14
|
+
|
|
15
|
+
1. Set the workspace defaults (one step)
|
|
16
|
+
ukp use endpoint-a endpoint-b
|
|
17
|
+
Writes .ukp/client.toml (default_endpoints = the names you gave, explicit
|
|
18
|
+
replace, idempotent) and makes it version-visible in the same step:
|
|
19
|
+
.ukp/.gitignore ignores machine-local state only (the instance
|
|
20
|
+
identity), so client.toml and service.toml can be committed. Existing
|
|
21
|
+
repos carrying the retired '*' template are migrated off it (a local
|
|
22
|
+
'ukp register' does the same on the service side); operator-owned
|
|
23
|
+
ignore files are left untouched.
|
|
24
|
+
Manual form: .ukp/client.toml with
|
|
17
25
|
default_endpoints = ["endpoint-a", "endpoint-b"]
|
|
18
26
|
Each name must match a registered endpoint; run 'ukp list' to see what is registered.
|
|
19
27
|
|
|
@@ -41,8 +49,13 @@ Model:
|
|
|
41
49
|
|
|
42
50
|
Remember:
|
|
43
51
|
- Client path consumes Services; provider path (ukp guide service) creates them.
|
|
44
|
-
- Client Config lists names, not paths or provider details.
|
|
52
|
+
- Client Config lists names, not paths or provider details; 'ukp use' is the one-step writer (register still does not edit it).
|
|
45
53
|
- Explicit selectors always beat Client Config defaults.
|
|
46
54
|
- Register does not edit Client Config, and Client Config does not register endpoints.
|
|
55
|
+
- Declarations (client.toml, service.toml) are committable by design; only
|
|
56
|
+
machine-local state (.ukp/instance-uid) stays ignored - the serve TLS
|
|
57
|
+
identity lives beside the host registry (~/.ukp/tls/), never in the
|
|
58
|
+
folder. Commit the config so a fresh clone restores the workspace scope
|
|
59
|
+
once endpoints are registered.
|
|
47
60
|
- Search run artifacts live outside your folders: %LOCALAPPDATA%\ukp\artifacts
|
|
48
61
|
on Windows, ~/.cache/ukp/artifacts (or $XDG_CACHE_HOME) elsewhere.
|
|
@@ -5,7 +5,7 @@ Goal: expose registered Services from one machine and consume them from another
|
|
|
5
5
|
Current model:
|
|
6
6
|
- The ssh:// path needs nothing running on the host: each invocation wakes a loopback door over the SSH connection (it serves the host Registry fresh and self-reaps when idle). Host prerequisites: the host is ssh reachable and has ukp installed the usual way (npm i -g or bun add -g - the wake finds the standard locations).
|
|
7
7
|
- The https path (LAN, public IP, or behind a reverse proxy) uses a resident door: run ukp serve under your process manager. Arbitrary consumers have no SSH credentials, so the listener itself is the trust boundary (TLS + bearer token).
|
|
8
|
-
- Prefer host door mode: run ukp serve without --endpoint to expose every local endpoint behind one port.
|
|
8
|
+
- Prefer host door mode when the whole Registry is safe to declare: run ukp serve without --endpoint to expose every local endpoint behind one port. A Registry that mixes in endpoints you do not want to expose uses --select instead: ukp serve --select a,b,c keeps one port and one door for exactly the named endpoints - everything unselected is not served, routed, or declared (probing its name behaves exactly like probing one that never existed). Reserve per-endpoint --endpoint <name> doors for exposing a single endpoint to different consumers.
|
|
9
9
|
- The door reads the host Registry fresh on each request, so newly registered local endpoints appear without restarting the door.
|
|
10
10
|
- Declared endpoint names come from the host Service Manifest or discovery document; your registry binds each one under a local handle - the declared name by default, or --name <handle> when that name is already taken (two hosts both declaring 'notes' become your 'notes' and 'ali-notes'). --endpoint during register stays an expected-name assertion on the declared name.
|
|
11
11
|
|
|
@@ -41,6 +41,27 @@ LAN HTTPS path:
|
|
|
41
41
|
ukp read --endpoint <name> notes/x.md
|
|
42
42
|
ukp rg "keyword" --endpoint <name>
|
|
43
43
|
|
|
44
|
+
Scoped keys for a second machine or an agent (instead of sharing the env token):
|
|
45
|
+
1. On the host, issue a labeled key (run WITHOUT UKP_SERVE_TOKEN set - while the
|
|
46
|
+
env token is set it takes precedence and the key file is not consulted)
|
|
47
|
+
ukp serve token add liku "notes:ro,cad:rw" # prints the raw key ONCE
|
|
48
|
+
The key lands in ~/.ukp/tokens (0600, hash-only); scopes are name:ro|rw,
|
|
49
|
+
comma-separated (full power is the explicit '*:rw'); several keys may share
|
|
50
|
+
a label for zero-downtime rotation.
|
|
51
|
+
|
|
52
|
+
2. Serve the door with no env token (the file arms the gate; it is re-read on
|
|
53
|
+
every request)
|
|
54
|
+
ukp serve --host 0.0.0.0 --tls
|
|
55
|
+
|
|
56
|
+
3. On the consuming machine, register with the raw key as the token
|
|
57
|
+
ukp register --url https://<ip>:8570 --token <raw-key>
|
|
58
|
+
A scoped key only sees its own endpoints: out-of-scope names answer exactly
|
|
59
|
+
like nonexistent ones; a read-only scope has no propose route.
|
|
60
|
+
|
|
61
|
+
4. Revoke on the host (effective on the next request - no restart)
|
|
62
|
+
ukp serve token list
|
|
63
|
+
ukp serve token revoke liku
|
|
64
|
+
|
|
44
65
|
Single endpoint mode:
|
|
45
66
|
Use this when you deliberately want to expose only one endpoint instead of the whole host Registry.
|
|
46
67
|
UKP_SERVE_TOKEN=<token> ukp serve --endpoint <name> --host 0.0.0.0 --tls
|
|
@@ -27,8 +27,8 @@ Model:
|
|
|
27
27
|
|
|
28
28
|
4. Visibility (every rg mode)
|
|
29
29
|
Hidden files (dot-prefixed, including the .ukp/ wiring) are skipped by
|
|
30
|
-
default. Add -- --hidden after '--' to include them; -
|
|
31
|
-
.gitignore rules. 'ukp read' can address hidden files directly either way.
|
|
30
|
+
default. Add -- --hidden after '--' to include them; --no-ignore also
|
|
31
|
+
lifts .gitignore rules. 'ukp read' can address hidden files directly either way.
|
|
32
32
|
|
|
33
33
|
Remember:
|
|
34
34
|
- Exact flags and the passthrough allowlist live in 'ukp rg --help'.
|