@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 CHANGED
@@ -12,6 +12,10 @@ memorize, no provider-specific commands to relearn.
12
12
  > [!NOTE]
13
13
  > Early-stage CLI (MVP) — commands and behavior may still change.
14
14
 
15
+ ```bash
16
+ npm install -g @catheadowl/ukp # or: bun add -g @catheadowl/ukp
17
+ ```
18
+
15
19
  ```bash
16
20
  $ ukp init service --name notes && ukp register
17
21
  initialized Service: notes
@@ -27,11 +31,15 @@ $ ukp read ukp://notes/2026/q3-plan.md # read straight from the reference
27
31
  # Q3 Plan
28
32
  Goals for Q3: ship the knowledge plane.
29
33
 
30
- $ echo '[capabilities.propose]' >> .ukp/service.toml # the write face is opt-in: declare propose
34
+ $ echo '[capabilities.propose]' >> .ukp/service.toml # the write path is opt-in: declare propose
35
+ $ echo '# typo fix' > typo-fix.md # a proposal is any Markdown draft
31
36
  $ ukp propose --endpoint notes --id typo-fix --file typo-fix.md
32
37
  proposal typo-fix created (revision 1)
33
38
  ```
34
39
 
40
+ A `ukp://` reference is not a web link — it names a registered endpoint
41
+ (local or remote); open one with `ukp read <uri>`.
42
+
35
43
  ## Why UKP
36
44
 
37
45
  Knowledge work starts in folders, but repeated use needs stable names,
@@ -50,8 +58,9 @@ predictable scope, and a single command surface. Use UKP when:
50
58
 
51
59
  - Register local folders as named knowledge endpoints — a registered folder
52
60
  is a *Service*, addressed by its endpoint name.
53
- - `ukp inspect` shows which endpoints the current scope selects — see what a
54
- command will touch before running it.
61
+ - `ukp inspect` shows which endpoints the current scope selects (the
62
+ nearest `.ukp/client.toml`; every registered endpoint when there is
63
+ none) — see what a command will touch before running it.
55
64
  - Navigate the Markdown structure of an endpoint — folders, descriptions,
56
65
  depth — with zero provider dependency.
57
66
  - Read endpoint files through the built-in `read/file` capability — if a
@@ -71,13 +80,15 @@ predictable scope, and a single command surface. Use UKP when:
71
80
  - Propose changes as an idempotent, reviewable suggestion — the proposal
72
81
  lands in the endpoint's inbox and the verdict stays with its owner.
73
82
  - Update provider-owned indexes through a stable UKP command (QMD-backed).
74
- - Serve an endpoint over HTTP — a single endpoint, or every local binding as
75
- a host door — when you want it reachable from other machines.
83
+ - Serve an endpoint over HTTP — a single endpoint, a `--select`ed subset, or
84
+ every local binding as a host door — when you want it reachable from other
85
+ machines.
76
86
  - Want a read-only endpoint? Declare zero capabilities — an empty
77
87
  `[capabilities]` table still registers one (derived `read`/`nav` only).
78
88
  - Give agents JSON output (`--json`; `ukp read` uses `--format json`),
79
89
  reference sidecars, docid handoff keys, and durable `ukp://` references
80
- when they need machine-readable results.
90
+ when they need machine-readable results; every `ukp://` reference opens
91
+ with `ukp read <uri>`.
81
92
 
82
93
  UKP owns endpoint names, Registry bindings, scope, and output shape; the
83
94
  provider owns collection setup, indexes, ranking, and maintenance. UKP
@@ -125,14 +136,17 @@ endpoint without it; `propose` works wherever it's declared.
125
136
 
126
137
  An agent can carry out the same flow on your behalf.
127
138
 
128
- For workspace defaults, use `ukp guide client` and `.ukp/client.toml`.
139
+ For workspace defaults, run `ukp use <endpoint>` (see `ukp guide client`) —
140
+ it writes `.ukp/client.toml` and keeps it committable in one step.
129
141
 
130
142
  ## Commands
131
143
 
132
- Grouping below follows `ukp --help`; every command stays a flat
133
- `ukp <verb>`. `--endpoint <name>` is the canonical endpoint selector
134
- (`ukp unregister <name>` still takes the endpoint as a positional legacy
135
- form); `-g` selects the full local Host Registry where supported.
144
+ Grouping below follows `ukp --help`; primary capability commands stay a flat
145
+ `ukp <verb>`, while multi-verb capability families appear as object command
146
+ groups (`ukp init service`, `ukp guide <topic>`). `--endpoint <name>`
147
+ is the canonical endpoint selector (`ukp unregister <name>` still takes the
148
+ endpoint as a positional legacy form); `-g` selects the full local Host
149
+ Registry where supported.
136
150
 
137
151
  The same five commands — search, read, nav, rg, propose — work on local and
138
152
  remote endpoints; consuming a remote endpoint is a one-command gesture
@@ -142,7 +156,7 @@ Endpoint commands:
142
156
 
