@catheadowl/ukp 0.2.2 → 0.2.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/README.md CHANGED
@@ -1,13 +1,13 @@
1
- # UKP
1
+ # UKP — Unified Knowledge Plane
2
2
 
3
3
  Folder as a Service: turn folders into named knowledge endpoints — one
4
4
  stable command surface, on this machine or your own server.
5
5
 
6
6
  Register a folder and it becomes an endpoint for humans and agents alike —
7
7
  nothing is started, the folder stays a folder. Navigate, read, and search
8
- it; propose changes back to the owner as reviewable suggestions; update
9
- provider-owned indexes (local today). No physical paths to memorize, no
10
- provider-specific commands to relearn.
8
+ it; propose changes back to the owner as reviewable suggestions; refresh
9
+ the search index behind it (local folders today). No physical paths to
10
+ memorize, no provider-specific commands to relearn.
11
11
 
12
12
  > [!NOTE]
13
13
  > Early-stage CLI (MVP) — commands and behavior may still change.
@@ -27,7 +27,8 @@ $ ukp read ukp://notes/2026/q3-plan.md # read straight from the reference
27
27
  # Q3 Plan
28
28
  Goals for Q3: ship the knowledge plane.
29
29
 
30
- $ ukp propose --endpoint notes --id typo-fix --file typo-fix.md # the write face: declare propose first
30
+ $ echo '[capabilities.propose]' >> .ukp/service.toml # the write face is opt-in: declare propose
31
+ $ ukp propose --endpoint notes --id typo-fix --file typo-fix.md
31
32
  proposal typo-fix created (revision 1)
32
33
  ```
33
34
 
@@ -53,38 +54,41 @@ predictable scope, and a single command surface. Use UKP when:
53
54
  command will touch before running it.
54
55
  - Navigate the Markdown structure of an endpoint — folders, descriptions,
55
56
  depth — with zero provider dependency.
56
- - Read endpoint-scoped references through `read/file` — if a file moves,
57
- reference recovery re-finds it (git history first, then search). The
57
+ - Read endpoint files through the built-in `read/file` capability — if a
58
+ file moves, reference recovery re-finds it best-effort (git history
59
+ first, then search). The
58
60
  optional `read/qmd` goes through QMD (an external tool — see
59
61
  Requirements). `read` and `nav` are derived defaults of every local
60
62
  Service.
61
- - Run base lexical search with `rg` across endpoint files — provider-free
62
- and on by default, shaped into `read`-ready references.
63
- - Search one endpoint, a workspace default scope, the whole local Registry,
64
- or endpoints you have explicitly declared as its dependencies
65
- (authority/context links), with explicit recursion (QMD-backed).
63
+ - Run base lexical search with `rg` across endpoint files — no search
64
+ provider needed, on by default, shaped into `read`-ready references.
65
+ - Search indexed content, QMD-backed: one endpoint, the workspace default
66
+ scope, or the whole local Registry. `--recursive` additionally searches
67
+ the endpoints a Service declares in its manifest `dependencies` —
68
+ nothing recurs unless declared.
66
69
  - Propose changes as an idempotent, reviewable suggestion — the proposal
67
70
  lands in the endpoint's inbox and the verdict stays with its owner.
68
71
  - Update provider-owned indexes through a stable UKP command (QMD-backed).
69
- - Serve an endpoint over HTTP — a single endpoint, or the whole registry as
72
+ - Serve an endpoint over HTTP — a single endpoint, or every local binding as
70
73
  a host door — when you want it reachable from other machines.
71
74
  - Want a read-only endpoint? Declare zero capabilities — an empty
72
75
  `[capabilities]` table still registers one (derived `read`/`nav` only).
73
76
  - Give agents JSON output (`--json`; `ukp read` uses `--format json`),
74
- reference sidecars, and
75
- `ukp://` handoff keys when they need machine-readable results.
77
+ reference sidecars, docid handoff keys, and durable `ukp://` references
78
+ when they need machine-readable results.
76
79
 
77
- UKP owns endpoint names, Registry bindings, Client scope, capability
78
- selection, routing, and output shape; the provider owns collection setup,
79
- indexes, ranking, and maintenance. UKP points to the delegated setup path —
80
- it does not rewrite provider config for you.
80
+ UKP owns endpoint names, Registry bindings, scope, and output shape; the
81
+ provider owns collection setup, indexes, ranking, and maintenance. UKP
82
+ points you at the provider's own setup — it does not rewrite provider
83
+ config for you.
81
84
 
82
85
  ## Install
83
86
 
84
87
  Requirements:
