@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.
@@ -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
- * `<serviceFolder>/.ukp/tls/` (machine-local, gitignored — the instance-uid
128
- * posture). openssl signs it because node:crypto cannot issue X.509; the
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. The stderr
422
- // footnotes wrap at the terminal width there too — piped stderr keeps
423
- // the raw single lines (wrapLinesForTerminal's contract).
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") {
@@ -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. Create a workspace Client Config
16
- .ukp/client.toml at the workspace root:
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; -uu also lifts
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'.