@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 +93 -21
- package/SECURITY.md +69 -0
- package/bin/ukp.js +31 -7
- package/docs/remote-deployment.md +175 -19
- package/package.json +11 -6
- package/skills/ukp/SKILL.md +32 -0
- package/src/capabilities/door-health.ts +480 -0
- package/src/capabilities/nav.ts +209 -444
- package/src/capabilities/qmd.ts +19 -0
- package/src/capabilities/read.ts +18 -2
- package/src/capabilities/remote-client.ts +4 -2
- package/src/capabilities/search.ts +23 -7
- package/src/capabilities/session.ts +455 -0
- package/src/capabilities/tls-identity.ts +55 -3
- package/src/cli.ts +38 -64
- package/src/commands/diagnose.ts +80 -2
- package/src/commands/guide-content/client.txt +19 -6
- package/src/commands/guide-content/remote.txt +22 -1
- package/src/commands/guide-content/rg.txt +2 -2
- package/src/commands/init.ts +243 -1
- package/src/commands/inventory.ts +277 -17
- package/src/commands/nav.ts +17 -12
- package/src/commands/read.ts +1 -1
- package/src/commands/rg.ts +2 -2
- package/src/commands/search.ts +19 -3
- package/src/commands/serve-task.ts +334 -44
- package/src/commands/serve.ts +410 -44
- package/src/commands/session.ts +541 -0
- package/src/commands/use.ts +178 -0
- package/src/config/agent-backed.ts +136 -0
- package/src/config/manifest.ts +39 -3
- package/src/manifest.yml +15 -0
- package/src/path-safety.ts +9 -4
- package/src/plumbing/token-file.ts +368 -0
- package/src/registry.ts +78 -1
- package/src/server.ts +291 -75
- package/src/spawn-relay.ts +21 -3
|
@@ -2,18 +2,59 @@
|
|
|
2
2
|
* renders the process-manager artifacts for an https door on a Windows host
|
|
3
3
|
* — the start script, the hidden launcher (vbs), the Task Scheduler command,
|
|
4
4
|
* the firewall rule — as text the operator reviews and applies. Print-only
|
|
5
|
-
* by
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* by default: UKP installs nothing, starts nothing, and the printed script
|
|
6
|
+
* line carries a placeholder (the operator pastes the token into the file
|
|
7
|
+
* they save, under their own permission regime).
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* `--print-task --write` (O-020 ruling C, 2026-09-23 owner verdict: the
|
|
10
|
+
* D-090 secret-free clause scopes to the print surface) swaps the two file
|
|
11
|
+
* artifacts' sink from stdout to disk: the same bodies land as
|
|
12
|
+
* `%USERPROFILE%\.ukp\start-door.cmd` + `start-door-hidden.vbs`, CRLF, with
|
|
13
|
+
* the token interpolated from `UKP_SERVE_TOKEN` (never displayed, never in
|
|
14
|
+
* argv, refused outright if it carries characters that would corrupt a cmd
|
|
15
|
+
* file). Existing files are never overwritten - operator edits (PATH
|
|
16
|
+
* patches) live there; regeneration goes through the removal recipe. The
|
|
17
|
+
* remaining steps stay on stdout: register the task, open the firewall,
|
|
18
|
+
* verify with `ukp diagnose --door`. Still nothing is installed, started,
|
|
19
|
+
* or uninstalled by UKP.
|
|
20
|
+
*
|
|
21
|
+
* The emitted shapes mirror the field-verified recipes (deployment
|
|
22
|
+
* handbook, Windows section; provider round 2026-09-23): a bare `ukp`
|
|
23
|
+
* resolves through the logged-on task's per-user PATH and the
|
|
24
|
+
* self-locating launcher finds bun itself, so no PATH export is emitted.
|
|
25
|
+
* The hidden launcher is the product default (owner ruling 2026-09-21: a
|
|
26
|
+
* visibly popping cmd window is not a correct product form): zero flash
|
|
27
|
+
* (hidden at process creation), sole door.log capture (ISSUE-016: a
|
|
28
|
+
* redirect on the cmd's serve line too deadlocks the nested append-open),
|
|
15
29
|
* exit-code propagation, bounded crash retry — Task Scheduler's own
|
|
16
|
-
* restart-on-failure does not fire on exit codes (disproven on liku).
|
|
30
|
+
* restart-on-failure does not fire on exit codes (disproven on liku). The
|
|
31
|
+
* scheduled task registers unelevated via a per-user logon trigger with
|
|
32
|
+
* no execution time limit (the scheduler's 72h default silently kills
|
|
33
|
+
* resident doors).
|
|
34
|
+
*
|
|
35
|
+
* Riders 2026-09-23 (O-020 ruling A, copy-only): single-endpoint prints
|
|
36
|
+
* name the whole-registry host-door alternative (one door instead of one
|
|
37
|
+
* port per endpoint), the self-signed notes add the task-PATH openssl gap
|
|
38
|
+
* (the logged-on task may lack the shell's openssl source; Git for
|
|
39
|
+
* Windows' mingw64\bin is named as the common remedy, no hardcoded path),
|
|
40
|
+
* and every print carries the stop-before-editing hazard (a running cmd
|
|
41
|
+
* re-reads the script at a stale byte offset).
|
|
42
|
+
*
|
|
43
|
+
* §5 "To remove the door" (2026-09-23, O-020 ruling B1 / teardown FR):
|
|
44
|
+
* the print's own inverse, three levels — stop now (the same launcher-root
|
|
45
|
+
* kill as the restart note), stop for good (Unregister-ScheduledTask:
|
|
46
|
+
* confirms by default, and does NOT stop the running instance), erase
|
|
47
|
+
* everything (the reverse of steps 4→0, firewall rule included, the
|
|
48
|
+
* token-bearing cmd named, self-signed identity located by door shape —
|
|
49
|
+
* the single-endpoint identity sits INSIDE the served folder, so sync
|
|
50
|
+
* carries the private key out). The scheduler's own End/Stop task route
|
|
51
|
+
* is deliberately absent: it does not kill the tree (disproven in the
|
|
52
|
+
* field). Print-only as ever — the removal advice documents, never
|
|
53
|
+
* executes. */
|
|
54
|
+
|
|
55
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
56
|
+
import { join } from "node:path";
|
|
57
|
+
import { KitUsageError } from "./kit.ts";
|
|
17
58
|
|
|
18
59
|
export interface ServeTaskTlsSelfSigned {
|
|
19
60
|
mode: "self-signed";
|
|
@@ -31,6 +72,9 @@ export type ServeTaskTls = ServeTaskTlsSelfSigned | ServeTaskTlsCertificates;
|
|
|
31
72
|
export interface ServeTaskInput {
|
|
32
73
|
/** Present = single-endpoint mode; absent = host door mode. */
|
|
33
74
|
endpoint?: string;
|
|
75
|
+
/** Subset door (ADR-REM-010): the mirrored --select names; the §1 serve
|
|
76
|
+
* line carries them so the resident door serves the same subset. */
|
|
77
|
+
select?: readonly string[];
|
|
34
78
|
host: string;
|
|
35
79
|
port: number;
|
|
36
80
|
tls?: ServeTaskTls;
|
|
@@ -71,6 +115,7 @@ function serveCommandLine(input: ServeTaskInput): string {
|
|
|
71
115
|
return [
|
|
72
116
|
"ukp serve",
|
|
73
117
|
...(input.endpoint !== undefined ? [`--endpoint ${input.endpoint}`] : []),
|
|
118
|
+
...(input.select !== undefined ? [`--select ${input.select.join(",")}`] : []),
|
|
74
119
|
`--host ${input.host}`,
|
|
75
120
|
`--port ${input.port}`,
|
|
76
121
|
...tls,
|
|
@@ -83,8 +128,87 @@ function parentDirOf(path: string): string | undefined {
|
|
|
83
128
|
return cut > 0 ? path.slice(0, cut) : undefined;
|
|
84
129
|
}
|
|
85
130
|
|
|
131
|
+
/** The start script's file body, unindented — the single source both the
|
|
132
|
+
* print form (indented embed, `<paste-your-token>` slot) and `--write`
|
|
133
|
+
* (bytes on disk, real token) render. `written` switches the token rem
|
|
134
|
+
* lines to the written-file provenance so the file reads truthfully about
|
|
135
|
+
* where its secret came from. */
|
|
136
|
+
export function startDoorCmdBody(
|
|
137
|
+
input: ServeTaskInput,
|
|
138
|
+
tokenSlot: string,
|
|
139
|
+
written: boolean,
|
|
140
|
+
): string[] {
|
|
141
|
+
return [
|
|
142
|
+
"@echo off",
|
|
143
|
+
"rem UKP resident door (generated by ukp serve --print-task)",
|
|
144
|
+
...(written
|
|
145
|
+
? [
|
|
146
|
+
"rem The token line is the only secret: --write interpolated it",
|
|
147
|
+
"rem from UKP_SERVE_TOKEN (never displayed). Keep this file",
|
|
148
|
+
"rem private; deleting it destroys this copy.",
|
|
149
|
+
]
|
|
150
|
+
: [
|
|
151
|
+
"rem The token line is the only secret: mint a long random string",
|
|
152
|
+
"rem (e.g. openssl rand -hex 32), paste it in, keep this file private.",
|
|
153
|
+
]),
|
|
154
|
+
`set "UKP_SERVE_TOKEN=${tokenSlot}"`,
|
|
155
|
+
"rem ukp resolves through the logged-on task's per-user PATH; the",
|
|
156
|
+
"rem launcher finds bun itself - no PATH export needed.",
|
|
157
|
+
'if not exist "%USERPROFILE%\\.ukp" mkdir "%USERPROFILE%\\.ukp"',
|
|
158
|
+
"rem logging is owned by start-door-hidden.vbs (outer redirect) - a",
|
|
159
|
+
"rem second redirect to the same file here deadlocks the append open",
|
|
160
|
+
serveCommandLine(input),
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The hidden launcher's file body, unindented (print embeds it indented;
|
|
165
|
+
* --write writes these lines as-is). Carries no secret - the token lives
|
|
166
|
+
* only in start-door.cmd. */
|
|
167
|
+
export function startDoorVbsBody(): string[] {
|
|
168
|
+
return [
|
|
169
|
+
"' UKP door hidden launcher: runs start-door.cmd with NO visible",
|
|
170
|
+
"' window (hidden at process creation - nothing flashes), captures",
|
|
171
|
+
"' everything to door.log, waits out the door's lifetime, propagates",
|
|
172
|
+
"' its exit code, and retries a crashed door 3 times, 30s apart.",
|
|
173
|
+
'Dim shell : Set shell = CreateObject("WScript.Shell")',
|
|
174
|
+
"Dim q : q = Chr(34)",
|
|
175
|
+
'Dim home : home = shell.ExpandEnvironmentStrings("%USERPROFILE%")',
|
|
176
|
+
'Dim cmdline : cmdline = "cmd /c " & q & q & home & "\\.ukp\\start-door.cmd" & q & " >> " & q & home & "\\.ukp\\door.log" & q & " 2>&1" & q',
|
|
177
|
+
"Dim rc : rc = 0",
|
|
178
|
+
"Dim attempt",
|
|
179
|
+
"For attempt = 1 To 3",
|
|
180
|
+
" rc = shell.Run(cmdline, 0, True)",
|
|
181
|
+
" If rc = 0 Then Exit For",
|
|
182
|
+
" If attempt < 3 Then WScript.Sleep 30000",
|
|
183
|
+
"Next",
|
|
184
|
+
"WScript.Quit rc",
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Task-registration step (PowerShell, unelevated) - shared by the print
|
|
189
|
+
* form's step 3 and the --write report's next steps. */
|
|
190
|
+
function taskRegistrationLines(): string[] {
|
|
191
|
+
return [
|
|
192
|
+
'$t = New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\\$env:USERNAME"',
|
|
193
|
+
"$s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `",
|
|
194
|
+
"-DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew `",
|
|
195
|
+
"-ExecutionTimeLimit ([TimeSpan]::Zero)",
|
|
196
|
+
"Register-ScheduledTask -TaskName 'ukp-door' -Action (New-ScheduledTaskAction `",
|
|
197
|
+
"-Execute 'wscript.exe' -Argument '\"%USERPROFILE%\\.ukp\\start-door-hidden.vbs\"') `",
|
|
198
|
+
"-Trigger $t -Settings $s -Force",
|
|
199
|
+
"Start-ScheduledTask -TaskName ukp-door",
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function firewallRuleLine(port: number): string {
|
|
204
|
+
return `netsh advfirewall firewall add rule name="ukp-door" dir=in action=allow protocol=TCP localport=${port}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function indent3(lines: readonly string[]): string[] {
|
|
208
|
+
return lines.map((line) => ` ${line}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
86
211
|
export function renderServeTaskArtifacts(input: ServeTaskInput): string {
|
|
87
|
-
const serveLine = serveCommandLine(input);
|
|
88
212
|
const out: string[] = [
|
|
89
213
|
"ukp serve --print-task - Windows resident-door artifacts",
|
|
90
214
|
"",
|
|
@@ -119,49 +243,91 @@ export function renderServeTaskArtifacts(input: ServeTaskInput): string {
|
|
|
119
243
|
" (write it with a real editor and CRLF line endings; writing cmd",
|
|
120
244
|
" files over ssh echo mangles % escaping)",
|
|
121
245
|
"",
|
|
122
|
-
|
|
123
|
-
" rem UKP resident door (generated by ukp serve --print-task)",
|
|
124
|
-
" rem The token line is the only secret: mint a long random string",
|
|
125
|
-
" rem (e.g. openssl rand -hex 32), paste it in, keep this file private.",
|
|
126
|
-
" set \"UKP_SERVE_TOKEN=<paste-your-token>\"",
|
|
127
|
-
" rem ukp resolves through the logged-on task's per-user PATH; the",
|
|
128
|
-
" rem launcher finds bun itself - no PATH export needed.",
|
|
129
|
-
" if not exist \"%USERPROFILE%\\.ukp\" mkdir \"%USERPROFILE%\\.ukp\"",
|
|
130
|
-
` ${serveLine} >> "%USERPROFILE%\\.ukp\\door.log" 2>&1`,
|
|
246
|
+
...indent3(startDoorCmdBody(input, "<paste-your-token>", false)),
|
|
131
247
|
"",
|
|
132
248
|
"2) Hidden launcher - save as %USERPROFILE%\\.ukp\\start-door-hidden.vbs",
|
|
133
249
|
" (CRLF and a real editor again: the vbs carries literal % signs that",
|
|
134
250
|
" ssh echo would expand - copy the file instead)",
|
|
135
251
|
"",
|
|
136
|
-
|
|
137
|
-
" ' window (hidden at process creation - nothing flashes), captures",
|
|
138
|
-
" ' everything to door.log, waits out the door's lifetime, propagates",
|
|
139
|
-
" ' its exit code, and retries a crashed door 3 times, 30s apart.",
|
|
140
|
-
" Dim shell : Set shell = CreateObject(\"WScript.Shell\")",
|
|
141
|
-
" Dim q : q = Chr(34)",
|
|
142
|
-
" Dim home : home = shell.ExpandEnvironmentStrings(\"%USERPROFILE%\")",
|
|
143
|
-
" Dim cmdline : cmdline = \"cmd /c \" & q & q & home & \"\\.ukp\\start-door.cmd\" & q & \" >> \" & q & home & \"\\.ukp\\door.log\" & q & \" 2>&1\" & q",
|
|
144
|
-
" Dim rc : rc = 0",
|
|
145
|
-
" Dim attempt",
|
|
146
|
-
" For attempt = 1 To 3",
|
|
147
|
-
" rc = shell.Run(cmdline, 0, True)",
|
|
148
|
-
" If rc = 0 Then Exit For",
|
|
149
|
-
" If attempt < 3 Then WScript.Sleep 30000",
|
|
150
|
-
" Next",
|
|
151
|
-
" WScript.Quit rc",
|
|
252
|
+
...indent3(startDoorVbsBody()),
|
|
152
253
|
"",
|
|
153
254
|
"3) Scheduled task - starts the door at LOGON, hidden",
|
|
154
|
-
" (
|
|
155
|
-
"
|
|
255
|
+
" (run this block in PowerShell; no elevation needed: this per-user",
|
|
256
|
+
" logon trigger registers from a plain shell. A machine-level logon",
|
|
257
|
+
" trigger or another user's session still needs the elevated",
|
|
258
|
+
" schtasks route)",
|
|
156
259
|
"",
|
|
157
|
-
|
|
158
|
-
" schtasks /Run /TN ukp-door",
|
|
260
|
+
...indent3(taskRegistrationLines()),
|
|
159
261
|
"",
|
|
160
262
|
"4) Firewall rule for the port (from an elevated shell)",
|
|
161
263
|
"",
|
|
162
|
-
`
|
|
264
|
+
` ${firewallRuleLine(input.port)}`,
|
|
265
|
+
"",
|
|
266
|
+
"5) To remove the door - stopping it is not disabling it, and",
|
|
267
|
+
" disabling it is not erasing it. Three levels; go as far as you",
|
|
268
|
+
" mean to.",
|
|
269
|
+
"",
|
|
270
|
+
" Stop it NOW: kill the LAUNCHER root - the same command as the",
|
|
271
|
+
" restart note in the Notes below:",
|
|
272
|
+
" taskkill /PID <wscript-pid> /T /F",
|
|
273
|
+
" The door stays down until the next logon or a manual Start; with",
|
|
274
|
+
" the task still registered, the next logon brings it back.",
|
|
275
|
+
"",
|
|
276
|
+
" Stop it FOR GOOD: delete the scheduled task step 3 registered",
|
|
277
|
+
" (ukp-door in this printout - a renamed second door has its own",
|
|
278
|
+
" name), from a plain shell, the same zero-elevation path as",
|
|
279
|
+
" registering it:",
|
|
280
|
+
" Unregister-ScheduledTask -TaskName ukp-door -Confirm:$false",
|
|
281
|
+
" The cmdlet asks for confirmation by default; unregistering does",
|
|
282
|
+
" NOT kill a running door - the kill above is not optional.",
|
|
283
|
+
"",
|
|
284
|
+
" Erase EVERYTHING - the reverse of steps 4 -> 0 (the firewall",
|
|
285
|
+
" delete needs the elevated shell again):",
|
|
286
|
+
' netsh advfirewall firewall delete rule name="ukp-door"',
|
|
287
|
+
// Comma-separated paths: cmd's native delimiter AND PowerShell's
|
|
288
|
+
// array argument (a space-separated del line binds only the first
|
|
289
|
+
// path in PowerShell - caught live in the 2026-09-23 drill).
|
|
290
|
+
' del "%USERPROFILE%\\.ukp\\start-door.cmd", "%USERPROFILE%\\.ukp\\door.log"',
|
|
291
|
+
' del "%USERPROFILE%\\.ukp\\start-door-hidden.vbs"',
|
|
292
|
+
" start-door.cmd carries the token in plaintext: deleting it",
|
|
293
|
+
" destroys this copy, but if it ever left this machine (a backup,",
|
|
294
|
+
" a copy), treat the token as burned and mint a fresh one for any",
|
|
295
|
+
" future door.",
|
|
296
|
+
...(input.tls?.mode === "certificates"
|
|
297
|
+
? [
|
|
298
|
+
` del "${input.tls.keyPath}" "${input.tls.certPath}"`,
|
|
299
|
+
" (the certificate paths step 0 printed)",
|
|
300
|
+
]
|
|
301
|
+
: []),
|
|
302
|
+
...(input.tls?.mode === "self-signed" && input.endpoint !== undefined
|
|
303
|
+
? [
|
|
304
|
+
` The --tls identity is host-local: %USERPROFILE%\\.ukp\\tls\\`,
|
|
305
|
+
` ${input.endpoint}\\ - beside the host registry, never inside`,
|
|
306
|
+
" the knowledge folder (git or cloud sync cannot carry it out;",
|
|
307
|
+
" a pre-2026-11-01 .ukp\\tls\\ moved there on first serve).",
|
|
308
|
+
" Delete that folder with the endpoint if you are done.",
|
|
309
|
+
]
|
|
310
|
+
: []),
|
|
311
|
+
...(input.tls?.mode === "self-signed" && input.endpoint === undefined
|
|
312
|
+
? [
|
|
313
|
+
" The --tls identity is host-local: the tls\\ folder beside the",
|
|
314
|
+
" host registry - delete it with the door if you are done.",
|
|
315
|
+
]
|
|
316
|
+
: []),
|
|
317
|
+
" The endpoints stay registered on the host for local use;",
|
|
318
|
+
" unregister them too only if you are done with them, one per",
|
|
319
|
+
" endpoint: ukp unregister --endpoint <name>. Every consumer that",
|
|
320
|
+
" ran ukp register --url holds the token in plaintext in its own",
|
|
321
|
+
" registry - run ukp unregister --endpoint <name> there as well.",
|
|
163
322
|
"",
|
|
164
323
|
"Notes:",
|
|
324
|
+
...(input.endpoint !== undefined
|
|
325
|
+
? [
|
|
326
|
+
"- Single-endpoint door (one port, task, and token per endpoint):",
|
|
327
|
+
" omit --endpoint to serve the whole registry as one host door",
|
|
328
|
+
" (/e/<name>/ routing, all endpoints, one port).",
|
|
329
|
+
]
|
|
330
|
+
: []),
|
|
165
331
|
...(input.tls?.mode === "self-signed"
|
|
166
332
|
? [
|
|
167
333
|
"- --tls self-signs at first start using an openssl on PATH (Git",
|
|
@@ -169,6 +335,11 @@ export function renderServeTaskArtifacts(input: ServeTaskInput): string {
|
|
|
169
335
|
" anywhere, pre-generate the certificate once instead: re-run with",
|
|
170
336
|
" --tls-cert/--tls-key - the handbook's Windows section carries",
|
|
171
337
|
" that recipe.",
|
|
338
|
+
"- The task's PATH is not your shell's: the logged-on task",
|
|
339
|
+
" environment may lack the openssl source your interactive shell",
|
|
340
|
+
" sees (--tls then fails to self-sign or re-sign). The common",
|
|
341
|
+
" source is Git for Windows' mingw64\\bin - extend PATH inside",
|
|
342
|
+
" start-door.cmd: set \"PATH=%PATH%;<git>\\mingw64\\bin\".",
|
|
172
343
|
]
|
|
173
344
|
: []),
|
|
174
345
|
"- No visible window, by design: the hidden launcher starts the console",
|
|
@@ -180,15 +351,35 @@ export function renderServeTaskArtifacts(input: ServeTaskInput): string {
|
|
|
180
351
|
" failure visible (non-zero task result + door.log). Do not rely on",
|
|
181
352
|
" Task Scheduler's restart-on-failure setting - it does not fire on",
|
|
182
353
|
" exit codes.",
|
|
354
|
+
"- The task recipe pins no execution time limit on purpose: the Task",
|
|
355
|
+
" Scheduler default stops a task 72 hours after it starts, which",
|
|
356
|
+
" would silently kill a resident door (no window, no log line).",
|
|
183
357
|
"- To restart the door by hand (new token, ukp upgrade, new cert):",
|
|
184
|
-
"
|
|
185
|
-
"
|
|
358
|
+
" kill the LAUNCHER root, not just the serve leaf. The kill command:",
|
|
359
|
+
" taskkill /PID <wscript-pid> /T /F - where <wscript-pid> is the",
|
|
360
|
+
" wscript.exe running start-door-hidden.vbs; then",
|
|
361
|
+
" Start-ScheduledTask -TaskName ukp-door. A leaf-only kill lets",
|
|
362
|
+
" the launcher's bounded retry re-grab the port within 30s, racing",
|
|
363
|
+
" your Start (which IgnoreNew no-ops while the old launcher lives);",
|
|
364
|
+
" the scheduler's End-Task does not kill the tree either. Each door",
|
|
365
|
+
" has its own wscript - in PowerShell, list them and match the vbs",
|
|
366
|
+
" name: Get-CimInstance Win32_Process -Filter \"Name='wscript.exe'\"",
|
|
367
|
+
" | Select-Object ProcessId,CommandLine",
|
|
368
|
+
"- Git Bash users: prefix slash-style commands with MSYS_NO_PATHCONV=1",
|
|
369
|
+
" (e.g. MSYS_NO_PATHCONV=1 taskkill /PID <pid> /T /F), or MSYS rewrites",
|
|
370
|
+
" the slash arguments into Git paths and the command fails with a",
|
|
371
|
+
" misleading Invalid-argument error.",
|
|
186
372
|
"- LOGON, not boot: the door starts when someone logs on - nobody",
|
|
187
373
|
" logged on means no door. Unattended-boot always-on needs a service",
|
|
188
374
|
" wrapper (WinSW-class) - deliberately outside this recipe; prefer the",
|
|
189
375
|
" ssh:// path or a Linux host (systemd socket activation) for that.",
|
|
376
|
+
"- Stop the door before editing start-door.cmd: a running cmd",
|
|
377
|
+
" re-reads the script from a stale byte offset when the current",
|
|
378
|
+
" line's child exits, so editing lines above can make it execute",
|
|
379
|
+
" torn fragments of the edit.",
|
|
190
380
|
"- Re-run ukp serve --print-task with different flags to regenerate;",
|
|
191
|
-
" edits land by saving the files and
|
|
381
|
+
" edits land by saving the files and starting the task again",
|
|
382
|
+
" (Start-ScheduledTask -TaskName ukp-door).",
|
|
192
383
|
"- Full recipes and hardening: the deployment handbook -",
|
|
193
384
|
" docs/remote-deployment.md in this package, or",
|
|
194
385
|
" https://github.com/CatheadOwl/ukp/blob/main/docs/remote-deployment.md",
|
|
@@ -196,3 +387,102 @@ export function renderServeTaskArtifacts(input: ServeTaskInput): string {
|
|
|
196
387
|
);
|
|
197
388
|
return out.join("\n");
|
|
198
389
|
}
|
|
390
|
+
|
|
391
|
+
/** Characters that would corrupt the written cmd file: a quote breaks the
|
|
392
|
+
* `set "..."` quoting, a percent sign pairs into cmd's parse-time
|
|
393
|
+
* expansion, any control character can tear the file into lines. This is
|
|
394
|
+
* the third-live-observed hand-error class (printf `\b` mangling the PATH
|
|
395
|
+
* line, 2026-09-23) refused by construction - minted hex tokens (openssl
|
|
396
|
+
* rand -hex 32) always pass. Returns the problem description, not the
|
|
397
|
+
* token: the value is never echoed anywhere. */
|
|
398
|
+
export function unsafeTokenReason(token: string): string | undefined {
|
|
399
|
+
for (const ch of token) {
|
|
400
|
+
if (ch === '"') return "a double quote (it would break the script's set quoting)";
|
|
401
|
+
if (ch === "%") return "a percent sign (cmd expands % pairs at parse time)";
|
|
402
|
+
const code = ch.codePointAt(0)!;
|
|
403
|
+
if (code < 0x20 || code === 0x7f) return "a control character (it would tear the file into lines)";
|
|
404
|
+
}
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function crlf(lines: readonly string[]): string {
|
|
409
|
+
return `${lines.join("\r\n")}\r\n`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** `--print-task --write` (O-020 ruling C): the two file artifacts land on
|
|
413
|
+
* disk under `<home>\\.ukp\\` with the env token interpolated, CRLF line
|
|
414
|
+
* endings by construction. Existing files are never overwritten (operator
|
|
415
|
+
* edits like PATH patches live in the cmd); anything already there aborts
|
|
416
|
+
* the whole write before the first byte. Throws KitUsageError for
|
|
417
|
+
* invocation-level token problems, Error for the existing-file refusal.
|
|
418
|
+
* Returns the stdout report - composed only from token-free pieces, so the
|
|
419
|
+
* token exists in exactly one place: the cmd file's bytes. */
|
|
420
|
+
export function writeServeTaskArtifacts(
|
|
421
|
+
input: ServeTaskInput,
|
|
422
|
+
options: { token: string | undefined; homeDir: string },
|
|
423
|
+
): string {
|
|
424
|
+
if (options.token === undefined || options.token.length === 0) {
|
|
425
|
+
throw new KitUsageError(
|
|
426
|
+
"--write needs the door token in the environment: set UKP_SERVE_TOKEN (mint one, e.g. openssl rand -hex 32) - the generator never invents or displays it",
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const unsafe = unsafeTokenReason(options.token);
|
|
430
|
+
if (unsafe !== undefined) {
|
|
431
|
+
throw new KitUsageError(
|
|
432
|
+
`--write refuses this UKP_SERVE_TOKEN: it contains ${unsafe} - mint a token without such characters (e.g. openssl rand -hex 32); the value itself is never echoed`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const dir = join(options.homeDir, ".ukp");
|
|
436
|
+
const cmdPath = join(dir, "start-door.cmd");
|
|
437
|
+
const vbsPath = join(dir, "start-door-hidden.vbs");
|
|
438
|
+
const existing = [cmdPath, vbsPath].filter((path) => existsSync(path));
|
|
439
|
+
if (existing.length > 0) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
`refusing to overwrite ${existing.join(", ")}: --write never clobbers existing files (operator edits like PATH patches live there) - remove them first (the print-only form's section 5 carries the removal recipe) or keep hand-saving the printed form`,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
mkdirSync(dir, { recursive: true });
|
|
445
|
+
// The cmd carries the token: 0600 like every other secret-bearing file
|
|
446
|
+
// this CLI writes (the vbs is inert). CRLF, not the platform default -
|
|
447
|
+
// the same ending the print form begs the operator to preserve by hand.
|
|
448
|
+
writeFileSync(cmdPath, crlf(startDoorCmdBody(input, options.token, true)), { mode: 0o600 });
|
|
449
|
+
writeFileSync(vbsPath, crlf(startDoorVbsBody()), { mode: 0o644 });
|
|
450
|
+
const printInvocation = serveCommandLine(input).replace("ukp serve", "ukp serve --print-task");
|
|
451
|
+
return [
|
|
452
|
+
"ukp serve --print-task --write - door artifacts written",
|
|
453
|
+
"",
|
|
454
|
+
` wrote: ${cmdPath}`,
|
|
455
|
+
" (the token line was interpolated from UKP_SERVE_TOKEN;",
|
|
456
|
+
" it is never displayed - lose the file, lose the token)",
|
|
457
|
+
` wrote: ${vbsPath}`,
|
|
458
|
+
"",
|
|
459
|
+
"Nothing was registered, started, or opened - apply the rest yourself:",
|
|
460
|
+
"",
|
|
461
|
+
" register the task (PowerShell, no elevation needed):",
|
|
462
|
+
...indent2(taskRegistrationLines()),
|
|
463
|
+
"",
|
|
464
|
+
" firewall rule for the port (from an elevated shell):",
|
|
465
|
+
` ${firewallRuleLine(input.port)}`,
|
|
466
|
+
"",
|
|
467
|
+
...(input.tls?.mode === "certificates"
|
|
468
|
+
? [
|
|
469
|
+
" the certificate and key paths written into the script must",
|
|
470
|
+
" exist before the door starts (the print-only form's step 0",
|
|
471
|
+
" generates them once):",
|
|
472
|
+
` ${printInvocation}`,
|
|
473
|
+
"",
|
|
474
|
+
]
|
|
475
|
+
: []),
|
|
476
|
+
" then verify once applied: ukp diagnose --door",
|
|
477
|
+
"",
|
|
478
|
+
"Restart recipes, crash behavior, and the removal section live in the",
|
|
479
|
+
"print-only form and the deployment handbook:",
|
|
480
|
+
` ${printInvocation}`,
|
|
481
|
+
" docs/remote-deployment.md (in this package)",
|
|
482
|
+
"",
|
|
483
|
+
].join("\n");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function indent2(lines: readonly string[]): string[] {
|
|
487
|
+
return lines.map((line) => ` ${line}`);
|
|
488
|
+
}
|