@catheadowl/ukp 0.2.3 → 0.2.5
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 +5 -2
- package/bin/ukp.js +31 -11
- package/docs/remote-deployment.md +7 -0
- package/package.json +1 -1
- package/src/capabilities/propose.ts +1 -1
- package/src/capabilities/read.ts +1 -1
- package/src/capabilities/remote-client.ts +361 -70
- package/src/capabilities/rg.ts +148 -21
- package/src/capabilities/search.ts +2 -2
- package/src/cli.ts +73 -4
- package/src/commands/guide-content/propose.txt +1 -1
- package/src/commands/guide-content/remote.txt +9 -7
- package/src/commands/guide-content/rg.txt +35 -0
- package/src/commands/guide-content/service-qmd.txt +1 -1
- package/src/commands/guide.ts +11 -4
- package/src/commands/inventory.ts +182 -36
- package/src/commands/propose.ts +1 -1
- package/src/commands/read.ts +6 -6
- package/src/commands/rg.ts +45 -14
- package/src/commands/serve-task.ts +17 -17
- package/src/commands/serve.ts +22 -22
- package/src/registry.ts +2 -2
- package/src/server.ts +41 -26
- package/src/spawn-relay.ts +57 -0
|
@@ -48,15 +48,15 @@ export function printTaskPreflightError(input: {
|
|
|
48
48
|
allowAnonymous: boolean;
|
|
49
49
|
}): string | undefined {
|
|
50
50
|
if (input.platform !== "win32") {
|
|
51
|
-
return "--print-task is Windows-only; on Linux prefer systemd socket activation (ukp serve --systemd-socket)
|
|
51
|
+
return "--print-task is Windows-only; on Linux prefer systemd socket activation (ukp serve --systemd-socket) - the deployment handbook carries that recipe";
|
|
52
52
|
}
|
|
53
53
|
const loopback =
|
|
54
54
|
input.host === "127.0.0.1" || input.host === "localhost" || input.host === "::1" || input.host === "[::1]";
|
|
55
55
|
if (!loopback && !input.hasTls) {
|
|
56
|
-
return "refusing to print an off-loopback door without TLS: clients refuse to register plain-http urls
|
|
56
|
+
return "refusing to print an off-loopback door without TLS: clients refuse to register plain-http urls - pass --tls (self-signed, pinned at registration) or --tls-cert/--tls-key, or bind loopback behind a TLS-terminating proxy";
|
|
57
57
|
}
|
|
58
58
|
if (input.allowAnonymous && !loopback) {
|
|
59
|
-
return "refusing to print this shape: --allow-anonymous is loopback-only and serve would refuse it at task start
|
|
59
|
+
return "refusing to print this shape: --allow-anonymous is loopback-only and serve would refuse it at task start - set the token in the generated script instead";
|
|
60
60
|
}
|
|
61
61
|
return undefined;
|
|
62
62
|
}
|
|
@@ -86,10 +86,10 @@ function parentDirOf(path: string): string | undefined {
|
|
|
86
86
|
export function renderServeTaskArtifacts(input: ServeTaskInput): string {
|
|
87
87
|
const serveLine = serveCommandLine(input);
|
|
88
88
|
const out: string[] = [
|
|
89
|
-
"ukp serve --print-task
|
|
89
|
+
"ukp serve --print-task - Windows resident-door artifacts",
|
|
90
90
|
"",
|
|
91
91
|
"Print-only: review these, then apply them yourself. UKP installs nothing,",
|
|
92
|
-
"starts nothing, and supervises nothing
|
|
92
|
+
"starts nothing, and supervises nothing - the door's lifecycle belongs to",
|
|
93
93
|
"you and your process manager.",
|
|
94
94
|
"",
|
|
95
95
|
];
|
|
@@ -97,7 +97,7 @@ export function renderServeTaskArtifacts(input: ServeTaskInput): string {
|
|
|
97
97
|
if (input.tls?.mode === "certificates") {
|
|
98
98
|
const certDir = parentDirOf(input.tls.certPath);
|
|
99
99
|
out.push(
|
|
100
|
-
"0) Certificate
|
|
100
|
+
"0) Certificate - generate ONCE (Git for Windows' openssl works; this",
|
|
101
101
|
" avoids any runtime openssl dependency of --tls):",
|
|
102
102
|
"",
|
|
103
103
|
" openssl req -x509 -newkey rsa:2048 -nodes -days 825 -keyout",
|
|
@@ -115,7 +115,7 @@ export function renderServeTaskArtifacts(input: ServeTaskInput): string {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
out.push(
|
|
118
|
-
"1) Start script
|
|
118
|
+
"1) Start script - save as %USERPROFILE%\\.ukp\\start-door.cmd",
|
|
119
119
|
" (write it with a real editor and CRLF line endings; writing cmd",
|
|
120
120
|
" files over ssh echo mangles % escaping)",
|
|
121
121
|
"",
|
|
@@ -125,13 +125,13 @@ export function renderServeTaskArtifacts(input: ServeTaskInput): string {
|
|
|
125
125
|
" rem (e.g. openssl rand -hex 32), paste it in, keep this file private.",
|
|
126
126
|
" set \"UKP_SERVE_TOKEN=<paste-your-token>\"",
|
|
127
127
|
" rem ukp resolves through the logged-on task's per-user PATH; the",
|
|
128
|
-
" rem launcher finds bun itself
|
|
128
|
+
" rem launcher finds bun itself - no PATH export needed.",
|
|
129
129
|
" if not exist \"%USERPROFILE%\\.ukp\" mkdir \"%USERPROFILE%\\.ukp\"",
|
|
130
130
|
` ${serveLine} >> "%USERPROFILE%\\.ukp\\door.log" 2>&1`,
|
|
131
131
|
"",
|
|
132
|
-
"2) Hidden launcher
|
|
132
|
+
"2) Hidden launcher - save as %USERPROFILE%\\.ukp\\start-door-hidden.vbs",
|
|
133
133
|
" (CRLF and a real editor again: the vbs carries literal % signs that",
|
|
134
|
-
" ssh echo would expand
|
|
134
|
+
" ssh echo would expand - copy the file instead)",
|
|
135
135
|
"",
|
|
136
136
|
" ' UKP door hidden launcher: runs start-door.cmd with NO visible",
|
|
137
137
|
" ' window (hidden at process creation - nothing flashes), captures",
|
|
@@ -150,7 +150,7 @@ export function renderServeTaskArtifacts(input: ServeTaskInput): string {
|
|
|
150
150
|
" Next",
|
|
151
151
|
" WScript.Quit rc",
|
|
152
152
|
"",
|
|
153
|
-
"3) Scheduled task
|
|
153
|
+
"3) Scheduled task - starts the door at LOGON, hidden",
|
|
154
154
|
" (create it from an elevated shell: creating an ONLOGON task from a",
|
|
155
155
|
" plain shell fails with Access denied)",
|
|
156
156
|
"",
|
|
@@ -167,29 +167,29 @@ export function renderServeTaskArtifacts(input: ServeTaskInput): string {
|
|
|
167
167
|
"- --tls self-signs at first start using an openssl on PATH (Git",
|
|
168
168
|
" for Windows' openssl qualifies). On a host with no openssl",
|
|
169
169
|
" anywhere, pre-generate the certificate once instead: re-run with",
|
|
170
|
-
" --tls-cert/--tls-key
|
|
170
|
+
" --tls-cert/--tls-key - the handbook's Windows section carries",
|
|
171
171
|
" that recipe.",
|
|
172
172
|
]
|
|
173
173
|
: []),
|
|
174
174
|
"- No visible window, by design: the hidden launcher starts the console",
|
|
175
175
|
" hidden at process creation (nothing ever flashes on the desktop) and",
|
|
176
|
-
" captures everything to door.log
|
|
176
|
+
" captures everything to door.log - that redirect is the paper trail (a",
|
|
177
177
|
" silent EADDRINUSE exit leaves a line).",
|
|
178
178
|
"- Crash self-recovery is the launcher's bounded retry: a non-zero exit",
|
|
179
179
|
" is retried up to 3 times, 30s apart; exhausted retries leave the",
|
|
180
180
|
" failure visible (non-zero task result + door.log). Do not rely on",
|
|
181
|
-
" Task Scheduler's restart-on-failure setting
|
|
181
|
+
" Task Scheduler's restart-on-failure setting - it does not fire on",
|
|
182
182
|
" exit codes.",
|
|
183
183
|
"- To restart the door by hand (new token, ukp upgrade, new cert):",
|
|
184
184
|
" taskkill /PID <door-pid> /T /F, then schtasks /Run /TN ukp-door",
|
|
185
185
|
" (schtasks /End does not reliably kill the process tree).",
|
|
186
|
-
"- LOGON, not boot: the door starts when someone logs on
|
|
186
|
+
"- LOGON, not boot: the door starts when someone logs on - nobody",
|
|
187
187
|
" logged on means no door. Unattended-boot always-on needs a service",
|
|
188
|
-
" wrapper (WinSW-class)
|
|
188
|
+
" wrapper (WinSW-class) - deliberately outside this recipe; prefer the",
|
|
189
189
|
" ssh:// path or a Linux host (systemd socket activation) for that.",
|
|
190
190
|
"- Re-run ukp serve --print-task with different flags to regenerate;",
|
|
191
191
|
" edits land by saving the files and re-running schtasks /Run.",
|
|
192
|
-
"- Full recipes and hardening: the deployment handbook
|
|
192
|
+
"- Full recipes and hardening: the deployment handbook -",
|
|
193
193
|
" docs/remote-deployment.md in this package, or",
|
|
194
194
|
" https://github.com/CatheadOwl/ukp/blob/main/docs/remote-deployment.md",
|
|
195
195
|
"",
|
package/src/commands/serve.ts
CHANGED
|
@@ -34,23 +34,23 @@ function parseServeTokens(raw: string | undefined): string[] {
|
|
|
34
34
|
* over `/e/<name>/` routing. */
|
|
35
35
|
export const SERVE_SPEC: UkpCommandSpec = {
|
|
36
36
|
name: "serve",
|
|
37
|
-
summary: "serve one endpoint
|
|
37
|
+
summary: "serve one endpoint - or the whole registry as a host door, one HTTP server for every endpoint - for remote UKP clients",
|
|
38
38
|
group: "operations",
|
|
39
39
|
description:
|
|
40
|
-
"Expose a registered endpoint over HTTP using the ukp-remote wire, or
|
|
41
|
-
usage: "[--endpoint <name>] [--host <addr>] [--port <n>] [--tls [--tls-san <ip|dns>]
|
|
40
|
+
"Expose a registered endpoint over HTTP using the ukp-remote wire, or - without --endpoint - serve every local endpoint as one host door routed by name.",
|
|
41
|
+
usage: "[--endpoint <name>] [--host <addr>] [--port <n>] [--tls [--tls-san <ip|dns>]... | --tls-cert <pem> --tls-key <pem>] [--max-idle <seconds>] [--systemd-socket] [--print-task]",
|
|
42
42
|
options: [
|
|
43
43
|
{ flags: "--endpoint <name>", help: "the registered endpoint to expose; omit it to serve the whole registry as a host door (/e/<name>/ routing, all endpoints, one port)" },
|
|
44
44
|
{ flags: "--host <addr>", help: "listen address (default 127.0.0.1, loopback only without a token)" },
|
|
45
45
|
{ flags: "--port <n>", help: "listen port (default 8570)" },
|
|
46
46
|
{ flags: "--tls", help: "serve HTTPS with a self-signed identity (auto-generated under .ukp/tls/, SAN covers this host's addresses; clients pin it at registration)" },
|
|
47
47
|
{ flags: "--tls-san <ip|dns>", help: "extra SAN entry for the --tls self-signed identity (repeatable): a public/NAT IP or hostname no NIC of this host carries; a persisted certificate missing an entry is re-signed over the same key (pin unchanged, pinned clients re-anchor)", multi: true },
|
|
48
|
-
{ flags: "--tls-cert <pem>", help: "TLS certificate (chain) PEM path
|
|
48
|
+
{ flags: "--tls-cert <pem>", help: "TLS certificate (chain) PEM path - Let's Encrypt, mkcert, or a private CA; pair with --tls-key" },
|
|
49
49
|
{ flags: "--tls-key <pem>", help: "TLS private key PEM path; pair with --tls-cert" },
|
|
50
50
|
{ flags: "--allow-anonymous", help: "permit tokenless access on loopback (local testing, or an ssh-forwarded host door where SSH carries encryption and auth; reverse-proxy deployments still require UKP_SERVE_TOKEN)" },
|
|
51
|
-
{ flags: "--max-idle <seconds>", help: "exit after <seconds> without requests (self-reap; the orphan backstop for on-demand-woken doors
|
|
52
|
-
{ flags: "--systemd-socket", help: "serve on the systemd socket-activation listener (LISTEN_FDS fd 3) instead of binding a port
|
|
53
|
-
{ flags: "--print-task", help: "Windows: print this door's process-manager artifacts (start script, hidden launcher, Task Scheduler command, firewall rule) instead of serving
|
|
51
|
+
{ flags: "--max-idle <seconds>", help: "exit after <seconds> without requests (self-reap; the orphan backstop for on-demand-woken doors - fractional values accepted for tests)" },
|
|
52
|
+
{ flags: "--systemd-socket", help: "serve on the systemd socket-activation listener (LISTEN_FDS fd 3) instead of binding a port - the port belongs to your .socket unit; Linux-only" },
|
|
53
|
+
{ flags: "--print-task", help: "Windows: print this door's process-manager artifacts (start script, hidden launcher, Task Scheduler command, firewall rule) instead of serving - review and apply them yourself; nothing is installed or started for you, and the token is yours to paste (never printed)" },
|
|
54
54
|
],
|
|
55
55
|
helpSuffix: [
|
|
56
56
|
"",
|
|
@@ -58,9 +58,9 @@ export const SERVE_SPEC: UkpCommandSpec = {
|
|
|
58
58
|
` GET ${DISCOVERY_PATH} discovery document (manifest projection,`,
|
|
59
59
|
" protocol version, instance identity)",
|
|
60
60
|
" POST /v1/search {query, limit} -> ukp.search.v1 envelope",
|
|
61
|
-
" GET /v1/read?ref
|
|
61
|
+
" GET /v1/read?ref=...|uri=... endpoint-relative ref or ukp:// URI",
|
|
62
62
|
" GET /v1/nav?path=&depth= markdown route view (ukp.nav.v1)",
|
|
63
|
-
" GET /v1/rg?query
|
|
63
|
+
" GET /v1/rg?query=... lexical search (ukp.rg.v1, ukp_uri handoff)",
|
|
64
64
|
"",
|
|
65
65
|
"Wire (ukp-remote v1), host door (no --endpoint):",
|
|
66
66
|
` GET ${DISCOVERY_PATH} door document (scope:"host", endpoint roster)`,
|
|
@@ -75,26 +75,26 @@ export const SERVE_SPEC: UkpCommandSpec = {
|
|
|
75
75
|
" --allow-anonymous, loopback binds only. The discovery document stays",
|
|
76
76
|
" public. A reverse proxy on the same host forwards from the public side",
|
|
77
77
|
" to the loopback bind, so proxy deployments treat the token as",
|
|
78
|
-
" mandatory
|
|
78
|
+
" mandatory - serve cannot see past its own bind address.",
|
|
79
79
|
"",
|
|
80
80
|
" Loopback-without-token covers testing/dogfood AND the ssh door",
|
|
81
81
|
" deployment: bind the door on the remote host's loopback and let ssh",
|
|
82
82
|
" forwarding carry encryption and authentication (clients register",
|
|
83
|
-
" 'ukp register --url ssh://<host>'
|
|
83
|
+
" 'ukp register --url ssh://<host>' - zero tokens, docker",
|
|
84
84
|
" DOCKER_HOST=ssh:// posture). It is not the way to consume a",
|
|
85
|
-
" same-machine endpoint
|
|
85
|
+
" same-machine endpoint - register its local path instead.",
|
|
86
86
|
" TLS: pass --tls to serve HTTPS with a self-signed identity",
|
|
87
|
-
" (generated under .ukp/tls/
|
|
88
|
-
" mode
|
|
87
|
+
" (generated under .ukp/tls/ - under the registry directory in door",
|
|
88
|
+
" mode -, SAN covers this host's addresses; remote clients TOFU-pin it",
|
|
89
89
|
" at registration and refresh by re-registering), or --tls-cert/--tls-key",
|
|
90
|
-
" for your own certificate (Let's Encrypt
|
|
91
|
-
" 2026-01
|
|
90
|
+
" for your own certificate (Let's Encrypt - IP certs available since",
|
|
91
|
+
" 2026-01 -, mkcert, a private CA). Plain HTTP remains loopback-only by",
|
|
92
92
|
" admission; public exposure needs TLS or the ssh:// transport.",
|
|
93
93
|
"",
|
|
94
94
|
" A cloud NAT/EIP host's public IP is on no NIC (--tls alone cannot",
|
|
95
95
|
" cover it): add --tls-san <public-ip> next to --tls. Repeatable; DNS",
|
|
96
96
|
" names accepted. The persisted certificate re-signs over the same key",
|
|
97
|
-
" when a new entry appears
|
|
97
|
+
" when a new entry appears - the pin is unchanged and pinned clients",
|
|
98
98
|
" re-anchor transparently; dropping entries never re-signs.",
|
|
99
99
|
"",
|
|
100
100
|
].join("\n"),
|
|
@@ -136,7 +136,7 @@ function toParsedServe(parsed: KitParsed): ParsedServe {
|
|
|
136
136
|
}
|
|
137
137
|
const printTask = options.printTask === true;
|
|
138
138
|
if (printTask && systemdSocket) {
|
|
139
|
-
throw new KitUsageError("--print-task is the Windows resident form; --systemd-socket is the Linux one (systemd holds the port
|
|
139
|
+
throw new KitUsageError("--print-task is the Windows resident form; --systemd-socket is the Linux one (systemd holds the port - see the deployment handbook)");
|
|
140
140
|
}
|
|
141
141
|
let maxIdleSeconds: number | undefined;
|
|
142
142
|
if (options.maxIdle !== undefined) {
|
|
@@ -183,10 +183,10 @@ export function renderServeBanner(info: ServeInfo): string {
|
|
|
183
183
|
`serving host door (ukp-remote v1)`,
|
|
184
184
|
` listening: ${info.url}`,
|
|
185
185
|
` discovery: ${info.url.startsWith("http") ? `${info.url}${DISCOVERY_PATH}` : info.url} (host door)`,
|
|
186
|
-
` endpoints: ${info.door!.endpoints.length > 0 ? info.door!.endpoints.join(", ") : "(none
|
|
186
|
+
` endpoints: ${info.door!.endpoints.length > 0 ? info.door!.endpoints.join(", ") : "(none - register endpoints on this host)"}`,
|
|
187
187
|
` write: ${info.door!.write.length > 0 ? info.door!.write.join(", ") + " (propose via PUT /e/<name>/v1/propose/<id>)" : "(no endpoint declares propose)"}`,
|
|
188
188
|
` auth: ${info.authRequired ? "bearer token required" : "no token (loopback bind; ssh-forwarded clients authenticate by SSH key)"}`,
|
|
189
|
-
` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning
|
|
189
|
+
` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning - install ripgrep on this door's PATH)"}`,
|
|
190
190
|
...(info.maxIdleSeconds !== undefined
|
|
191
191
|
? [` idle: exits after ${info.maxIdleSeconds}s without requests (--max-idle)`]
|
|
192
192
|
: []),
|
|
@@ -201,7 +201,7 @@ export function renderServeBanner(info: ServeInfo): string {
|
|
|
201
201
|
` listening: ${info.url}`,
|
|
202
202
|
` discovery: ${info.url.startsWith("http") ? `${info.url}${DISCOVERY_PATH}` : info.url}`,
|
|
203
203
|
` auth: ${info.authRequired ? "bearer token required" : "no token (loopback only)"}`,
|
|
204
|
-
` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning
|
|
204
|
+
` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning - install ripgrep on this door's PATH)"}`,
|
|
205
205
|
...(info.maxIdleSeconds !== undefined
|
|
206
206
|
? [` idle: exits after ${info.maxIdleSeconds}s without requests (--max-idle)`]
|
|
207
207
|
: []),
|
|
@@ -295,7 +295,7 @@ export function executeServeCommand(
|
|
|
295
295
|
// premise of --allow-anonymous is unverifiable here (RQ-18 review P1).
|
|
296
296
|
if (systemdSocket && tokens.length === 0) {
|
|
297
297
|
throw new Error(
|
|
298
|
-
"refusing to serve on --systemd-socket without a token: the listener's bind address belongs to the socket unit and may be public
|
|
298
|
+
"refusing to serve on --systemd-socket without a token: the listener's bind address belongs to the socket unit and may be public - tokenless loopback serving cannot be verified here; set UKP_SERVE_TOKEN",
|
|
299
299
|
);
|
|
300
300
|
}
|
|
301
301
|
const decision = serveAuthDecision(host, tokens, allowAnonymous);
|
package/src/registry.ts
CHANGED
|
@@ -230,7 +230,7 @@ export function assertRegistrableRemoteUrl(raw: string): void {
|
|
|
230
230
|
+ `${parts.endpointName !== undefined ? `/${parts.endpointName}` : ""}`;
|
|
231
231
|
throw new RegistryError(
|
|
232
232
|
`ssh:// urls take no port: the woken door listens on a client-chosen loopback port and the url port selects nothing`
|
|
233
|
-
+ `
|
|
233
|
+
+ ` - register '${portless}' instead`
|
|
234
234
|
+ ` (for a non-standard sshd port, use an ssh config Host alias)`,
|
|
235
235
|
);
|
|
236
236
|
}
|
|
@@ -240,7 +240,7 @@ export class RegistryBusyError extends RegistryError {
|
|
|
240
240
|
constructor(path: string) {
|
|
241
241
|
super(
|
|
242
242
|
`registry_busy: ${path} (another ukp process holds the registry lock; ` +
|
|
243
|
-
`stale locks are reclaimed automatically
|
|
243
|
+
`stale locks are reclaimed automatically - close other ukp commands or retry shortly)`,
|
|
244
244
|
);
|
|
245
245
|
this.name = "RegistryBusyError";
|
|
246
246
|
}
|
package/src/server.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
runRg,
|
|
34
34
|
projectRgEnvelope,
|
|
35
35
|
RG_DEFAULT_LIMIT,
|
|
36
|
+
RG_FILES_DEFAULT_LIMIT,
|
|
36
37
|
RG_MAX_LIMIT,
|
|
37
38
|
validateRgPassthrough,
|
|
38
39
|
RgUsageError,
|
|
@@ -407,7 +408,7 @@ function parseReadParams(
|
|
|
407
408
|
pin: string | null,
|
|
408
409
|
): { request: ReadRequest } | { error: string; class?: string } {
|
|
409
410
|
if ((ref === null) === (uri === null)) {
|
|
410
|
-
return { error: "exactly one of 'ref' (endpoint-relative) or 'uri' (ukp
|
|
411
|
+
return { error: "exactly one of 'ref' (endpoint-relative) or 'uri' (ukp://...) is required" };
|
|
411
412
|
}
|
|
412
413
|
const args = uri !== null
|
|
413
414
|
? [
|
|
@@ -463,18 +464,44 @@ function parseNavParams(
|
|
|
463
464
|
}
|
|
464
465
|
|
|
465
466
|
/** Wire rg params → ParsedRg with serve-fixed scope. Params are flat (no
|
|
466
|
-
* argv reconstruction needed): query/limit/glob/type/i/count map onto
|
|
467
|
+
* argv reconstruction needed): query/limit/glob/type/i/count/files map onto
|
|
467
468
|
* ParsedRg directly and passthrough re-validates against the same
|
|
468
|
-
* allowlist (ADR-RG-002) before any endpoint work starts.
|
|
469
|
+
* allowlist (ADR-RG-002) before any endpoint work starts. Files mode
|
|
470
|
+
* (ADR-RG-005) mirrors the CLI parse layer's mutual exclusions so the wire
|
|
471
|
+
* keeps the same strictness as query/limit. */
|
|
469
472
|
function parseRgParams(
|
|
470
473
|
endpointName: string,
|
|
471
474
|
url: URL,
|
|
472
475
|
): { parsed: ParsedRg } | { error: string } {
|
|
476
|
+
// Boolean params are strictly "1"/"0" — same strictness as query/limit, so
|
|
477
|
+
// a hand-crafted `i=true` is a visible usage error, not a silent mode miss.
|
|
478
|
+
const booleanOf = (name: string): boolean | { error: string } => {
|
|
479
|
+
const raw = url.searchParams.get(name);
|
|
480
|
+
if (raw === null) return false;
|
|
481
|
+
if (raw === "1") return true;
|
|
482
|
+
if (raw === "0") return false;
|
|
483
|
+
return { error: `'${name}' must be 1 or 0` };
|
|
484
|
+
};
|
|
485
|
+
const files = booleanOf("files");
|
|
486
|
+
if (typeof files === "object") return files;
|
|
487
|
+
const ignoreCase = booleanOf("i");
|
|
488
|
+
if (typeof ignoreCase === "object") return ignoreCase;
|
|
489
|
+
const count = booleanOf("count");
|
|
490
|
+
if (typeof count === "object") return count;
|
|
473
491
|
const query = url.searchParams.get("query");
|
|
474
|
-
if (
|
|
475
|
-
return { error: "'
|
|
492
|
+
if (files && count) {
|
|
493
|
+
return { error: "'files' and 'count' cannot be used together (pick one output mode)" };
|
|
494
|
+
}
|
|
495
|
+
if (files && ignoreCase) {
|
|
496
|
+
return { error: "'files' cannot be combined with 'i': pass an --iglob passthrough arg for a case-insensitive glob filter" };
|
|
497
|
+
}
|
|
498
|
+
if (files && query !== null) {
|
|
499
|
+
return { error: "'files' takes no 'query' - use 'glob' to filter by name" };
|
|
476
500
|
}
|
|
477
|
-
|
|
501
|
+
if (!files && (query === null || query.length === 0)) {
|
|
502
|
+
return { error: "'query' is required and must be a non-empty pattern (or pass files=1 for enumeration mode)" };
|
|
503
|
+
}
|
|
504
|
+
let limit = files ? RG_FILES_DEFAULT_LIMIT : RG_DEFAULT_LIMIT;
|
|
478
505
|
const rawLimit = url.searchParams.get("limit");
|
|
479
506
|
if (rawLimit !== null) {
|
|
480
507
|
if (!/^[0-9]+$/.test(rawLimit)) return { error: "'limit' must be a decimal integer" };
|
|
@@ -490,31 +517,19 @@ function parseRgParams(
|
|
|
490
517
|
if (error instanceof RgUsageError) return { error: error.message };
|
|
491
518
|
throw error;
|
|
492
519
|
}
|
|
493
|
-
|
|
494
|
-
// a hand-crafted `i=true` is a visible usage error, not a silent mode miss.
|
|
495
|
-
const booleanOf = (name: string): boolean | { error: string } => {
|
|
496
|
-
const raw = url.searchParams.get(name);
|
|
497
|
-
if (raw === null) return false;
|
|
498
|
-
if (raw === "1") return true;
|
|
499
|
-
if (raw === "0") return false;
|
|
500
|
-
return { error: `'${name}' must be 1 or 0` };
|
|
501
|
-
};
|
|
502
|
-
const ignoreCase = booleanOf("i");
|
|
503
|
-
if (typeof ignoreCase === "object") return ignoreCase;
|
|
504
|
-
const count = booleanOf("count");
|
|
505
|
-
if (typeof count === "object") return count;
|
|
506
|
-
const glob = url.searchParams.get("glob");
|
|
520
|
+
const globs = url.searchParams.getAll("glob");
|
|
507
521
|
const type = url.searchParams.get("type");
|
|
508
522
|
return {
|
|
509
523
|
parsed: {
|
|
510
|
-
request: { query
|
|
524
|
+
request: { query: files ? "" : query!, limit },
|
|
511
525
|
options: {
|
|
512
526
|
explicitEndpoints: [endpointName],
|
|
513
527
|
global: false,
|
|
514
|
-
...(
|
|
528
|
+
...(globs.length > 0 ? { globs } : {}),
|
|
515
529
|
...(type !== null ? { type } : {}),
|
|
516
530
|
...(ignoreCase ? { ignoreCase: true } : {}),
|
|
517
531
|
...(count ? { count: true } : {}),
|
|
532
|
+
...(files ? { files: true } : {}),
|
|
518
533
|
passthrough,
|
|
519
534
|
},
|
|
520
535
|
warnings: [],
|
|
@@ -546,14 +561,14 @@ export function parseListenFds(env: NodeJS.ProcessEnv): { fd: number } | { error
|
|
|
546
561
|
};
|
|
547
562
|
}
|
|
548
563
|
if (Number(env.LISTEN_PID) !== process.pid) {
|
|
549
|
-
return { error: `LISTEN_PID ${env.LISTEN_PID} does not match this process (${process.pid})
|
|
564
|
+
return { error: `LISTEN_PID ${env.LISTEN_PID} does not match this process (${process.pid}) - inherited activation variables belong to another process` };
|
|
550
565
|
}
|
|
551
566
|
const count = Number(env.LISTEN_FDS);
|
|
552
567
|
if (!Number.isSafeInteger(count) || count < 1) {
|
|
553
568
|
return { error: `LISTEN_FDS=${String(env.LISTEN_FDS)} is not a positive integer` };
|
|
554
569
|
}
|
|
555
570
|
if (count !== 1) {
|
|
556
|
-
return { error: `expected exactly one listening fd (LISTEN_FDS=1), got ${count}
|
|
571
|
+
return { error: `expected exactly one listening fd (LISTEN_FDS=1), got ${count} - run one socket unit per serve instance` };
|
|
557
572
|
}
|
|
558
573
|
delete env.LISTEN_FDS;
|
|
559
574
|
delete env.LISTEN_PID;
|
|
@@ -645,7 +660,7 @@ function startServeListener(
|
|
|
645
660
|
isSocket = false;
|
|
646
661
|
}
|
|
647
662
|
if (!isSocket) {
|
|
648
|
-
throw new ServeSetupError(`fd ${parsed.fd} is not an open socket
|
|
663
|
+
throw new ServeSetupError(`fd ${parsed.fd} is not an open socket - socket activation must pass a bound listening socket`);
|
|
649
664
|
}
|
|
650
665
|
const server = Bun.serve({ hostname: "127.0.0.1", port: 0, ...tls, fetch });
|
|
651
666
|
const acceptor = netCreateServer();
|
|
@@ -1170,7 +1185,7 @@ function handleRg(url: URL, request: Request, config: ServeConfig, endpointName:
|
|
|
1170
1185
|
currentDirectory: config.currentDirectory,
|
|
1171
1186
|
registryPath: config.registryPath,
|
|
1172
1187
|
});
|
|
1173
|
-
return jsonResponse(projectRgEnvelope(result
|
|
1188
|
+
return jsonResponse(projectRgEnvelope(result));
|
|
1174
1189
|
}
|
|
1175
1190
|
|
|
1176
1191
|
/** PUT /v1/propose/{id} (W8 / ADR-REM-005): the write face. Body = the
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Win32 tool-spawn relay (2026-09-22 finding, ukp_remote TODO
|
|
3
|
+
* 20260922-win32-ssh-spawn-tax): Windows Defender holds the DIRECT
|
|
4
|
+
* bun-to-<tool>.exe child edge for seconds per spawn — measured 9.7-10.7s for
|
|
5
|
+
* ssh.exe and ~5.4s for rg.exe across stdio modes, windowsHide, and absolute
|
|
6
|
+
* paths, while the same binaries start in 0.16-0.26s from bash and in
|
|
7
|
+
* 0.55-0.8s from bun through ANY intermediary parent (cmd, node, powershell).
|
|
8
|
+
* The inspection keys on the parent-child edge, so routing the spawn through
|
|
9
|
+
* a one-level intermediary escapes it.
|
|
10
|
+
*
|
|
11
|
+
* The intermediary is powershell.exe driven by -EncodedCommand: the script
|
|
12
|
+
* rides as one base64 token (no quoting stress from the spawning side), and
|
|
13
|
+
* the script splats a single-quoted array so argv reaches the tool
|
|
14
|
+
* byte-identical — the only escape in a PS single-quoted literal is doubling
|
|
15
|
+
* inner single quotes; double quotes, backslashes, and cmd metacharacters
|
|
16
|
+
* are all literal (verified E2E: the pinned wake command's load-bearing
|
|
17
|
+
* quotes survive, door READY ~1.4s). powershell.exe ships with every
|
|
18
|
+
* Windows; no extra prerequisite is imposed.
|
|
19
|
+
*
|
|
20
|
+
* Exit-code contract: the script exits with the tool's own exit status. With
|
|
21
|
+
* the not-found guard requested, a tool that does not resolve on PATH exits
|
|
22
|
+
* TOOL_NOT_FOUND_EXIT before any invocation — distinct from every real tool
|
|
23
|
+
* exit, so callers can keep treating "tool missing" as a degradation instead
|
|
24
|
+
* of a fault.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Distinct not-found sentinel: no real tool invocation ran when this is the
|
|
28
|
+
* exit status (ripgrep itself only ever exits 0/1/2; ssh diagnostics arrive
|
|
29
|
+
* via stderr instead). */
|
|
30
|
+
export const TOOL_NOT_FOUND_EXIT = 127;
|
|
31
|
+
|
|
32
|
+
/** PS single-quote literal: the only escape is doubling inner single quotes. */
|
|
33
|
+
function psSingleQuoted(arg: string): string {
|
|
34
|
+
return `'${arg.replaceAll("'", "''")}'`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The relay script. `notFoundGuard` adds a Get-Command pre-check that exits
|
|
38
|
+
* TOOL_NOT_FOUND_EXIT when argv[0] does not resolve — for callers that must
|
|
39
|
+
* distinguish a missing tool from a tool's own failure exit. */
|
|
40
|
+
export function toolRelayScript(argv: readonly string[], options: { notFoundGuard?: boolean } = {}): string {
|
|
41
|
+
const guard = options.notFoundGuard === true
|
|
42
|
+
? " if (-not (Get-Command -LiteralName $a[0] -ErrorAction SilentlyContinue)) { exit 127 };"
|
|
43
|
+
: "";
|
|
44
|
+
return `$a = @(${argv.map(psSingleQuoted).join(",")});${guard} & $a[0] $a[1..($a.Count-1)]; exit $LASTEXITCODE`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The powershell carrier argv for a tool invocation: spawn THIS instead of
|
|
48
|
+
* the tool when the direct bun-to-tool edge is taxed (win32 real-tool
|
|
49
|
+
* spawns). Test-injected commands must not be wrapped. */
|
|
50
|
+
export function toolRelayArgv(argv: readonly string[], options: { notFoundGuard?: boolean } = {}): string[] {
|
|
51
|
+
return [
|
|
52
|
+
"powershell",
|
|
53
|
+
"-NoProfile",
|
|
54
|
+
"-EncodedCommand",
|
|
55
|
+
Buffer.from(toolRelayScript(argv, options), "utf16le").toString("base64"),
|
|
56
|
+
];
|
|
57
|
+
}
|