143
157
  | Command | What it does |
144
158
  |---|---|
145
- | `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. |
159
+ | `ukp search` | Indexed search through the endpoint's provider (QMD today); `--recursive` expands the manifest's declared `dependencies`. Results hand off a docid handoff key and verified `ukp://` references. |
146
160
  | `ukp read` | Reads an endpoint-scoped resource — exact path, `ukp://` URI, or a `docid` handed off by search results. |
147
161
  | `ukp nav` | Markdown outline of an endpoint (`--depth`, respects `.gitignore`); on by default, configurable via `[capabilities.nav]`. |
148
162
  | `ukp rg` | Lexical grep (ripgrep) across endpoint files — provider-free, on by default; `--files` enumerates them instead of searching. Results become `read`-ready `ukp://` references. A missing rg binary skips the endpoint with a warning. |
@@ -152,9 +166,14 @@ Registry commands:
152
166
 
153
167
  | Command | What it does |
154
168
  |---|---|
155
- | `ukp init service` | Creates a minimal `.ukp/service.toml`. |
156
- | `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`. |
157
- | `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. |
169
+ | `ukp init service` | Creates a minimal `.ukp/service.toml`. The `.ukp/.gitignore` it drops ignores machine-local state only (the instance identity; the serve TLS identity lives in `~/.ukp/tls/`) — `service.toml` and `client.toml` are pure declarations and committable by design (a fresh clone re-registers in one `ukp register`). |
170
+ | `ukp use <endpoint> [endpoint...]` | One-step consumer gesture: sets this workspace's `default_endpoints` (`.ukp/client.toml`, explicit replace, idempotent — every name must be registered) and keeps the config version-visible in the same step. `ukp inspect` confirms the active scope. |
171
+ | `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`. A folder that moved re-binds in one step: `ukp register --rebind` from the new location (local bindings). |
172
+ | `ukp list` | Lists endpoint bindings and capabilities under the **workspace scope** by default (nearest `.ukp/client.toml`; every registered endpoint when there is none); `-g` lists the full Host Registry without reading the client config. `--json` emits the offline machine snapshot (`ukp.list.v1`) riding the same scope — stable field names, the same data core as the text listing, zero network (remote bindings are registry facts only, so their capability fields are `null`); pin `-g` for the full snapshot. New endpoints on a host door show as a stderr note (`door <origin>: N unimported endpoint(s) …`) — importing stays an explicit gesture. |
173
+
174
+ A legacy `.ukp/.gitignore` (the retired `*` self-ignore template) migrates
175
+ to this posture in passing whenever `ukp register` (local folders) or
176
+ `ukp use` meets one.
158
177
 
159
178
  Operations commands:
160
179
 
@@ -163,7 +182,7 @@ Operations commands:
163
182
  | `ukp diagnose` | Checks a Service folder or endpoint scope for wiring problems. |
164
183
  | `ukp inspect` | Explains current scope, bindings, manifest capabilities, and provider availability. |
165
184
  | `ukp update` | Runs provider-owned maintenance when `update/qmd` is declared (local endpoints today). |
166
- | `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). |
185
+ | `ukp serve` | Serves one endpoint — or, without `--endpoint`, local bindings as a **host door** (every one of them, or just the ones `--select` names — unselected endpoints are not served, routed, or declared) — over HTTP (a discovery document plus the five command routes; loopback by default). |
167
186
 
168
187
  Help commands:
169
188
 
@@ -180,9 +199,11 @@ Help commands:
180
199
  ## Remote in 60 seconds
181
200
 
182
201
  `ukp serve` exposes an endpoint over HTTP. Authentication is deny-by-default:
183
- serving with a `--host` beyond loopback requires `UKP_SERVE_TOKEN` (tokenless
184
- serving needs an explicit `--allow-anonymous` and is refused off loopback).
185
- The two fastest paths — each block notes where its commands run:
202
+ serving with a `--host` beyond loopback requires a credential — the
203
+ `UKP_SERVE_TOKEN` env token (full power) or a labeled key issued into
204
+ the `~/.ukp/tokens` file (tokenless serving needs an explicit
205
+ `--allow-anonymous` and is refused off loopback). The two fastest
206
+ paths — each block notes where its commands run:
186
207
 
187
208
  ```bash
188
209
  # From the consumer machine — SSH between two personal machines; the door is
@@ -198,15 +219,34 @@ UKP_SERVE_TOKEN=<token> ukp serve --endpoint <name> --host 0.0.0.0 --port 8570 -
198
219
  ukp register --url https://<ip>:8570 --endpoint <name> --token <token>
199
220
  ```
200
221
 