85
88
 
86
- - Bun `1.3.14` or newer — npm installs the `ukp` command, but the CLI runs
87
- on Bun.
89
+ - Bun `1.3.14` or newer, installed by you — the npm package installs the
90
+ `ukp` command, but the CLI runs on Bun; when `bun` is missing the command
91
+ says how to install it instead of showing a stack trace.
88
92
  - QMD on `PATH` only for the QMD-backed capabilities (`search/qmd`,
89
93
  `read/qmd`, `update/qmd`). QMD is an external tool maintained as a
90
94
  separate project; see its own release channel for installation. UKP
@@ -114,8 +118,8 @@ For a human or agent starting from a folder:
114
118
  7. `ukp search "<query>" --endpoint <name>` finds indexed matches after
115
119
  provider setup.
116
120
 
117
- No QMD installed? Stop at step 6 — `nav`, `read`, `rg`, and `propose` work on
118
- any endpoint without it.
121
+ No QMD installed? Stop at step 6 — `nav`, `read`, and `rg` work on any
122
+ endpoint without it; `propose` works wherever it's declared.
119
123
 
120
124
  An agent can carry out the same flow on your behalf.
121
125
 
@@ -125,20 +129,21 @@ For workspace defaults, use `ukp guide client` and `.ukp/client.toml`.
125
129
 
126
130
  Grouping below follows `ukp --help`; every command stays a flat
127
131
  `ukp <verb>`. `--endpoint <name>` is the canonical endpoint selector
128
- (`-c` is a compatibility alias); `-g` selects the full local Host Registry
129
- where supported.
132
+ (`ukp unregister <name>` still takes the endpoint as a positional legacy
133
+ form); `-g` selects the full local Host Registry where supported.
130
134
 
131
- The same five commands work on local and remote endpoints — consuming a
132
- remote endpoint is a one-command gesture (see below).
135
+ The same five commands — search, read, nav, rg, propose — work on local and
136
+ remote endpoints; consuming a remote endpoint is a one-command gesture
137
+ (see below).
133
138
 
134
139
  Endpoint commands:
135
140
 
136
141
  | Command | What it does |
137
142
  |---|---|
138
- | `ukp search` | Indexed search through the endpoint's provider (QMD today); `--recursive` expands endpoints explicitly declared as dependencies (authority/context). Results hand off via `ukp://` references. |
139
- | `ukp read` | Reads an endpoint-scoped resource — exact path, `ukp://` URI, or `docid` handoff key. |
143
+ | `ukp search` | Indexed search through the endpoint's provider (QMD today); `--recursive` expands the manifest's declared `dependencies`. Results hand off via docid session keys and verified `ukp://` references. |
144
+ | `ukp read` | Reads an endpoint-scoped resource — exact path, `ukp://` URI, or a `docid` handed off by search results. |
140
145
  | `ukp nav` | Markdown outline of an endpoint (`--depth`, respects `.gitignore`); on by default, configurable via `[capabilities.nav]`. |
141
- | `ukp rg` | Lexical grep (ripgrep) across endpoint files — provider-free, on by default; results become `read`-ready `ukp://` references. A missing rg binary skips the endpoint with a warning (a single-endpoint run exits non-zero). |
146
+ | `ukp rg` | Lexical grep (ripgrep) across endpoint files — provider-free, on by default; results become `read`-ready `ukp://` references. A missing rg binary skips the endpoint with a warning. |
142
147
  | `ukp propose` | Submits an idempotent change proposal (suggestion box — the owner decides what happens next). Created/unchanged/updated; the revision bumps only on `updated`. |
143
148
 
144
149
  Registry commands:
@@ -146,8 +151,8 @@ Registry commands:
146
151
  | Command | What it does |
147
152
  |---|---|
148
153
  | `ukp init service` | Creates a minimal `.ukp/service.toml`. |
