@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
package/src/commands/init.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, openSync, closeSync, writeFileSync, unlinkSync, realpathSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, openSync, closeSync, writeFileSync, unlinkSync, readFileSync, readdirSync, renameSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
3
|
import { stringify } from "smol-toml";
|
|
4
4
|
import { ENDPOINT_NAME, loadManifest, type ManifestDependency } from "../config/manifest.ts";
|
|
@@ -139,6 +139,242 @@ function renderServiceManifest(options: { name?: string; description?: string; d
|
|
|
139
139
|
return encoded.endsWith("\n") ? encoded : `${encoded}\n`;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/** Precise self-ignore template for `.ukp/` (2026-10-28, inbox
|
|
143
|
+
* client-portability-and-skill-coverage adoption; `/tls/` line retired
|
|
144
|
+
* 2026-11-01 by the inbox service-dir-layering adjudication item 2): the
|
|
145
|
+
* tool-owned directory holds exactly two declaration files
|
|
146
|
+
* (`service.toml`, `client.toml` — pure declarations with zero machine
|
|
147
|
+
* paths, versionable by design: a fresh clone re-registers the Service
|
|
148
|
+
* with one `ukp register`) plus the machine-local state named below.
|
|
149
|
+
* Ignoring by name keeps identity out of version control and folder sync
|
|
150
|
+
* (the O-020 B-b goal) while declarations ride with the repo — the `*`
|
|
151
|
+
* blanket it replaces swallowed them (workflow-lab migration evidence:
|
|
152
|
+
* the endpoint declaration did not travel with the clone, and carving
|
|
153
|
+
* client.toml back into tracking cost three fiddly steps). The serve TLS
|
|
154
|
+
* identity no longer lives here (see UKP_SELF_IGNORE_WITH_TLS). */
|
|
155
|
+
export const UKP_SELF_IGNORE = [
|
|
156
|
+
"# UKP machine-local state (instance identity) only;",
|
|
157
|
+
"# declarations (service.toml, client.toml) are versionable by design.",
|
|
158
|
+
"/instance-uid",
|
|
159
|
+
"",
|
|
160
|
+
].join("\n");
|
|
161
|
+
|
|
162
|
+
/** The 2026-10-28 template's first form, retired 2026-11-01 when the
|
|
163
|
+
* serve TLS identity moved to the machine home (`~/.ukp/tls/<name>/`,
|
|
164
|
+
* inbox service-dir-layering item 2): its `/tls/` rule named a resident
|
|
165
|
+
* that no longer exists. Kept verbatim for two recognitions — the posture
|
|
166
|
+
* ladder classifies it "current" (it IS precise, and a folder that still
|
|
167
|
+
* carries its legacy `tls/` dir stays correctly ignored until the
|
|
168
|
+
* serve-side migration moves the material out), and the serve migration
|
|
169
|
+
* (`retireTlsIgnoreLine`) swaps it onto the current template in the same
|
|
170
|
+
* gesture that removes the directory. */
|
|
171
|
+
export const UKP_SELF_IGNORE_WITH_TLS = [
|
|
172
|
+
"# UKP machine-local state (instance identity, TLS key material) only;",
|
|
173
|
+
"# declarations (service.toml, client.toml) are versionable by design.",
|
|
174
|
+
"/instance-uid",
|
|
175
|
+
"/tls/",
|
|
176
|
+
"",
|
|
177
|
+
].join("\n");
|
|
178
|
+
|
|
179
|
+
/** The superseded 2026-09-23 `*` template (the original O-020 B-b
|
|
180
|
+
* landing), kept verbatim for recognition: the posture step below migrates
|
|
181
|
+
* a `.ukp/.gitignore` that still carries it onto the precise template;
|
|
182
|
+
* anything else is operator-owned and stays untouched (the B-b
|
|
183
|
+
* operator-policy-wins posture). */
|
|
184
|
+
export const LEGACY_UKP_SELF_IGNORE = [
|
|
185
|
+
"# UKP local Service state (manifest, identity, TLS keys) is machine-local:",
|
|
186
|
+
"# self-ignored to keep it out of version control and folder sync.",
|
|
187
|
+
"*",
|
|
188
|
+
"",
|
|
189
|
+
].join("\n");
|
|
190
|
+
|
|
191
|
+
/** Closed resident set of `.ukp/` (INV-21, owner ruling 2026-11-02): the
|
|
192
|
+
* declarations, the instance identity, the ignore guard — plus the two
|
|
193
|
+
* atomic-write transients a crash can leave behind (documented as
|
|
194
|
+
* "overwritten by the next write"; tolerating them is not an ignore rule,
|
|
195
|
+
* it is the tool owning its own residue). Anything else is a violation
|
|
196
|
+
* the posture step refuses on. The serve TLS identity left this directory
|
|
197
|
+
* on 2026-11-01 (ADR-REM-012), which is exactly why the set is closed:
|
|
198
|
+
* the template no longer carries defensive rules for residents that
|
|
199
|
+
* must not exist. */
|
|
200
|
+
const UKP_RESIDENTS: ReadonlySet<string> = new Set([
|
|
201
|
+
"service.toml",
|
|
202
|
+
"client.toml",
|
|
203
|
+
"instance-uid",
|
|
204
|
+
".gitignore",
|
|
205
|
+
".gitignore.ukp-tmp",
|
|
206
|
+
"client.toml.ukp-tmp",
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
/** Refusal of a posture step over a `.ukp/` holding entries outside the
|
|
210
|
+
* closed set. Carries the offending paths so every carrier gesture can
|
|
211
|
+
* print the same operator-facing body. */
|
|
212
|
+
export class UkpResidentError extends Error {
|
|
213
|
+
constructor(
|
|
214
|
+
readonly ukpDirectory: string,
|
|
215
|
+
readonly residents: readonly string[],
|
|
216
|
+
endpointName?: string,
|
|
217
|
+
) {
|
|
218
|
+
const home = `~/.ukp/tls/${endpointName ?? "<endpoint-name>"}/`;
|
|
219
|
+
const hasTls = residents.includes("tls");
|
|
220
|
+
const others = residents.filter((name) => name !== "tls");
|
|
221
|
+
const lines = [
|
|
222
|
+
".ukp/ holds entries UKP does not own (INV-21: service.toml, client.toml, instance-uid and .gitignore only):",
|
|
223
|
+
...residents.map((name) => ` ${join(ukpDirectory, name)}`),
|
|
224
|
+
...(hasTls
|
|
225
|
+
? [`tls/ is a pre-2026-11-01 serve TLS identity: move its cert.pem and key.pem to ${home} to keep the pin clients hold, or delete it.`]
|
|
226
|
+
: []),
|
|
227
|
+
...(others.length > 0 ? [`Delete ${hasTls ? "the rest" : "them"}.`] : []),
|
|
228
|
+
"The version-control step is refused until .ukp/ is clean - rewriting the ignore guard over material it no longer covers is how a private key becomes committable.",
|
|
229
|
+
];
|
|
230
|
+
super(lines.join("\n"));
|
|
231
|
+
this.name = "UkpResidentError";
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** INV-21 admission gate of every posture step (init / use / register):
|
|
236
|
+
* enumerate `.ukp/` and refuse on any entry outside the closed resident
|
|
237
|
+
* set. Runs BEFORE any write of the carrying gesture — the template
|
|
238
|
+
* rewrite most of all. The 2026-11-02 KB incident was a `*` guard swapped
|
|
239
|
+
* onto the current template while a legacy `tls/` still sat beneath it:
|
|
240
|
+
* key.pem became addable. Disposition is the owner's (delete, or move the
|
|
241
|
+
* identity to its machine home); nothing migrates here (key material
|
|
242
|
+
* relocation is serve's lifecycle), nothing is ignored, no tombstone rule
|
|
243
|
+
* is written. A missing `.ukp/` passes (nothing to inspect). */
|
|
244
|
+
export function assertUkpResidents(ukpDirectory: string, options: { endpointName?: string } = {}): void {
|
|
245
|
+
let entries: string[];
|
|
246
|
+
try {
|
|
247
|
+
entries = readdirSync(ukpDirectory);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (error instanceof Error && (error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
const strangers = entries.filter((name) => !UKP_RESIDENTS.has(name)).sort();
|
|
253
|
+
if (strangers.length === 0) return;
|
|
254
|
+
throw new UkpResidentError(ukpDirectory, strangers, options.endpointName);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Posture of `.ukp/.gitignore` after a version-control step ran. Two
|
|
258
|
+
* gestures carry the declarations' version visibility and share this
|
|
259
|
+
* ladder: `ukp use` (client face) and `ukp register`'s local path (service
|
|
260
|
+
* face — the service-side touchpoint pure Service folders were missing
|
|
261
|
+
* until 2026-11-01; they have no reason to run `use`, and re-running
|
|
262
|
+
* `init service` is refused once the manifest exists). */
|
|
263
|
+
export type UkpIgnorePosture =
|
|
264
|
+
| "created"
|
|
265
|
+
| "migrated"
|
|
266
|
+
/** The 2026-10-28 first precise form (WITH_TLS) swapped onto the current
|
|
267
|
+
* template: its `/tls/` rule named a resident INV-21 now guarantees
|
|
268
|
+
* absent — safe to drop only because the admission gate ran first. */
|
|
269
|
+
| "refreshed"
|
|
270
|
+
| "current"
|
|
271
|
+
| "operator-owned"
|
|
272
|
+
/** Operator-owned AND still carries a bare `*` rule — the declarations
|
|
273
|
+
* stay invisible; surfaced as a stderr warning with the remedy. */
|
|
274
|
+
| "operator-owned-star";
|
|
275
|
+
|
|
276
|
+
/** Same-file template swap through a temp + rename: a concurrent reader
|
|
277
|
+
* sees either template, never a torn file. */
|
|
278
|
+
function swapUkpIgnoreTemplate(ukpDirectory: string): void {
|
|
279
|
+
const tempPath = join(ukpDirectory, ".gitignore.ukp-tmp");
|
|
280
|
+
writeFileSync(tempPath, UKP_SELF_IGNORE, "utf8");
|
|
281
|
+
renameSync(tempPath, join(ukpDirectory, ".gitignore"));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Bring `.ukp/.gitignore` to the precise-ignore posture (the version
|
|
285
|
+
* control half of `ukp use` and register's local path): create when absent,
|
|
286
|
+
* migrate a verbatim legacy `*` template, refresh the retired first precise
|
|
287
|
+
* form, leave anything else untouched — the O-020 B-b operator-policy-wins
|
|
288
|
+
* rule. The `*` detector is advisory only (a note, never a rewrite). The
|
|
289
|
+
* INV-21 gate runs here too, so the rewrite itself can never land over
|
|
290
|
+
* foreign residents even if a carrier forgot to pre-gate (carriers still
|
|
291
|
+
* pre-gate before their own writes so the whole gesture fails atomically). */
|
|
292
|
+
export function ensureVersionVisibleUkpIgnore(ukpDirectory: string): UkpIgnorePosture {
|
|
293
|
+
assertUkpResidents(ukpDirectory);
|
|
294
|
+
const gitignorePath = join(ukpDirectory, ".gitignore");
|
|
295
|
+
const classify = (): UkpIgnorePosture => {
|
|
296
|
+
const content = readFileSync(gitignorePath, "utf8");
|
|
297
|
+
if (content === UKP_SELF_IGNORE) return "current";
|
|
298
|
+
if (content === LEGACY_UKP_SELF_IGNORE) {
|
|
299
|
+
swapUkpIgnoreTemplate(ukpDirectory);
|
|
300
|
+
return "migrated";
|
|
301
|
+
}
|
|
302
|
+
if (content === UKP_SELF_IGNORE_WITH_TLS) {
|
|
303
|
+
swapUkpIgnoreTemplate(ukpDirectory);
|
|
304
|
+
return "refreshed";
|
|
305
|
+
}
|
|
306
|
+
return content.split("\n").some((line) => line.trim() === "*")
|
|
307
|
+
? "operator-owned-star"
|
|
308
|
+
: "operator-owned";
|
|
309
|
+
};
|
|
310
|
+
if (!existsSync(gitignorePath)) {
|
|
311
|
+
try {
|
|
312
|
+
writeFileSync(gitignorePath, UKP_SELF_IGNORE, { flag: "wx", mode: 0o644 });
|
|
313
|
+
return "created";
|
|
314
|
+
} catch (error) {
|
|
315
|
+
// Concurrent writer installed a guard — classify whatever landed.
|
|
316
|
+
if (!(error instanceof Error) || (error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return classify();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** stdout line reporting a posture — shared verbatim by every carrier
|
|
323
|
+
* gesture so the version-control face reads identically wherever it runs. */
|
|
324
|
+
export function renderUkpIgnorePostureLine(posture: UkpIgnorePosture, ukpDirectory: string): string {
|
|
325
|
+
switch (posture) {
|
|
326
|
+
case "created":
|
|
327
|
+
return `version_control: .ukp/.gitignore created (machine-local state ignored; declarations committable)`;
|
|
328
|
+
case "migrated":
|
|
329
|
+
return `version_control: .ukp/.gitignore migrated off the retired '*' template (declarations committable; machine-local state stays ignored)`;
|
|
330
|
+
case "refreshed":
|
|
331
|
+
return `version_control: .ukp/.gitignore refreshed (retired tls/ rule dropped; machine-local state stays ignored)`;
|
|
332
|
+
case "current":
|
|
333
|
+
return `version_control: .ukp/.gitignore already precise (machine-local state only)`;
|
|
334
|
+
case "operator-owned":
|
|
335
|
+
return `version_control: .ukp/.gitignore is operator-owned - left untouched (${join(ukpDirectory, ".gitignore")})`;
|
|
336
|
+
case "operator-owned-star":
|
|
337
|
+
return `version_control: .ukp/.gitignore is operator-owned - left untouched (see warning)`;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** stderr warning for the operator-owned-star posture: the declarations
|
|
342
|
+
* stay invisible until the operator narrows the file. */
|
|
343
|
+
export function ukpIgnoreStarWarning(ukpDirectory: string): string {
|
|
344
|
+
return `warning: ${join(ukpDirectory, ".gitignore")} ignores everything ('*'); declarations stay invisible to version control - narrow it to machine-local state (instance-uid) so they can be committed\n`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Serve-side half of the TLS identity relocation (2026-11-01): once the
|
|
348
|
+
* legacy `.ukp/tls/` material has moved into the machine home, the
|
|
349
|
+
* by-then-retired first precise template (whose `/tls/` rule guarded it)
|
|
350
|
+
* swaps onto the current one — verbatim recognition only, so an
|
|
351
|
+
* operator-owned file is never touched. The posture ladder performs the
|
|
352
|
+
* same swap as its `refreshed` state; this entry point exists for serve,
|
|
353
|
+
* which carries no posture step. Returns whether a swap happened. */
|
|
354
|
+
export function retireTlsIgnoreLine(ukpDirectory: string): boolean {
|
|
355
|
+
const gitignorePath = join(ukpDirectory, ".gitignore");
|
|
356
|
+
let content: string;
|
|
357
|
+
try {
|
|
358
|
+
content = readFileSync(gitignorePath, "utf8");
|
|
359
|
+
} catch {
|
|
360
|
+
return false; // no guard in place — nothing to retire
|
|
361
|
+
}
|
|
362
|
+
if (content !== UKP_SELF_IGNORE_WITH_TLS) return false;
|
|
363
|
+
swapUkpIgnoreTemplate(ukpDirectory);
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function ensureUkpSelfIgnore(ukpDirectory: string): void {
|
|
368
|
+
const gitignorePath = join(ukpDirectory, ".gitignore");
|
|
369
|
+
if (existsSync(gitignorePath)) return;
|
|
370
|
+
try {
|
|
371
|
+
writeFileSync(gitignorePath, UKP_SELF_IGNORE, { flag: "wx", mode: 0o644 });
|
|
372
|
+
} catch (error) {
|
|
373
|
+
// Concurrent writer installed the same guard — in place either way.
|
|
374
|
+
if (!(error instanceof Error) || (error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
142
378
|
function createServiceManifest(
|
|
143
379
|
currentDirectory: string,
|
|
144
380
|
options: InitServiceOptions,
|
|
@@ -147,6 +383,10 @@ function createServiceManifest(
|
|
|
147
383
|
const ukpDirectory = join(folder, ".ukp");
|
|
148
384
|
const manifestPath = join(ukpDirectory, "service.toml");
|
|
149
385
|
const { name, nameSource, dependencies } = validateInitServiceOptions(folder, options);
|
|
386
|
+
// INV-21 admission gate: a pre-existing .ukp/ must hold nothing UKP does
|
|
387
|
+
// not own before init writes into it — operator-made junk fails loudly
|
|
388
|
+
// with zero residue (nothing has been written yet).
|
|
389
|
+
assertUkpResidents(ukpDirectory, { endpointName: name });
|
|
150
390
|
if (existsSync(manifestPath)) {
|
|
151
391
|
throw new Error(`Service Manifest already exists: ${manifestPath}`);
|
|
152
392
|
}
|
|
@@ -169,6 +409,8 @@ function createServiceManifest(
|
|
|
169
409
|
throw error;
|
|
170
410
|
}
|
|
171
411
|
|
|
412
|
+
ensureUkpSelfIgnore(ukpDirectory);
|
|
413
|
+
|
|
172
414
|
return { folder, manifestPath, name, nameSource };
|
|
173
415
|
}
|
|
174
416
|
|