222
+ Handing a second machine or an agent a narrower key? Issue labeled, scoped,
223
+ revocable keys instead of sharing the env token (ON THE HOST):
224
+
225
+ ```bash
226
+ ukp serve token add liku "notes:ro,cad:rw" # prints the raw key ONCE
227
+ ukp serve token list # labels, key ids, scopes
228
+ ukp serve token revoke liku # next request is a 401
229
+ ```
230
+
231
+ Scopes are `name:ro|rw` comma-separated (full power is the explicit `*:rw`);
232
+ the file stores hashes only, and the door re-reads it on every request —
233
+ deleting a line revokes without a restart. A scoped key sees only its own
234
+ endpoints: out-of-scope names answer exactly like nonexistent ones. While
235
+ `UKP_SERVE_TOKEN` is set it takes precedence and the file is not consulted.
236
+
201
237
  NAT/EIP cloud host (public IP on no NIC)? Name it with
202
238
  `ukp serve --tls --tls-san <public-ip>` — the persisted certificate re-signs
203
239
  over the same key, so the pin (and every registration) survives; details in
204
240
  the deployment guide.
205
241
 
206
- Registration mechanics: a **host door** url imports every endpoint behind it
242
+ Registration mechanics: a **host door** url imports every endpoint it declares
207
243
  (`--endpoint <name>` checks the served name and narrows the import to that
208
244
  one, `--select` narrows to a subset); `--token` is stored with the binding;
209
- re-running a registration refreshes it idempotently.
245
+ re-running a registration refreshes it idempotently. A host whose registry
246
+ mixes shareable knowledge with internal projects can serve
247
+ `ukp serve --select a,b,c` — one port for exactly those endpoints; everything
248
+ the door does not select behaves as if it did not exist (discovery, routes,
249
+ and 404 rosters alike).
210
250
 
211
251
  Remote endpoints take the same commands as local ones — search, read, nav,
212
252
  rg, propose. The full guide — the host door (one port for the whole
@@ -214,6 +254,36 @@ registry), Caddy and Tailscale fronting, the certificate pinning lifecycle,
214
254
  and the hardening posture — lives in the