149
- | `ukp register` / `ukp unregister --endpoint <name>` | Manages Host Registry bindings. `ukp register --url <url>` registers a remote endpoint 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 <name>` stays an expected-name assertion on the declared name, and `--token <token>` stores the bearer credential in the binding. Identity is pinned TOFU-style (trust on first use), and self-signed certificates are pinned automatically. A **host door** url imports every endpoint behind it — the `--endpoint` assertion narrows the import to exactly that one endpoint, `--select` narrows to a subset, and re-running refreshes idempotently. |
150
- | `ukp list` | Lists registered endpoints with their declared capabilities. Door drift shows as a stderr note (`door <origin>: N unimported endpoint(s) …`) — importing stays an explicit gesture. |
154
+ | `ukp register` / `ukp unregister --endpoint <name>` | Manages Host Registry bindings — local folders and remote endpoints alike (`ukp register --url`, see Remote in 60 seconds). `--name <handle>` disambiguates collisions: `notes` vs `ali-notes`. |
155
+ | `ukp list` | Lists registered endpoints with their declared capabilities. New endpoints on a host door show as a stderr note (`door <origin>: N unimported endpoint(s) …`) — importing stays an explicit gesture. |
151
156
 
152
157
  Operations commands:
153
158
 
@@ -156,7 +161,7 @@ Operations commands:
156
161
  | `ukp diagnose` | Checks a Service folder or endpoint scope for wiring problems. |
157
162
  | `ukp inspect` | Explains current scope, bindings, manifest capabilities, and provider availability. |
158
163
  | `ukp update` | Runs provider-owned maintenance when `update/qmd` is declared (local endpoints today). |
159
- | `ukp serve` | Serves one endpoint — or, without `--endpoint`, the whole registry as a **host door** — over HTTP: a discovery document plus the five command routes (the write face requires the endpoint to declare propose). Loopback by default; `UKP_SERVE_TOKEN` enables bearer auth; `--tls` for HTTPS. |
164
+ | `ukp serve` | Serves one endpoint — or, without `--endpoint`, every local binding as a **host door** — over HTTP (a discovery document plus the five command routes; loopback by default). |
160
165
 
161
166
  Help commands:
162
167
 
@@ -180,7 +185,7 @@ The two fastest paths — each block notes where its commands run:
180
185
  # From the consumer machine — SSH between two personal machines; the door is
181
186
  # woken on demand, nothing to start on the host (prerequisites: ssh
182
187
  # reachable + ukp on its PATH):
183
- ukp register --url ssh://<host> # once; TOFU stored
188
+ ukp register --url ssh://<host> # once; trust-on-first-use pinned
184
189
  ukp read --endpoint <name> notes/x.md # just works, like local
185
190
 
186
191
  # Native TLS on a bare IP — self-signed, pinned automatically at registration.
@@ -188,11 +193,18 @@ ukp read --endpoint <name> notes/x.md # just works, like local
188
193
  # register line runs on the consumer machine:
189
194
  UKP_SERVE_TOKEN=<token> ukp serve --endpoint <name> --host 0.0.0.0 --port 8570 --tls
190
195
  ukp register --url https://<ip>:8570 --endpoint <name> --token <token>
191
- # NAT/EIP cloud host (public IP on no NIC)? Name it explicitly — the persisted
192
- # cert re-signs over the same key, so the pin (and every registration) survives:
193
- # ukp serve … --tls --tls-san <public-ip>
194
196
  ```
195
197
 
198
+ NAT/EIP cloud host (public IP on no NIC)? Name it with
199
+ `ukp serve --tls --tls-san <public-ip>` — the persisted certificate re-signs
200
+ over the same key, so the pin (and every registration) survives; details in
201
+ the deployment guide.
202
+
203
+ Registration mechanics: a **host door** url imports every endpoint behind it
204
+ (`--endpoint <name>` checks the served name and narrows the import to that
205
+ one, `--select` narrows to a subset); `--token` is stored with the binding;
206
+ re-running a registration refreshes it idempotently.
207
+
196
208
  Remote endpoints take the same commands as local ones — search, read, nav,
197
209
  rg, propose. The full guide — the host door (one port for the whole
198
210
  registry), Caddy and Tailscale fronting, the certificate pinning lifecycle,
@@ -210,9 +222,7 @@ and the hardening posture — lives in the
210
222
 
211
223
  ## Documentation
212
224
 
