@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 +50 -40
- package/bin/ukp.js +31 -11
- package/docs/remote-deployment.md +309 -0
- package/package.json +2 -1
- package/src/capabilities/propose.ts +1 -1
- package/src/capabilities/read.ts +1 -1
- package/src/capabilities/remote-client.ts +163 -52
- package/src/capabilities/rg.ts +1 -1
- package/src/capabilities/search.ts +2 -2
- package/src/cli.ts +74 -5
- package/src/commands/diagnose.ts +1 -1
- package/src/commands/guide-content/propose.txt +1 -1
- package/src/commands/guide-content/remote.txt +6 -5
- package/src/commands/guide-content/service-qmd.txt +1 -1
- package/src/commands/init.ts +1 -1
- package/src/commands/inspect.ts +1 -1
- package/src/commands/inventory.ts +194 -36
- package/src/commands/propose.ts +1 -1
- package/src/commands/read.ts +7 -7
- package/src/commands/serve-task.ts +198 -0
- package/src/commands/serve.ts +67 -22
- package/src/registry.ts +2 -2
- package/src/server.ts +4 -4
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;
|
|
9
|
-
|
|
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
|
-
$
|
|
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
|
|
57
|
-
reference recovery re-finds it (git history
|
|
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 —
|
|
62
|
-
|
|
63
|
-
- Search
|
|
64
|
-
or
|
|
65
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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`,
|
|
118
|
-
|
|
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
|
-
(
|
|
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
|
|
132
|
-
remote endpoint is a one-command gesture
|
|
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
|
|
139
|
-
| `ukp read` | Reads an endpoint-scoped resource — exact path, `ukp://` URI, or `docid`
|
|
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
|
|
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
|
|
150
|
-
| `ukp list` | Lists registered endpoints with their declared capabilities.
|
|
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`,
|
|
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;
|
|
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
|
|
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
|
|
71
|
+
fail("looked at UKP_BUN, the standard install locations, and PATH");
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
@@ -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
|
-
+ `
|
|
445
|
+
+ ` - declare [capabilities.propose] in the Service Manifest (.ukp/service.toml); 'ukp guide propose' walks through it`,
|
|
446
446
|
},
|
|
447
447
|
};
|
|
448
448
|
}
|
package/src/capabilities/read.ts
CHANGED
|
@@ -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
|
|
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
|
);
|