215
255
  [deployment guide](https://github.com/CatheadOwl/ukp/blob/main/docs/remote-deployment.md).
216
256
 
257
+ ## Security
258
+
259
+ UKP is local-first: nothing listens on the network until you run
260
+ `ukp serve`. When you do, the surface is deny-by-default — serving beyond
261
+ loopback without a credential is refused, and anonymous serving is an
262
+ explicit flag that only works on loopback. Credentials come in two forms:
263
+ the `UKP_SERVE_TOKEN` env token (full power, takes precedence while set)
264
+ and labeled keys in the 0600 `~/.ukp/tokens` file (hash-only storage,
265
+ required per-key scopes, revocation by deleting the line — effective on
266
+ the next request). Transport is `https://`, pinned
267
+ automatically at registration (a certificate that renews on the same key
268
+ re-pins silently; a different key hard-blocks with both fingerprints), or
269
+ `ssh://`, where SSH carries both encryption and authentication. A
270
+ `--select` door neither routes, declares, nor lists unselected endpoints —
271
+ excluded and unknown names return byte-identical 404s, and a scoped key's
272
+ out-of-scope endpoints answer the same way.
273
+
274
+ Accepted tradeoffs, disclosed rather than hidden: tokens are long-lived
275
+ static bearer secrets stored client-side in the 0600 registry file or an
276
+ environment variable (the same tradeoff as `~/.aws/credentials`); the
277
+ Windows door generator (`ukp serve --print-task`) embeds the token in a
278
+ 0600 `start-door.cmd` it never prints; identity pinning is
279
+ trust-on-first-use (an SSH-style first-connect window). Hardening
280
+ guidance — token strength, reverse proxies and their same-machine
281
+ blind spot, firewalls — is in the
282
+ [deployment guide](https://github.com/CatheadOwl/ukp/blob/main/docs/remote-deployment.md).
283
+
284
+ Found something? Report it privately — see [SECURITY.md](SECURITY.md);
285
+ please don't use public issues for security reports.
286
+
217
287
  ## Not Yet
218
288
 
219
289
  - remote operation of `update`, and a formally specified network protocol
@@ -226,6 +296,8 @@ and the hardening posture — lives in the
226
296
  ## Documentation
227
297
 
228
298
  - [Remote deployment](https://github.com/CatheadOwl/ukp/blob/main/docs/remote-deployment.md) — serving endpoints across machines: the full guide.
299
+ Also ships inside the npm package (`docs/remote-deployment.md` in the
300
+ installed package); the other docs live in the GitHub repository.
229
301
  - [Contributing](https://github.com/CatheadOwl/ukp/blob/main/CONTRIBUTING.md) — development setup and release packaging.
230
302
  - [Changelog](https://github.com/CatheadOwl/ukp/blob/main/CHANGELOG.md) — what changed per release.
231
303
  - Repository: https://github.com/CatheadOwl/ukp ·
package/SECURITY.md ADDED
@@ -0,0 +1,69 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Report privately using GitHub security advisories:
6
+
7
+ https://github.com/CatheadOwl/ukp/security/advisories/new
8
+
9
+ Please do not open public issues for security reports. We follow
10
+ coordinated vulnerability disclosure (the CERT process, with the 90-day
11
+ convention as the default clock) and credit reporters unless they prefer
12
+ to stay anonymous. Good-faith research on your own deployments is welcome.
13
+
14
+ ## Supported versions
15
+
16
+ Only the latest published release line receives security fixes.
17
+
18
+ ## Scope
19
+
20
+ In scope:
21
+
22
+ - the `ukp` CLI runtime: local capability commands, `ukp serve`
23
+ (transport, authentication, TLS identity, door routing), the generated
24
+ door artifacts (`ukp serve --print-task`), and credential handling on
25
+ consumer machines;
26
+ - the published package supply chain (npm tarball, GitHub repository).
27
+
28
+ Out of scope:
29
+
30
+ - hardening of your own host, SSH daemon, or reverse proxy — deployment
31
+ guidance exists, but operator-side misconfiguration is not a UKP
32
+ vulnerability;
33
+ - private working materials that are not part of the published package.
34
+
35
+ ## Posture summary
36
+
37
+ - Local-first: no network listener exists until you run `ukp serve`.
38
+ - Deny-by-default serving: serving beyond loopback without
39
+ `UKP_SERVE_TOKEN` is refused; anonymous serving is an explicit flag and
40
+ loopback-only.
41
+ - Transport: `https://` with registration-time pinning (self-signed
42
+ certificates are pinned by SPKI; a renewal on the same key re-pins
43
+ transparently, a different key hard-blocks) or `ssh://`, where SSH
44
+ carries both encryption and authentication.
45
+ - Door scoping: a `--select` door neither routes, declares, nor lists
46
+ unselected endpoints; excluded and unknown names return byte-identical
47
+ 404s.
48
+
49
+ Deployment hardening (token strength, reverse proxies, firewalls,
50
+ systemd) is covered in the
51
+ [deployment guide](https://github.com/CatheadOwl/ukp/blob/main/docs/remote-deployment.md).
52
+
53
+ ## Known, accepted tradeoffs
54
+
55
+ Disclosed rather than hidden; each has a documented trigger for change:
56
+
57
+ - Tokens are long-lived static bearer secrets. OAuth flows are deferred
58
+ until a multi-tenant or hosted scenario exists.
59
+ - Read and write use the same token today; separation becomes mandatory
60
+ with the first remote content-write capability.
61
+ - Client-side tokens live in the 0600 registry file or an environment
62
+ variable, in plaintext — the same tradeoff as `~/.aws/credentials`.
63
+ - The Windows door artifact embeds the token in a 0600 `start-door.cmd`
64
+ and never prints it; a file that ever left the machine burns the token.
65
+ - Identity pinning is trust-on-first-use, with the SSH-style
66
+ first-connect window that implies.
67
+ - npm provenance attestations prove where and how a package was built,
68
+ not that the code is safe — verify with `npm audit signatures` and
69
+ treat provenance as one signal among several.
package/bin/ukp.js CHANGED
@@ -56,28 +56,52 @@ function bunCandidates() {
56
56
  return candidates;
57
57
  }
58
58
 
59
- function fail(detail) {
59
+ function fail(detail, hint = "") {
60
60
  process.stderr.write(
61
61
  "ukp: no bun executable found (" + detail + ").\n"
62
62
  + "ukp runs on bun — install it (https://bun.sh), or point UKP_BUN at the\n"
63
63
  + "bun executable. Standard install locations (~/.bun/bin and the npm\n"
64
- + "global prefix) are checked before PATH.\n",
64
+ + "global prefix) are checked before PATH.\n"
65
+ + hint,
65
66
  );
66
67
  process.exit(1);
67
68
  }
68
69
 
70
+ /** Windows-only diagnosis for a UKP_BUN that the user's shell accepts but
71
+ * existsSync rejects: name the shape mismatch instead of implying the file
72
+ * is absent. Two real shapes (0.3.0 release review): Git Bash converts an
73
+ * exported MSYS path (/c/.../bun) to a native form WITHOUT the .exe
74
+ * extension before this launcher sees it, so the .exe sibling exists while
75
+ * the named path does not; and a variable set outside a converting shell
76
+ * (cmd/PowerShell) reaches us as the raw /c/... or /cygdrive/... form,
77
+ * which Windows file APIs cannot resolve at all. */
78
+ function ukpBunHint() {
79
+ const value = process.env.UKP_BUN;
80
+ if (!isWindows || typeof value !== "string") return "";
81
+ if (/^\/(?:[a-z]|cygdrive\/[a-z])\//i.test(value)) {
82
+ return "ukp: UKP_BUN looks like an MSYS-style path; Windows needs a native "
83
+ + "form like C:/path/to/bun.exe\n";
84
+ }
85
+ if (!/\.exe$/i.test(value) && existsSync(value + ".exe")) {
86
+ return "ukp: UKP_BUN omits the .exe extension (the executable sits next to "
87
+ + "it); use its full name, e.g. C:/path/to/bun.exe\n";
88
+ }
89
+ return "";
90
+ }
91
+
69
92
  const bunPath = bunCandidates().find((candidate) => existsSync(candidate));
70
93
  if (bunPath === undefined) {
71
- fail("looked at UKP_BUN, the standard install locations, and PATH");
94
+ fail("looked at UKP_BUN, the standard install locations, and PATH", ukpBunHint());
72
95
  }
73
96
 
74
97
  // In-process handoff when this launcher already runs under bun (bun's own
75
98
  // 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
99
+ // stdout/stderr keep their real TTY identity (isTTY) — a spawnSync
77
100
  // 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.
101
+ // degraded TTY detection (the third dogfood round: progress frames
102
+ // silently skipped while a direct run on the same console had them). One
103
+ // process also starts faster. The node-interpreted path (npm shims) keeps
104
+ // spawning.
81
105
  if (runningUnderBun) {
82
106
  const { runCli } = await import(pathToFileURL(cli).href);
83
107
  const exit = runCli(process.argv.slice(2));
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Operational recipes for serving UKP endpoints remotely — ssh wake, native TLS, Windows resident door, socket activation, fronting proxies, scoped keys, and hardening posture
3
+ ---
4
+
1
5
  # Remote deployment
2
6
 
3
7
  Operational guide for exposing UKP endpoints over the network. For the
@@ -28,6 +32,17 @@ the same host forwards from the public side to the loopback bind, so proxied
28
32
  deployments treat the token as mandatory (serve cannot see past its own bind
29
33
  address).
30
34
 
35
+ The complete route list (ukp-remote v1) — what a client or a fronting proxy's
36
+ path allowlist must account for: `GET /.well-known/ukp.json` (the public
37
+ discovery document — the door's roster document in host door mode, where each
38
+ endpoint also serves its own at `/e/<name>/.well-known/ukp.json`), the read
39
+ face — `POST /v1/search`, `GET /v1/read`, `GET /v1/nav`, `GET /v1/rg` (each
40
+ prefixed `/e/<name>` at a door) — and the one write route, `PUT
41
+ /v1/propose/<id>` (`PUT /e/<name>/v1/propose/<id>` at a door): the proposal
42
+ upsert. Propose needs the endpoint to declare the capability, and a scoped
43
+ key must carry that endpoint at `rw` (`ro` keys reach the read face only —
44
+ see Scoped keys below).
45
+
31
46
  ## Via SSH (two personal machines — zero extra components, zero ceremony)
32
47
 
33
48
  The `ssh://` URL scheme makes the transport transparent AND wakes the door
@@ -80,9 +95,12 @@ supported (its `-c` quoting drops the wake form).
80
95
 
81
96
  ## Native TLS (bare IP, no domain — zero extra components)
82
97
 
83
- `--tls` self-signs on first start (identity = keypair, persisted under the
84
- Service folder's `.ukp/tls/`; SAN covers the host's addresses). Clients pin
85
- the certificate automatically at registration — same command as any other
98
+ `--tls` self-signs on first start (identity = keypair, persisted in the
99
+ machine home beside the host registry — `~/.ukp/tls/<endpoint-name>/` for a
100
+ single endpoint, `~/.ukp/tls/` for a host door; SAN covers the host's
101
+ addresses, so the identity never travels with the knowledge folder and a
102
+ re-cloned endpoint on the same host keeps its pin). Clients pin the
103
+ certificate automatically at registration — same command as any other
86
104
  remote, no flags added:
87
105
 
88
106
  ```bash
@@ -115,6 +133,57 @@ only grows: the re-sign keeps everything the certificate already carried
115
133
  narrower requests reuse the certificate untouched. Explicit `--tls-cert` certificates carry their own SAN, so
116
134
  `--tls-san` next to them is a usage error, not a no-op.
117
135
 
136
+ ## Scoped keys (second machines, agents — labeled, revocable)
137
+
138
+ `UKP_SERVE_TOKEN` is the full-power shortcut: any listed token reaches every
139
+ endpoint and the write face. When you hand access to a second machine or an
140
+ agent, issue a **labeled, scoped key** instead — the issuance face is a serve
141
+ subcommand, and the credential store is a plain file you can audit:
142
+
143
+ ```bash
144
+ # ON THE HOST (run without UKP_SERVE_TOKEN set — while the env token is
145
+ # present it takes precedence and the file is not consulted at all):
146
+ ukp serve token add liku "notes:ro,cad:rw"
147
+ # issued key for 'liku' (scopes: notes:ro,cad:rw)
148
+ # ukp_liku_3f9c...TQ <- the raw key, shown ONCE, never stored
149
+ # stored in ~/.ukp/tokens (0600, hash-only)...
150
+
151
+ # serve the door with NO env token — the key file arms the gate:
152
+ ukp serve --host 0.0.0.0 --tls
153
+
154
+ # consumer machine registers with the raw key as the token:
155
+ ukp register --url https://<ip>:8570 --token ukp_liku_...
156
+ ```
157
+
158
+ How the pieces behave:
159
+
160
+ - **Scopes** are `name:ro|rw`, comma-separated (Docker-registry style); full
161
+ power is the explicit `*:rw` — a key with no scopes is a usage error, not a
162
+ wildcard. `rw` adds the propose route; `ro` keys get read/search/nav/rg
163
+ only. An endpoint outside the key's scope answers exactly like a name that
164
+ does not exist (same 404, same roster) — the key cannot even learn what it
165
+ cannot see.
166
+ - **The file** is `~/.ukp/tokens`, one line per key (`label key-id sha256$
167
+ hash scopes`), 0600, hash-only — a leaked file is not a leaked credential.
168
+ On POSIX, group/other permissions make the file unusable (machine refusal,
169
+ OpenSSH-style); Windows relies on the %USERPROFILE% ACLs instead.
170
+ - **Revocation is deleting the line.** `ukp serve token revoke <label>`
171
+ removes every key carrying that label; the running door re-reads the file
172
+ on every request, so the next call is a 401 — no restart, no deploy step.
173
+ Hand-editing (deleting or `#`-commenting a line) works too.
174
+ - **Rotation** is zero-downtime: `token add` with an existing label keeps the
175
+ old key active; hand out the new one, then revoke the label when the old
176
+ key's last consumer has switched. `ukp serve token list` shows labels, key
177
+ ids, and scopes (never hashes).
178
+ - **Chain order**: while `UKP_SERVE_TOKEN` is set, the env token wins and the
179
+ file is ignored — the env is the shortcut form of the same gate (the same
180
+ order as `AWS_*` env vars over `~/.aws/credentials`, or `GH_TOKEN` over the
181
+ stored gh login). An env var set to an empty value counts as unset.
182
+
183
+ The Windows resident form (`--print-task --write`) interpolates the env token
184
+ into `start-door.cmd`; a file-key door instead wants the cmd's `set
185
+ "UKP_SERVE_TOKEN=..."` line removed so the key file arms the gate.
186
+
118
187
  ## Windows host (https — resident form)
119
188
 
120
189
  Windows has no socket-activation equivalent, so the https path on a
@@ -123,12 +192,23 @@ one bun process always running — with **no visible window** (a hidden
123
192
  launcher owns the console; a popping cmd box is not an acceptable product
124
193
  form) and bounded crash retry. Verified end to end (register/TOFU →
125
194
  read → nav, plus zero-flash and crash-recovery drills) on a Windows 11
126
- host with a plain user-level install. Steps 3–6 are exactly what
195
+ host with a plain user-level install; the task recipe and the log
196
+ ownership were re-verified end to end on 2026-09-23 (unelevated
197
+ per-user registration, no execution time limit, whole-tree restart).
198
+ Steps 3–6 are exactly what
127
199
  `ukp serve --print-task --host 0.0.0.0 --tls`
128
200
  (or `--tls-cert/--tls-key`) prints for you — the generator assembles the
129
201
  mechanical half (start script, hidden launcher, Task Scheduler command,
130
202
  firewall rule, token placeholder); this section keeps the judgment calls
131
- and the pitfalls the verification run caught:
203
+ and the pitfalls the verification run caught. A fast path exists for the
204
+ two file artifacts: `ukp serve --print-task --write ...` writes
205
+ `start-door.cmd` (token interpolated from your `UKP_SERVE_TOKEN` — never
206
+ displayed) and the hidden launcher straight into `%USERPROFILE%\.ukp\`,
207
+ CRLF by construction; existing files are never overwritten (manual edits
208
+ — a PATH patch, say — are safe; regeneration means removing the old files
209
+ first, as the removal section prescribes). The task-registration and
210
+ firewall steps stay on stdout for you to run; the print-only form remains
211
+ the full playbook (its Notes and the removal section):
132
212
 
133
213
  1. Install the usual way (bun's official installer + `npm i -g
134
214
  @catheadowl/ukp` — the npm shim needs bun on PATH, both land in
@@ -143,8 +223,11 @@ and the pitfalls the verification run caught:
143
223
  -subj "/CN=ukp" -addext "subjectAltName=IP:<lan-ip>,DNS:localhost"`
144
224
  4. `%USERPROFILE%\.ukp\start-door.cmd` (CRLF!) — set UKP_SERVE_TOKEN,
145
225
  run `ukp serve --host 0.0.0.0 --port 8570 --tls-cert ... --tls-key ...`
146
- with the door.log redirect. No PATH export: the launcher finds bun
147
- itself, and the logged-on task carries the per-user PATH. Write it
226
+ with **no redirect on the serve line** — door.log is owned solely by
227
+ the vbs launcher's outer redirect; a second redirect here deadlocks
228
+ the nested append-open and the door never starts. No PATH export: the
229
+ launcher finds bun itself, and the logged-on task carries the
230
+ per-user PATH. Write it
148
231
  with a real editor (writing cmd files over ssh echo mangles `%`
149
232
  escaping — copy the file instead).
150
233
  5. `%USERPROFILE%\.ukp\start-door-hidden.vbs` (CRLF, real editor) — the
@@ -152,11 +235,16 @@ and the pitfalls the verification run caught:
152
235
  at process creation (nothing ever flashes), captures everything to
153
236
  door.log, propagates the exit code, and retries a crashed door
154
237
  3 times, 30s apart. `ukp serve --print-task` prints it verbatim.
155
- 6. `schtasks /Create /TN ukp-door /TR "wscript.exe
156
- \"%USERPROFILE%\.ukp\start-door-hidden.vbs\"" /SC ONLOGON /F` (from an
157
- elevated shell — creating an ONLOGON task from a plain one fails with
158
- Access denied) + a firewall rule for the port; `schtasks /Run /TN
159
- ukp-door` to start now.
238
+ 6. Scheduled task — register from a **plain PowerShell shell** (no
239
+ elevation needed: a per-user logon trigger is the OneDrive/Teams-class
240
+ autostart path; a machine-level logon trigger or another user's
241
+ session still needs the elevated schtasks route). Use the
242
+ `Register-ScheduledTask` block `ukp serve --print-task` prints —
243
+ per-user `AtLogOn -User` trigger, battery flags, `IgnoreNew`, and
244
+ deliberately **no execution time limit**
245
+ (`-ExecutionTimeLimit ([TimeSpan]::Zero)`). Then the firewall rule
246
+ for the port (that one does need an elevated shell), and
247
+ `Start-ScheduledTask -TaskName ukp-door` to start now.
160
248
  7. Client: `ukp register --url https://<host>:8570 --token <token>` —
161
249
  TOFU pins the certificate; re-registering refreshes the pin.
162
250
 
@@ -168,16 +256,75 @@ Pitfalls the verification run caught:
168
256
  - ONLOGON starts at LOGON, not boot, and the launcher's crash retry is
169
257
  bounded (3×30s): after an unattended reboot nobody logs into, or after
170
258
  exhausted retries, the door is down until the next logon or a manual
171
- `schtasks /Run`. Unattended-boot always-on would need WinSW/NSSM-class
172
- wrapping — or prefer ssh:// there. (Task Scheduler's own
173
- restart-on-failure setting does not fire on exit codes — do not rely
174
- on it.)
175
- - To restart the door by hand (new token, ukp upgrade, new cert):
176
- `taskkill /PID <door-pid> /T /F`, then `schtasks /Run /TN ukp-door` —
177
- `schtasks /End` does not reliably kill the process tree.
259
+ `Start-ScheduledTask -TaskName ukp-door`. Unattended-boot always-on
260
+ would need WinSW/NSSM-class wrapping — or prefer ssh:// there. (Task
261
+ Scheduler's own restart-on-failure setting does not fire on exit
262
+ codes — do not rely on it.)
263
+ - **The 72-hour default**: a task without an explicit execution time
264
+ limit is stopped by Task Scheduler 72 hours after it starts — for a
265
+ resident door that is a silent kill (no window, no log line). The
266
+ recipe pins `-ExecutionTimeLimit ([TimeSpan]::Zero)` (PT0S in the task
267
+ XML); keep it when editing the task by hand.
268
+ - To restart the door by hand (new token, ukp upgrade, new cert): kill
269
+ the **launcher root**, not just the serve leaf — `taskkill /PID
270
+ <wscript-pid> /T /F` on the `wscript.exe` running
271
+ `start-door-hidden.vbs`, then `Start-ScheduledTask -TaskName
272
+ ukp-door`. A leaf-only kill leaves the launcher's bounded retry to
273
+ re-grab the port within 30 seconds, racing the manual start (which
274
+ `IgnoreNew` turns into a no-op while the old launcher lives); the
275
+ scheduler's End-Task does not reliably kill the tree either. Each
276
+ door has its own wscript — in PowerShell, `Get-CimInstance
277
+ Win32_Process -Filter "Name='wscript.exe'" | Select-Object
278
+ ProcessId,CommandLine` and match the vbs name.
279
+ - **Git Bash path conversion**: MSYS rewrites slash-style arguments into
280
+ Git paths (`taskkill /PID …` fails with a misleading
281
+ Invalid-argument error that reads like a permission problem). Prefix
282
+ such commands with `MSYS_NO_PATHCONV=1`.
283
+ - **Do not edit start-door.cmd while the door runs**: cmd.exe resumes
284
+ reading a running batch file at a stale byte offset, so an in-place
285
+ edit above the current line makes it execute garbled fragments when
286
+ the current command returns (observed live: a mid-comment `a` ran as
287
+ a command). Stop the door first, edit, then restart.
178
288
  - `rg` on the host is the host's business — no ripgrep installed means
179
289
  the remote `rg` capability reports unavailable (everything else works).
180
290
 
291
+ `ukp diagnose --door` is the read-only health check for this resident form:
292
+ for every door it reports the task state, the wscript->cmd->bun process tree,
293
+ the port (and who holds it), and the door.log tail — and exits 1 when a door
294
+ is down (a host with no doors is healthy). Each finding names its repair;
295
+ removal and restart moves live in the `ukp serve --print-task` §5 checklist
296
+ (the three-level teardown below) and the pitfalls above.
297
+
298
+ To remove the door — the inverse of steps 4–0, in three levels (stopping
299
+ is not disabling, and disabling is not erasing; `ukp serve --print-task`
300
+ prints this checklist as its §5):
301
+
302
+ 1. **Stop now**: `taskkill /PID <wscript-pid> /T /F` on the launcher
303
+ root (same lookup as the restart pitfall above). With the task still
304
+ registered, the next logon brings the door back.
305
+ 2. **Stop for good**:
306
+ `Unregister-ScheduledTask -TaskName ukp-door -Confirm:$false` from a
307
+ plain shell — the same zero-elevation path as registering it. The
308
+ cmdlet confirms by default (hence the flag), and unregistering does
309
+ **not** stop a running instance: the kill in step 1 is not optional.
310
+ 3. **Erase everything**: `netsh advfirewall firewall delete rule
311
+ name="ukp-door"` (the elevated shell again), then delete
312
+ `%USERPROFILE%\.ukp\start-door.cmd` (**it carries the token in
313
+ plaintext** — deleting it destroys this copy; if it ever left the
314
+ machine, treat the token as burned), `start-door-hidden.vbs`, and
315
+ `door.log`, plus the certificate material: your `--tls-cert` /
316
+ `--tls-key` paths, or — with `--tls` — the self-signed identity beside
317
+ the host registry: `%USERPROFILE%\.ukp\tls\<endpoint-name>\` for a
318
+ single-endpoint door, `%USERPROFILE%\.ukp\tls\` for a host door (it
319
+ never lives inside the knowledge folder, so git or cloud sync cannot
320
+ carry the private key out; `ukp unregister` leaves it in place on
321
+ purpose — a re-registered endpoint keeps its pin — so delete it by
322
+ hand when you are done). The endpoints stay registered
323
+ on the host for local use — `ukp unregister --endpoint <name>` per
324
+ endpoint only if you are done with them; and every consumer that
325
+ registered holds the token in plaintext in its own registry —
326
+ `ukp unregister --endpoint <name>` there too.
327
+
181
328
  ## Socket activation (Linux — no resident process on the https path either)
182
329
 
183
330
  systemd can hold the listening port itself and spawn the door on first
@@ -236,6 +383,15 @@ Notes:
236
383
  addresses — a public EIP is not among them, and registration would fail
237
384
  CERT_ALTNAME_INVALID. Add `--tls-san <public-ip>` next to `--tls`
238
385
  (see Native TLS above): the SAN grows, the key (and pin) does not.
386
+ - **Removing the door** (the setup block's inverse — the same three
387
+ levels as the Windows teardown above, in one gesture):
388
+ `systemctl --user disable --now ukp-door.socket` (a door still
389
+ running stops with `systemctl --user stop ukp-door.service`), then
390
+ delete the two unit files and the token env file, and
391
+ `rm -rf ~/.ukp/tls` if you are done with the self-signed identity;
392
+ `loginctl disable-linger "$USER"` too if nothing else on the host
393
+ needs it. Nothing else is resident — consumer and host registry
394
+ bindings come off exactly as on the Windows side.
239
395
 
240
396
  ## Host door (one host, many endpoints — one gesture, zero tokens)
241
397
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catheadowl/ukp",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3.14"
@@ -18,10 +18,12 @@
18
18
  "keywords": [
19
19
  "knowledge",
20
20
  "cli",
21
- "bun",
22
- "qmd",
23
21
  "search",
24
- "agent"
22
+ "agent",
23
+ "ai-agents",
24
+ "knowledge-base",
25
+ "local-first",
26
+ "self-hosted"
25
27
  ],
26
28
  "bin": {
27
29
  "ukp": "bin/ukp.js"
@@ -30,16 +32,19 @@
30
32
  "bin",
31
33
  "src",
32
34
  "docs",
35
+ "skills/ukp/SKILL.md",
33
36
  "README.md",
34
- "LICENSE",
35
- "package.json"
37
+ "SECURITY.md",
38
+ "LICENSE"
36
39
  ],
37
40
  "scripts": {
38
41
  "dev": "bun run src/cli.ts",
42
+ "prepack": "bun ./scripts/pack-audit.mjs",
39
43
  "test": "bun test",
40
44
  "typecheck": "tsc --noEmit"
41
45
  },
42
46
  "dependencies": {
47
+ "@any-nav/scan": "^0.2.0",
43
48
  "commander": "15.0.0",
44
49
  "smol-toml": "1.7.1",
45
50
  "zod": "4.4.3"