213
- - [Remote deployment](https://github.com/CatheadOwl/ukp/blob/main/docs/remote-deployment.md) — serving endpoints over
214
- SSH/HTTPS, the host door, Caddy and Tailscale fronting, the certificate
215
- pinning lifecycle, and the hardening posture.
225
+ - [Remote deployment](https://github.com/CatheadOwl/ukp/blob/main/docs/remote-deployment.md) — serving endpoints across machines: the full guide.
216
226
  - [Contributing](https://github.com/CatheadOwl/ukp/blob/main/CONTRIBUTING.md) — development setup and release packaging.
217
227
  - [Changelog](https://github.com/CatheadOwl/ukp/blob/main/CHANGELOG.md) — what changed per release.
218
228
  - Repository: https://github.com/CatheadOwl/ukp ·
package/bin/ukp.js CHANGED
@@ -12,7 +12,7 @@ import { spawnSync } from "node:child_process";
12
12
  import { existsSync } from "node:fs";
13
13
  import { homedir } from "node:os";
14
14
  import { dirname, join } from "node:path";
15
- import { fileURLToPath } from "node:url";
15
+ import { fileURLToPath, pathToFileURL } from "node:url";
16
16
 
17
17
  const here = dirname(fileURLToPath(import.meta.url));
18
18
  const cli = join(here, "..", "src", "cli.ts");
@@ -68,15 +68,35 @@ function fail(detail) {
68
68
 
69
69
  const bunPath = bunCandidates().find((candidate) => existsSync(candidate));
70
70
  if (bunPath === undefined) {
71
- fail("looked for UKP_BUN, the standard install locations, and PATH");
71
+ fail("looked at UKP_BUN, the standard install locations, and PATH");
72
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)}`);
73
+
74
+ // In-process handoff when this launcher already runs under bun (bun's own
75
+ // bin links and `bunx`): importing the CLI keeps it in THIS process, where
76
+ // stdout/stderr keep their real TTY identity (columns, isTTY) — a spawnSync
77
+ // grandchild re-inherits duplicated handles, and on some hosts that layer
78
+ // degraded TTY detection (the third dogfood round: progress/wrap silently
79
+ // skipped while a direct run on the same console had both). One process
80
+ // also starts faster. The node-interpreted path (npm shims) keeps spawning.
81
+ if (runningUnderBun) {
82
+ const { runCli } = await import(pathToFileURL(cli).href);
83
+ const exit = runCli(process.argv.slice(2));
84
+ if (exit instanceof Promise) {
85
+ exit.then((code) => {
86
+ process.exitCode = code;
87
+ });
88
+ } else {
89
+ process.exitCode = exit;
90
+ }
91
+ } else {
92
+ const result = spawnSync(bunPath, [cli, ...process.argv.slice(2)], {
93
+ stdio: "inherit",
94
+ env: process.env,
95
+ });
96
+ if (result.error) {
97
+ // bunPath existed on disk a moment ago; a spawn failure here is exotic,
98
+ // but it must still land in the remedy, never a stack trace.
99
+ fail(`spawning '${bunPath}' failed: ${String(result.error.message ?? result.error)}`);
100
+ }
101
+ process.exit(result.status === null ? (result.signal !== null ? 130 : 1) : result.status);
81
102
  }
82
- process.exit(result.status === null ? (result.signal !== null ? 130 : 1) : result.status);
@@ -0,0 +1,309 @@
1
+ # Remote deployment
2
+
3
+ Operational guide for exposing UKP endpoints over the network. For the
4
+ two fastest consumer-side paths (SSH between personal machines, native TLS
5
+ on a bare IP), see the README's "Remote in 60 seconds"; this guide covers
6
+ every deployment shape end to end.
7
+
8
+ **Choosing a path by burden** (someone always holds the port — the only
9
+ question is who): the ssh:// path wakes the door on demand and adds zero
10
+ resident processes on every OS (Windows hosts enable the built-in OpenSSH
11
+ Server feature once). The https path needs a resident listener: on Linux,
12
+ systemd socket activation holds the port and spawns the door per use
13
+ (zero resident ukp); on Windows there is no systemd equivalent and a bun
14
+ process cannot natively be a service — the resident door runs as a
15
+ hidden logon task (no visible window, bounded crash retry; see the
16
+ Windows section), and only unattended-boot always-on would want a real
17
+ service wrapper (WinSW-class), or prefer ssh:// there. The https door is
18
+ the right shape when consumers have no SSH credentials.
19
+
20
+ `ukp serve` speaks plain HTTP by default; TLS and public exposure are either
21
+ **native** (`--tls` self-signs through the local openssl, `--tls-cert/--tls-key`
22
+ serve your own certificate) or **delegated to a fronting component** (reverse
23
+ proxy, SSH tunnel, overlay). The trust model is public PKI, or automatic TOFU
24
+ pinning for self-signed and private-CA certificates. **Authentication is
25
+ deny-by-default**: serving requires `UKP_SERVE_TOKEN`; tokenless serving needs
26
+ an explicit `--allow-anonymous` and is refused off loopback. A reverse proxy on
27
+ the same host forwards from the public side to the loopback bind, so proxied
28
+ deployments treat the token as mandatory (serve cannot see past its own bind
29
+ address).
30
+
31
+ ## Via SSH (two personal machines — zero extra components, zero ceremony)
32
+
33
+ The `ssh://` URL scheme makes the transport transparent AND wakes the door
34
+ on demand: each invocation runs one ssh process that opens the forward and
35
+ starts a pinned loopback `ukp serve` on the host (the door serves the host
36
+ Registry fresh and self-reaps when idle — nothing to pre-start, no token:
37
+ SSH carries encryption and auth). Later invocations within a couple of
38
+ minutes reuse the warmed SSH connection. Prerequisites on the host: ssh
39
+ reachable (your key) and ukp on its PATH. A first call that cannot find
40
+ `ukp` on the remote PATH says so with the remedy.
41
+
42
+ ```bash
43
+ ukp register --url ssh://<host> # once; the first call wakes the door
44
+ ukp read --endpoint <name> notes/x.md # just works, like local
45
+ ```
46
+
47
+ Notes:
48
+ - The `ssh://` url port selects nothing — the woken door listens on a
49
+ client-chosen loopback port per invocation.
50
+ - **Host prerequisite = a standard install.** `npm install -g
51
+ @catheadowl/ukp` (or `bun add -g`) on the host is all it takes — the wake
52
+ command prepends the standard install locations (`~/.bun/bin`,
53
+ `~/.npm-global/bin`, brew prefixes, then `$PATH`) before exec, so both
54
+ `ukp` and its bun shebang resolve even though `ssh host cmd` runs a bare
55
+ non-interactive PATH. Verified with a user-prefix npm install (ukp + bun
56
+ both in `~/.npm-global/bin`, invisible to a bare shell):
57
+ wake register 5.2s, day-2 read 4.8s, no orphans. Only exception:
58
+ nvm-style versioned layouts have no fixed path shape — link once with
59
+ `sudo ln -s "$(command -v ukp)" /usr/local/bin/ukp`.
60
+ - Consecutive invocations share an ssh multiplexing master for two minutes
61
+ (handshake amortization); Windows' native ssh has no multiplexing and
62
+ quietly pays the handshake per call.
63
+ - To lock down what SSH may run, restrict the login shell (git-shell
64
+ style). A forced `command=` in authorized_keys does NOT compose with
65
+ wake — the client chooses the door port per invocation, which a fixed
66
+ forced command cannot express.
67
+
68
+ Windows hosts work out of the box: when the host's OpenSSH default shell
69
+ (cmd.exe) rejects the POSIX wake form, the client detects the signature and
70
+ resends the pinned cmd.exe form automatically — nothing to configure beyond
71
+ the OpenSSH Server feature and your key. A powershell DefaultShell is not
72
+ supported (its `-c` quoting drops the wake form).
73
+
74
+ ## Native TLS (bare IP, no domain — zero extra components)
75
+
76
+ `--tls` self-signs on first start (identity = keypair, persisted under the
77
+ Service folder's `.ukp/tls/`; SAN covers the host's addresses). Clients pin
78
+ the certificate automatically at registration — same command as any other
79
+ remote, no flags added:
80
+
81
+ ```bash
82
+ UKP_SERVE_TOKEN=<token> ukp serve --endpoint <name> --host 0.0.0.0 --port 8570 --tls
83
+ # consumer machine (self-signed is TOFU-pinned at registration):
84
+ ukp register --url https://<ip>:8570 --endpoint <name> --token <token>
85
+ ukp read --endpoint <name> notes/x.md # just works, like local
86
+ ```
87
+
88
+ A certificate renewal that keeps the key re-anchors transparently; a new key
89
+ (reinstall) blocks with both fingerprints until you re-register. Prefer real
90
+ certificates? Let's Encrypt issues IP-address certificates (GA 2026-01) —
91
+ run certbot with a renewal timer and point `--tls-cert/--tls-key` at the
92
+ files; clients then need no pinning at all.
93
+
94
+ **NAT/EIP cloud hosts** (public IP on no NIC — Alibaba/AWS-style 1:1 NAT):
95
+ the automatic SAN coverage can never see the public address, so name it
96
+ explicitly with `--tls-san` (repeatable; DNS names too):
97
+
98
+ ```bash
99
+ UKP_SERVE_TOKEN=<token> ukp serve --endpoint <name> --host 0.0.0.0 --port 8570 \
100
+ --tls --tls-san <public-ip>
101
+ ```
102
+
103
+ A persisted certificate missing a requested entry is re-signed over the
104
+ SAME key — the pin (and thus every client registration) survives, the
105
+ banner honestly reports `self-signed identity (re-signed)`, and coverage
106
+ only grows: the re-sign keeps everything the certificate already carried
107
+ (so changing the flag list loses nothing), and already-covered or
108
+ narrower requests reuse the certificate untouched. Explicit `--tls-cert` certificates carry their own SAN, so
109
+ `--tls-san` next to them is a usage error, not a no-op.
110
+
111
+ ## Windows host (https — resident form)
112
+
113
+ Windows has no socket-activation equivalent, so the https path on a
114
+ Windows host is the resident form: one-time setup, auto-start at logon,
115
+ one bun process always running — with **no visible window** (a hidden
116
+ launcher owns the console; a popping cmd box is not an acceptable product
117
+ form) and bounded crash retry. Verified end to end (register/TOFU →
118
+ read → nav, plus zero-flash and crash-recovery drills) on a Windows 11
119
+ host with a plain user-level install. Steps 3–6 are exactly what
120
+ `ukp serve --print-task --host 0.0.0.0 --tls`
121
+ (or `--tls-cert/--tls-key`) prints for you — the generator assembles the
122
+ mechanical half (start script, hidden launcher, Task Scheduler command,
123
+ firewall rule, token placeholder); this section keeps the judgment calls
124
+ and the pitfalls the verification run caught:
125
+
126
+ 1. Install the usual way (bun's official installer + `npm i -g
127
+ @catheadowl/ukp` — the npm shim needs bun on PATH, both land in
128
+ per-user PATH dirs, which is fine here: the door is started by a
129
+ logged-on task, not a bare ssh shell).
130
+ 2. `ukp init service` in each folder, then run `ukp register` from inside
131
+ it (register takes no folder argument) for every endpoint to expose.
132
+ 3. Certificate — generate ONCE (Git for Windows' openssl works; avoids
133
+ any runtime openssl dependency of `--tls`):
134
+ `openssl req -x509 -newkey rsa:2048 -nodes -days 825 -keyout
135
+ %USERPROFILE%\.ukp\tls\key.pem -out %USERPROFILE%\.ukp\tls\cert.pem
136
+ -subj "/CN=ukp" -addext "subjectAltName=IP:<lan-ip>,DNS:localhost"`
137
+ 4. `%USERPROFILE%\.ukp\start-door.cmd` (CRLF!) — set UKP_SERVE_TOKEN,
138
+ run `ukp serve --host 0.0.0.0 --port 8570 --tls-cert ... --tls-key ...`
139
+ with the door.log redirect. No PATH export: the launcher finds bun
140
+ itself, and the logged-on task carries the per-user PATH. Write it
141
+ with a real editor (writing cmd files over ssh echo mangles `%`
142
+ escaping — copy the file instead).
143
+ 5. `%USERPROFILE%\.ukp\start-door-hidden.vbs` (CRLF, real editor) — the
144
+ hidden launcher the task actually runs: it starts the console hidden
145
+ at process creation (nothing ever flashes), captures everything to
146
+ door.log, propagates the exit code, and retries a crashed door
147
+ 3 times, 30s apart. `ukp serve --print-task` prints it verbatim.
148
+ 6. `schtasks /Create /TN ukp-door /TR "wscript.exe
149
+ \"%USERPROFILE%\.ukp\start-door-hidden.vbs\"" /SC ONLOGON /F` (from an
150
+ elevated shell — creating an ONLOGON task from a plain one fails with
151
+ Access denied) + a firewall rule for the port; `schtasks /Run /TN
152
+ ukp-door` to start now.
153
+ 7. Client: `ukp register --url https://<host>:8570 --token <token>` —
154
+ TOFU pins the certificate; re-registering refreshes the pin.
155
+
156
+ Pitfalls the verification run caught:
157
+ - **A silent bind failure**: if something already listens on the port
158
+ (a leftover manual door, say), the new door exits EADDRINUSE invisibly
159
+ — door.log carries the line (the hidden launcher captures everything);
160
+ check `netstat -ano | findstr :8570` and the PID's command line.
161
+ - ONLOGON starts at LOGON, not boot, and the launcher's crash retry is
162
+ bounded (3×30s): after an unattended reboot nobody logs into, or after
163
+ exhausted retries, the door is down until the next logon or a manual
164
+ `schtasks /Run`. Unattended-boot always-on would need WinSW/NSSM-class
165
+ wrapping — or prefer ssh:// there. (Task Scheduler's own
166
+ restart-on-failure setting does not fire on exit codes — do not rely
167
+ on it.)
168
+ - To restart the door by hand (new token, ukp upgrade, new cert):
169
+ `taskkill /PID <door-pid> /T /F`, then `schtasks /Run /TN ukp-door` —
170
+ `schtasks /End` does not reliably kill the process tree.
171
+ - `rg` on the host is the host's business — no ripgrep installed means
172
+ the remote `rg` capability reports unavailable (everything else works).
173
+
174
+ ## Socket activation (Linux — no resident process on the https path either)
175
+
176
+ systemd can hold the listening port itself and spawn the door on first
177
+ connection; the door then self-reaps after `--max-idle` seconds of no
178
+ requests and systemd re-spawns it on the next one. Nothing resident
179
+ between uses — the same zero-maintenance posture as the ssh wake, for the
180
+ consumer-facing https door (Cockpit and Ubuntu's own sshd run this way).
181
+
182
+ Two user-level units (matches a user-local install). Two systemd facts
183
+ learned the hard way: **`Environment=` does not expand
184
+ `%h`/`$HOME`** (specifiers work in `ExecStart=` paths but NOT inside
185
+ `Environment=` — a literal `%h/...` PATH yields 203/EXEC), and **units
186
+ source no profile** — so write literal paths:
187
+
188
+ ```ini
189
+ # ~/.config/systemd/user/ukp-door.socket
190
+ [Unit]
191
+ Description=ukp host door (socket-activated)
192
+
193
+ [Socket]
194
+ ListenStream=8570
195
+
196
+ # ~/.config/systemd/user/ukp-door.service
197
+ [Unit]
198
+ Description=ukp host door
199
+
200
+ [Service]
201
+ Environment=PATH=/home/<user>/.npm-global/bin:/home/<user>/.bun/bin:/usr/local/bin:/usr/bin
202
+ EnvironmentFile=-/home/<user>/.config/ukp-door.env # UKP_SERVE_TOKEN=… (chmod 600)
203
+ ExecStart=/home/<user>/.npm-global/bin/ukp serve --systemd-socket --max-idle 60 --tls
204
+ ```
205
+
206
+ ```bash
207
+ loginctl enable-linger "$USER" # let the socket answer while you're
208
+ # not logged in
209
+ systemctl --user daemon-reload
210
+ systemctl --user enable --now ukp-door.socket
211
+ # nothing is running yet — the first client connection spawns the door:
212
+ ukp register --url https://<host>:8570 --token <token> # cold start, TOFU-pins
213
+ ```
214
+
215
+ Notes:
216
+ - The port belongs to the SOCKET unit — serve runs `--systemd-socket`
217
+ (`--port` is rejected alongside) and requires `UKP_SERVE_TOKEN`: the
218
+ bind address is the unit's (possibly public), so tokenless serving is
219
+ not verifiable from inside the process.
220
+ - TLS: `--tls` self-signs on first start (identity under `~/.ukp/tls/`);
221
+ clients pin at registration. LE/mkcert: `--tls-cert/--tls-key` as usual.
222
+ - Do NOT put an HTTP health-check in front of the socket — it would wake
223
+ the door on every probe and defeat `--max-idle`.
224
+ - `systemctl --user` over bare ssh needs `XDG_RUNTIME_DIR=/run/user/$(id -u)`.
225
+ - Restarting the SOCKET while a door is still running is refused
226
+ ("Socket service already active") — `systemctl --user restart
227
+ ukp-door.service` instead, or let the door idle out first.
228
+ - **NAT/EIP hosts**: `--tls` self-signs with the machine's interface
229
+ addresses — a public EIP is not among them, and registration would fail
230
+ CERT_ALTNAME_INVALID. Add `--tls-san <public-ip>` next to `--tls`
231
+ (see Native TLS above): the SAN grows, the key (and pin) does not.
232
+
233
+ ## Host door (one host, many endpoints — one gesture, zero tokens)
234
+
235
+ Trust's natural unit is the host, not the endpoint: if you can ssh to a
236
+ machine, per-endpoint tokens are ceremony — and with the on-demand wake you
237
+ don't even run the door yourself: `ukp register --url ssh://<host>`
238
+ wakes a loopback door (`docker DOCKER_HOST=ssh://` posture — SSH carries
239
+ encryption and auth), imports everything behind it, and the door reaps
240
+ itself when idle:
241
+
242
+ ```bash
243
+ # nothing to run on the host first — one gesture imports everything:
244
+ ukp register --url ssh://<host>
245
+ # door ssh://<host>: 2 endpoint(s)
246
+ # imported: notes (search,propose)
247
+ # imported: archive (-)
248
+ ukp list # flat rows, urls like ssh://<host>/notes
249
+ ukp read ukp://notes/plan.md # day-2 is byte-identical to today's remotes
250
+ ```
251
+
252
+ A manually started door still works (e.g. under systemd, for a host you
253
+ deliberately keep serving): `ukp serve --allow-anonymous` without
254
+ `--endpoint` exposes the same whole-registry door on a fixed loopback port —
255
+ but the wake makes that an explicit choice, not a prerequisite.
256
+
257
+ Imported endpoints are ordinary remote bindings (`url = <door>/<name>`); a
258
+ name already bound elsewhere is skipped with a visible reason, re-running
259
+ the import refreshes pins idempotently, and `--select a,b` narrows the
260
+ take. When the door grows, `ukp list` says so on stderr (`1 unimported
261
+ endpoint(s): …`) — importing it stays your call. The consumer variant of
262
+ the same door is public: `ukp serve --host 0.0.0.0 --tls-cert …` with
263
+ `UKP_SERVE_TOKEN`, imported with `ukp register --url https://<ip>:8570
264
+ --token <t>` for the whole door, or `ukp register --url https://<ip>:8570
265
+ --endpoint <name> --token <t>` for one asserted endpoint (token and pinned
266
+ certificate copied into each binding).
267
+
268
+ ## Behind Caddy (public domain)
269
+
270
+ ```
271
+ # Caddyfile — automatic Let's Encrypt
272
+ kb.example.com {
273
+ reverse_proxy 127.0.0.1:8570
274
+ }
275
+ ```
276
+
277
+ ```bash
278
+ UKP_SERVE_TOKEN=<token> ukp serve --endpoint <name> --host 127.0.0.1 --port 8570
279
+ # on the consumer machine:
280
+ UKP_ENDPOINT_<NAME>_TOKEN=<token> ukp register --url https://kb.example.com
281
+ ```
282
+
283
+ ## Via Tailscale (two machines on any network — including the same LAN)
284
+
285
+ Enable HTTPS certificates on your tailnet once (admin console: DNS →
286
+ MagicDNS → HTTPS Certificates); Tailscale then provisions Let's Encrypt
287
+ certificates for your `*.ts.net` hostnames.
288
+
289
+ ```bash
290
+ # both machines: tailscale up (same tailnet)
291
+ # server machine:
292
+ UKP_SERVE_TOKEN=<token> ukp serve --endpoint <name> --host 127.0.0.1 --port 8570
293
+ tailscale serve --bg --https=443 http://127.0.0.1:8570
294
+ # consumer machine:
295
+ UKP_ENDPOINT_<NAME>_TOKEN=<token> ukp register --url https://<machine>.<tailnet>.ts.net
296
+ ```
297
+
298
+ The overlay covers LAN and roaming machines alike; a fully offline LAN (no
299
+ coordination reachability) is the one gap — a dedicated private-CA recipe is
300
+ not covered here.
301
+
302
+ ## Hardening posture
303
+
304
+ - rate limiting, IP allowlists, and audit logging live at the proxy layer —
305
+ the `ukp` core stays thin;
306
+ - multiple client tokens: comma-separate them — `UKP_SERVE_TOKEN=alice,bob`
307
+ (any listed token authorizes);
308
+ - identity pinning is TOFU on `instance_uid`: a changed endpoint warns by
309
+ default; set `UKP_TOFU=block` on hostile networks to refuse it outright.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catheadowl/ukp",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3.14"
@@ -29,6 +29,7 @@
29
29
  "files": [
30
30
  "bin",
31
31
  "src",
32
+ "docs",
32
33
  "README.md",
33
34
  "LICENSE",
34
35
  "package.json"
@@ -442,7 +442,7 @@ export function runPropose(request: ProposeRequest, context: ProposeContext): Pr
442
442
  failure: {
443
443
  errorClass: "capability-undeclared",
444
444
  message: `endpoint '${binding.name}' does not declare the propose capability`
445
- + ` — declare [capabilities.propose] in the Service Manifest (.ukp/service.toml); 'ukp guide propose' walks through it`,
445
+ + ` - declare [capabilities.propose] in the Service Manifest (.ukp/service.toml); 'ukp guide propose' walks through it`,
446
446
  },
447
447
  };
448
448
  }
@@ -870,7 +870,7 @@ function executeResolvedRead(
870
870
  return readFailure(
871
871
  "resource-missing",
872
872
  `resource-missing '${request.path}' in endpoint '${binding.name}'`
873
- + ` (absolute paths address files exactly; the mapped route does not exist — the target may live one level deeper, e.g. under src/)\n`
873
+ + ` (absolute paths address files exactly; the mapped route does not exist - the target may live one level deeper, e.g. under src/)\n`
874
874
  + formatRecoveryCandidates(recovered.recovery.candidates).trimEnd(),
875
875
  recovered.recovery,
876
876
  );