@catheadowl/ukp 0.2.6 → 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 +83 -16
- package/SECURITY.md +69 -0
- package/bin/ukp.js +31 -7
- package/docs/remote-deployment.md +86 -8
- package/package.json +11 -6
- package/skills/ukp/SKILL.md +32 -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 +54 -3
- package/src/cli.ts +32 -64
- package/src/commands/guide-content/client.txt +19 -6
- package/src/commands/guide-content/remote.txt +21 -0
- package/src/commands/guide-content/rg.txt +2 -2
- package/src/commands/init.ts +224 -9
- 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 +5 -5
- package/src/commands/serve.ts +313 -29
- 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 +228 -45
- package/src/spawn-relay.ts +21 -3
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { ENDPOINT_NAME } from "../config/manifest.ts";
|
|
4
|
+
import { findNearestClientConfig, loadClientConfig } from "../config/client.ts";
|
|
5
|
+
import { readRegistry } from "../registry.ts";
|
|
6
|
+
import {
|
|
7
|
+
assertUkpResidents,
|
|
8
|
+
ensureVersionVisibleUkpIgnore,
|
|
9
|
+
renderUkpIgnorePostureLine,
|
|
10
|
+
ukpIgnoreStarWarning,
|
|
11
|
+
} from "./init.ts";
|
|
12
|
+
import {
|
|
13
|
+
KitUsageError,
|
|
14
|
+
parseKitArgs,
|
|
15
|
+
renderKitHelp,
|
|
16
|
+
renderKitUsageError,
|
|
17
|
+
type UkpCommandSpec,
|
|
18
|
+
} from "./kit.ts";
|
|
19
|
+
import { HelpRequestError, isHelpRequest } from "./flags.ts";
|
|
20
|
+
|
|
21
|
+
export interface UseCommandContext {
|
|
22
|
+
currentDirectory: string;
|
|
23
|
+
registryPath: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UseCommandResult {
|
|
27
|
+
exitCode: number;
|
|
28
|
+
stdout: string;
|
|
29
|
+
stderr: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `ukp use` (inbox client-portability-and-skill-coverage, owner-endorsed
|
|
33
|
+
* 2026-10-28): the one-step consumer onboarding gesture `register` is
|
|
34
|
+
* deliberately not (register never edits Client Config — the guide owned
|
|
35
|
+
* that journey until now, which left hand-writing the file plus a
|
|
36
|
+
* version-control carve dance). Named `use` rather than `init client`
|
|
37
|
+
* because the lifecycle is create-or-update and idempotent re-runs are the
|
|
38
|
+
* point, while the init family is create-only and refuses re-runs. */
|
|
39
|
+
export const USE_SPEC: UkpCommandSpec = {
|
|
40
|
+
name: "use",
|
|
41
|
+
summary: "set this workspace's default endpoints in one step",
|
|
42
|
+
group: "registry",
|
|
43
|
+
description: "Write the workspace Client Config (default_endpoints) and keep it version-visible; idempotent.",
|
|
44
|
+
usage: "<endpoint> [endpoint...]",
|
|
45
|
+
arguments: [
|
|
46
|
+
{ name: "endpoint...", required: true, help: "registered endpoint name(s); the whole list becomes default_endpoints (explicit replace, no append)" },
|
|
47
|
+
],
|
|
48
|
+
helpSuffix: [
|
|
49
|
+
"",
|
|
50
|
+
"Semantics:",
|
|
51
|
+
" The list REPLACES default_endpoints: re-running with the same names",
|
|
52
|
+
" changes nothing; running with new names rewrites the list (the old",
|
|
53
|
+
" list is reported). The nearest existing .ukp/client.toml found",
|
|
54
|
+
" walking up from here is updated; with none found, one is created in",
|
|
55
|
+
" the current folder. Every name must already be registered",
|
|
56
|
+
" ('ukp list -g'); registering stays a separate explicit gesture.",
|
|
57
|
+
"",
|
|
58
|
+
"Version control rides the same step: .ukp/.gitignore is created with",
|
|
59
|
+
" machine-local ignores only (declarations stay committable), or",
|
|
60
|
+
" migrated off the retired '*' template; an operator-owned file is",
|
|
61
|
+
" left untouched. 'ukp inspect' shows the active scope afterwards.",
|
|
62
|
+
"",
|
|
63
|
+
].join("\n"),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Posture of `.ukp/.gitignore` after `ukp use` ran — the ladder and its
|
|
67
|
+
* mechanism live in init.ts (the template's home); `use` is the client-face
|
|
68
|
+
* carrier, register's local path the service-face one. */
|
|
69
|
+
|
|
70
|
+
function renderClientConfig(names: readonly string[]): string {
|
|
71
|
+
// Hand-rendered rather than smol-toml stringify: the schema is exactly
|
|
72
|
+
// one string-array field, and the serializer's `[ "a", "b" ]` spacing
|
|
73
|
+
// would diverge from every documented example (`["a", "b"]`). A slug
|
|
74
|
+
// name needs no escapes, so JSON.stringify is a valid TOML basic string.
|
|
75
|
+
return `default_endpoints = [${names.map((name) => JSON.stringify(name)).join(", ")}]\n`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function executeUseCommand(
|
|
79
|
+
args: readonly string[],
|
|
80
|
+
context: UseCommandContext,
|
|
81
|
+
): UseCommandResult {
|
|
82
|
+
if (isHelpRequest(args)) {
|
|
83
|
+
return { exitCode: 0, stdout: renderKitHelp(USE_SPEC), stderr: "" };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const parsed = parseKitArgs(USE_SPEC, args);
|
|
88
|
+
const names = parsed.positionals;
|
|
89
|
+
if (names.length === 0) {
|
|
90
|
+
throw new KitUsageError("use requires at least one endpoint name");
|
|
91
|
+
}
|
|
92
|
+
for (const name of names) {
|
|
93
|
+
if (!ENDPOINT_NAME.test(name)) {
|
|
94
|
+
throw new KitUsageError(`invalid endpoint name '${name}'. Endpoint names use the lowercase slug grammar ([a-z0-9-]).`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// The written file must satisfy the Client Config schema on the spot —
|
|
98
|
+
// duplicates are refused here, not left for every later load to warn.
|
|
99
|
+
const seen = new Set<string>();
|
|
100
|
+
for (const name of names) {
|
|
101
|
+
if (seen.has(name)) {
|
|
102
|
+
throw new KitUsageError(`duplicate endpoint '${name}' - default_endpoints cannot repeat`);
|
|
103
|
+
}
|
|
104
|
+
seen.add(name);
|
|
105
|
+
}
|
|
106
|
+
// Admission against the registry (the file stores names only; names
|
|
107
|
+
// must resolve): catches typos at write time instead of as dangling
|
|
108
|
+
// warnings on every later command. Registering stays separate.
|
|
109
|
+
const registered = new Set(readRegistry(context.registryPath).map((binding) => binding.name));
|
|
110
|
+
const unknown = names.filter((name) => !registered.has(name));
|
|
111
|
+
if (unknown.length > 0) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`unknown endpoint '${unknown[0]}'; run 'ukp list -g' to see every registered endpoint, or 'ukp register' first`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Nearest-wins, the same lookup rule scope resolution applies: the
|
|
118
|
+
// config updated is the one actually in effect from here; a workspace
|
|
119
|
+
// without one gets it created at the current folder.
|
|
120
|
+
const existingPath = findNearestClientConfig(context.currentDirectory);
|
|
121
|
+
const targetPath = existingPath ?? join(context.currentDirectory, ".ukp", "client.toml");
|
|
122
|
+
const ukpDirectory = dirname(targetPath);
|
|
123
|
+
// INV-21 admission gate before any write: the whole gesture refuses
|
|
124
|
+
// atomically when .ukp/ holds foreign residents (no half-written
|
|
125
|
+
// client.toml, no guard rewritten over material it no longer covers).
|
|
126
|
+
assertUkpResidents(ukpDirectory);
|
|
127
|
+
let status: "created" | "updated" | "unchanged";
|
|
128
|
+
let previous: string[] | undefined;
|
|
129
|
+
if (existingPath !== undefined) {
|
|
130
|
+
// Structural refusal, never a clobber: an invalid config is the
|
|
131
|
+
// operator's to fix or delete; overwriting it would silently discard
|
|
132
|
+
// whatever they were trying to express.
|
|
133
|
+
try {
|
|
134
|
+
const current = loadClientConfig(existingPath);
|
|
135
|
+
previous = current.default_endpoints;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`existing Client Config is invalid (${existingPath}): ${error instanceof Error ? error.message : String(error)}; fix or delete the file, then re-run 'ukp use'`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
status = previous.length === names.length && previous.every((name, index) => name === names[index])
|
|
142
|
+
? "unchanged"
|
|
143
|
+
: "updated";
|
|
144
|
+
} else {
|
|
145
|
+
status = "created";
|
|
146
|
+
}
|
|
147
|
+
if (status !== "unchanged") {
|
|
148
|
+
mkdirSync(ukpDirectory, { recursive: true });
|
|
149
|
+
// Fixed temp name + atomic rename: readers (this CLI included) never
|
|
150
|
+
// see a torn file; a crash residue is one file, overwritten next run.
|
|
151
|
+
const tempPath = join(ukpDirectory, "client.toml.ukp-tmp");
|
|
152
|
+
writeFileSync(tempPath, renderClientConfig(names), "utf8");
|
|
153
|
+
renameSync(tempPath, targetPath);
|
|
154
|
+
}
|
|
155
|
+
const posture = ensureVersionVisibleUkpIgnore(ukpDirectory);
|
|
156
|
+
|
|
157
|
+
const lines = [
|
|
158
|
+
`default_endpoints: ${names.join(", ")}`,
|
|
159
|
+
...(previous !== undefined && status === "updated" ? [`replaced: ${previous.join(", ")}`] : []),
|
|
160
|
+
`client_config: ${status}: ${targetPath}`,
|
|
161
|
+
renderUkpIgnorePostureLine(posture, ukpDirectory),
|
|
162
|
+
"next: ukp inspect (see the active scope)",
|
|
163
|
+
];
|
|
164
|
+
return {
|
|
165
|
+
exitCode: 0,
|
|
166
|
+
stdout: lines.join("\n"),
|
|
167
|
+
stderr: posture === "operator-owned-star" ? ukpIgnoreStarWarning(ukpDirectory) : "",
|
|
168
|
+
};
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof HelpRequestError) {
|
|
171
|
+
return { exitCode: 0, stdout: renderKitHelp(USE_SPEC), stderr: "" };
|
|
172
|
+
}
|
|
173
|
+
if (error instanceof KitUsageError) {
|
|
174
|
+
return { exitCode: 2, stdout: "", stderr: renderKitUsageError(USE_SPEC, error.message) };
|
|
175
|
+
}
|
|
176
|
+
return { exitCode: 1, stdout: "", stderr: `ukp use: ${error instanceof Error ? error.message : String(error)}\n` };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// ukp_session (ADR-SESSION-001): agent-backed capability tier — the table
|
|
2
|
+
// governing capabilities whose substrate is an external agent harness
|
|
3
|
+
// declared in the Host Registry. Mirrors the file-native (ADR 0016) and
|
|
4
|
+
// external-tool (ADR-RG-003) tables: set membership, the provider marker,
|
|
5
|
+
// and flat declaration keys live here and nowhere else. Unlike those tiers
|
|
6
|
+
// an agent-backed capability is never derived — absence of the declaration
|
|
7
|
+
// means the endpoint explicitly does not host sessions, and spawn treats
|
|
8
|
+
// that as a hard stop, not a warning (line spec §1 / §5).
|
|
9
|
+
|
|
10
|
+
import type { ManifestCapability } from "./manifest.ts";
|
|
11
|
+
|
|
12
|
+
/** Provider marker for the agent-backed tier. A bare
|
|
13
|
+
* `[capabilities.<name>]` declaration defaults to this value; any other
|
|
14
|
+
* provider is a hard error (the tier does not plugin alternative bases). */
|
|
15
|
+
export const AGENT_PROVIDER = "agent";
|
|
16
|
+
|
|
17
|
+
export type AgentBackedCapabilityName = "session";
|
|
18
|
+
|
|
19
|
+
/** Write permission tier for a hosted session (owner ruling 2026-10-18: the
|
|
20
|
+
* grant lives in the endpoint's own declaration, not in the adapter default
|
|
21
|
+
* and not in runtime approval). `read-only` is the scout default; `workspace`
|
|
22
|
+
* maps to the substrate's workspace-write sandbox (writes confined to the
|
|
23
|
+
* endpoint folder). */
|
|
24
|
+
export type SessionWriteTier = "read-only" | "workspace";
|
|
25
|
+
|
|
26
|
+
interface AgentBackedCapabilitySpec {
|
|
27
|
+
/** Always false: hosting is opt-in per endpoint (ADR-SESSION-001). */
|
|
28
|
+
readonly derived: false;
|
|
29
|
+
/** Flat keys accepted directly under `[capabilities.<name>]`. */
|
|
30
|
+
readonly flatConfigKeys: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const AGENT_BACKED_CAPABILITIES: Readonly<
|
|
34
|
+
Record<AgentBackedCapabilityName, AgentBackedCapabilitySpec>
|
|
35
|
+
> = {
|
|
36
|
+
session: { derived: false, flatConfigKeys: ["agents", "write"] },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function isAgentBackedCapability(name: string): name is AgentBackedCapabilityName {
|
|
40
|
+
return Object.hasOwn(AGENT_BACKED_CAPABILITIES, name);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
44
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Load-time normalization of flat declaration keys into `config`, the
|
|
48
|
+
* same discipline as the file-native pass (flat UX without the `.config`
|
|
49
|
+
* hop; the same key both places is a declaration conflict). Pure. */
|
|
50
|
+
export function normalizeAgentBackedFlatKeys(
|
|
51
|
+
raw: unknown,
|
|
52
|
+
fail: (message: string) => never,
|
|
53
|
+
): unknown {
|
|
54
|
+
if (!isRecord(raw) || !isRecord(raw.capabilities)) return raw;
|
|
55
|
+
const output: Record<string, unknown> = { ...raw.capabilities };
|
|
56
|
+
let changed = false;
|
|
57
|
+
for (const [name, declaration] of Object.entries(raw.capabilities)) {
|
|
58
|
+
if (!isAgentBackedCapability(name) || !isRecord(declaration)) continue;
|
|
59
|
+
const flatKeys = AGENT_BACKED_CAPABILITIES[name].flatConfigKeys;
|
|
60
|
+
if (!flatKeys.some((key) => Object.hasOwn(declaration, key))) continue;
|
|
61
|
+
const config = isRecord(declaration.config) ? { ...declaration.config } : {};
|
|
62
|
+
const next: Record<string, unknown> = { ...declaration };
|
|
63
|
+
for (const key of flatKeys) {
|
|
64
|
+
if (!Object.hasOwn(next, key)) continue;
|
|
65
|
+
if (Object.hasOwn(config, key)) {
|
|
66
|
+
fail(`[capabilities.${name}] '${key}' is declared both directly and under [capabilities.${name}.config]`);
|
|
67
|
+
}
|
|
68
|
+
config[key] = next[key];
|
|
69
|
+
delete next[key];
|
|
70
|
+
}
|
|
71
|
+
next.config = config;
|
|
72
|
+
output[name] = next;
|
|
73
|
+
changed = true;
|
|
74
|
+
}
|
|
75
|
+
return changed ? { ...raw, capabilities: output } : raw;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Handle grammar for agent handles and allowlist entries — the full
|
|
79
|
+
* ENDPOINT_NAME constraint (manifest.ts), length bound included, kept
|
|
80
|
+
* textual here so the governance table owns its own surface. */
|
|
81
|
+
export const AGENT_HANDLE = /^(?=.{1,63}$)[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
82
|
+
|
|
83
|
+
/** Declaration-level validation of the allowlist (shape only — whether a
|
|
84
|
+
* handle has a Host Registry substrate is use-time, spec §2). An empty
|
|
85
|
+
* allowlist is a valid "off" declaration (spec §1). */
|
|
86
|
+
export function validateAgentBackedDeclaration(
|
|
87
|
+
name: AgentBackedCapabilityName,
|
|
88
|
+
declaration: ManifestCapability,
|
|
89
|
+
fail: (message: string) => never,
|
|
90
|
+
): void {
|
|
91
|
+
if (name !== "session") return;
|
|
92
|
+
const agents = declaration.config?.agents;
|
|
93
|
+
if (agents === undefined) {
|
|
94
|
+
fail(`[capabilities.session] must declare 'agents' (an allowlist of agent handles; empty = hosting off)`);
|
|
95
|
+
}
|
|
96
|
+
if (!Array.isArray(agents)) {
|
|
97
|
+
fail("[capabilities.session] 'agents' must be an array of agent handles");
|
|
98
|
+
}
|
|
99
|
+
const seen = new Set<string>();
|
|
100
|
+
for (const item of agents) {
|
|
101
|
+
if (typeof item !== "string" || !AGENT_HANDLE.test(item)) {
|
|
102
|
+
fail(`[capabilities.session] 'agents' contains an invalid handle: ${JSON.stringify(item)}`);
|
|
103
|
+
}
|
|
104
|
+
if (seen.has(item)) {
|
|
105
|
+
fail(`[capabilities.session] 'agents' lists handle '${item}' more than once`);
|
|
106
|
+
}
|
|
107
|
+
seen.add(item);
|
|
108
|
+
}
|
|
109
|
+
const write = declaration.config?.write;
|
|
110
|
+
if (write !== undefined && write !== "workspace") {
|
|
111
|
+
fail(`[capabilities.session] 'write' must be "workspace" when declared (got ${JSON.stringify(write)}); absent = read-only`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The effective hosting allowlist: handles declared under
|
|
116
|
+
* `[capabilities.session]`. Absent declaration or empty list = hosting
|
|
117
|
+
* off (spawn refuses; never a warning). */
|
|
118
|
+
export function resolveSessionAgents(manifest: {
|
|
119
|
+
capabilities: Record<string, ManifestCapability>;
|
|
120
|
+
}): readonly string[] {
|
|
121
|
+
const declaration = manifest.capabilities.session;
|
|
122
|
+
if (declaration === undefined) return [];
|
|
123
|
+
const agents = declaration.config?.agents;
|
|
124
|
+
if (!Array.isArray(agents)) return [];
|
|
125
|
+
return agents.filter((item): item is string => typeof item === "string");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The effective write tier: `workspace` only when the endpoint declared it;
|
|
129
|
+
* anything else (absent declaration included) is the read-only scout default.
|
|
130
|
+
* Load-time validation already rejected foreign values — this reads what
|
|
131
|
+
* passed, it does not re-decide. */
|
|
132
|
+
export function resolveSessionWriteTier(manifest: {
|
|
133
|
+
capabilities: Record<string, ManifestCapability>;
|
|
134
|
+
}): SessionWriteTier {
|
|
135
|
+
return manifest.capabilities.session?.config?.write === "workspace" ? "workspace" : "read-only";
|
|
136
|
+
}
|
package/src/config/manifest.ts
CHANGED
|
@@ -4,6 +4,12 @@ import { parse } from "smol-toml";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { isExternalToolCapability, EXTERNAL_PROVIDER } from "./external-tool.ts";
|
|
6
6
|
import { isFileNativeCapability, normalizeFileNativeFlatKeys } from "./file-native.ts";
|
|
7
|
+
import {
|
|
8
|
+
AGENT_PROVIDER,
|
|
9
|
+
isAgentBackedCapability,
|
|
10
|
+
normalizeAgentBackedFlatKeys,
|
|
11
|
+
validateAgentBackedDeclaration,
|
|
12
|
+
} from "./agent-backed.ts";
|
|
7
13
|
|
|
8
14
|
export const ENDPOINT_NAME = /^(?=.{1,63}$)[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
9
15
|
|
|
@@ -58,6 +64,20 @@ export class ManifestError extends Error {
|
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
/** Dead-binding classification (registry-commands dead-binding work item 1,
|
|
68
|
+
* 2026-10-24): the Service folder itself is unreachable — the binding routes
|
|
69
|
+
* to a path that no longer exists. Subclasses ManifestError so broad
|
|
70
|
+
* `instanceof ManifestError` catches keep working; consumers that need the
|
|
71
|
+
* distinction (the list face frames this as a binding-level failure with the
|
|
72
|
+
* one-step rebind remedy, not a capability degradation) branch on the class
|
|
73
|
+
* instead of matching message text. */
|
|
74
|
+
export class ServiceFolderInaccessibleError extends ManifestError {
|
|
75
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
76
|
+
super(message, options);
|
|
77
|
+
this.name = "ServiceFolderInaccessibleError";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
61
81
|
export function assertRestrictedToml(value: unknown, path = "root"): void {
|
|
62
82
|
if (value === null || value === undefined || value instanceof Date) {
|
|
63
83
|
throw new ManifestError(`${path}: unsupported TOML value`);
|
|
@@ -122,7 +142,7 @@ export function loadManifest(serviceFolder: string): LoadedManifest {
|
|
|
122
142
|
folder = realpathSync(serviceFolder);
|
|
123
143
|
if (!statSync(folder).isDirectory()) throw new Error("not a directory");
|
|
124
144
|
} catch (error) {
|
|
125
|
-
throw new
|
|
145
|
+
throw new ServiceFolderInaccessibleError(`Service folder is not accessible: ${serviceFolder}`, { cause: error });
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
const manifestPath = join(folder, ".ukp", "service.toml");
|
|
@@ -143,8 +163,12 @@ export function loadManifest(serviceFolder: string): LoadedManifest {
|
|
|
143
163
|
|
|
144
164
|
const withoutLegacyGet = removeLegacyGetCapability(raw);
|
|
145
165
|
// Flat file-native declaration keys (table-driven, ADR 0016) fold into
|
|
146
|
-
// `config` before the strict schema parse
|
|
147
|
-
|
|
166
|
+
// `config` before the strict schema parse; agent-backed declarations
|
|
167
|
+
// (ukp_session) get the identical pass from their own governance table.
|
|
168
|
+
const normalizedFileNative = normalizeFileNativeFlatKeys(withoutLegacyGet, (message) => {
|
|
169
|
+
throw new ManifestError(message);
|
|
170
|
+
});
|
|
171
|
+
const normalized = normalizeAgentBackedFlatKeys(normalizedFileNative, (message) => {
|
|
148
172
|
throw new ManifestError(message);
|
|
149
173
|
});
|
|
150
174
|
if (isRecord(normalized) && !Object.hasOwn(normalized, "capabilities")) {
|
|
@@ -180,6 +204,8 @@ export function loadManifest(serviceFolder: string): LoadedManifest {
|
|
|
180
204
|
manifest.capabilities[name] = { ...declaration, provider: "file" };
|
|
181
205
|
} else if (isExternalToolCapability(name)) {
|
|
182
206
|
manifest.capabilities[name] = { ...declaration, provider: EXTERNAL_PROVIDER };
|
|
207
|
+
} else if (isAgentBackedCapability(name)) {
|
|
208
|
+
manifest.capabilities[name] = { ...declaration, provider: AGENT_PROVIDER };
|
|
183
209
|
} else {
|
|
184
210
|
throw new ManifestError(`[capabilities.${name}] must declare a provider`);
|
|
185
211
|
}
|
|
@@ -188,6 +214,16 @@ export function loadManifest(serviceFolder: string): LoadedManifest {
|
|
|
188
214
|
`[capabilities.${name}] provider must be '${EXTERNAL_PROVIDER}' or omitted `
|
|
189
215
|
+ `(external-tool base tier: the declaration only overrides config)`,
|
|
190
216
|
);
|
|
217
|
+
} else if (isAgentBackedCapability(name) && declaration.provider !== AGENT_PROVIDER) {
|
|
218
|
+
throw new ManifestError(
|
|
219
|
+
`[capabilities.${name}] provider must be '${AGENT_PROVIDER}' or omitted `
|
|
220
|
+
+ `(agent-backed tier: the substrate is a Host Registry agent, not a provider plugin)`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (isAgentBackedCapability(name)) {
|
|
224
|
+
validateAgentBackedDeclaration(name, manifest.capabilities[name]!, (message) => {
|
|
225
|
+
throw new ManifestError(message);
|
|
226
|
+
});
|
|
191
227
|
}
|
|
192
228
|
}
|
|
193
229
|
return { folder, manifestPath, manifest, effectiveName, nameSource };
|
package/src/manifest.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
schema_version: 1
|
|
2
|
+
kind: directory
|
|
3
|
+
|
|
4
|
+
metadata:
|
|
5
|
+
name: ukp-src
|
|
6
|
+
description: The ukp source publish face - the shipped src subtree (package.json files whitelist); design records live in the dev-side control plane (line ADRs) and do not ship, so src comments citing them are publish-face vocabulary for the write-time tip layer
|
|
7
|
+
|
|
8
|
+
publication:
|
|
9
|
+
enabled: false
|
|
10
|
+
mode: closed
|
|
11
|
+
root: .
|
|
12
|
+
include:
|
|
13
|
+
- "**/*"
|
|
14
|
+
exclude:
|
|
15
|
+
- "manifest.yml"
|
package/src/path-safety.ts
CHANGED
|
@@ -27,14 +27,19 @@ export type EndpointRelativeSegments =
|
|
|
27
27
|
| { ok: false; reason: "absolute" | "empty-segment" | "dot-segment" };
|
|
28
28
|
|
|
29
29
|
/** Segment-level validation of an endpoint-relative reference: rejects
|
|
30
|
-
* absolute shapes, empty segments (`a//b`), and `.`/`..` segments.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* absolute shapes, interior empty segments (`a//b`), and `.`/`..` segments.
|
|
31
|
+
* Trailing separators are address-encoding noise, not a segment claim —
|
|
32
|
+
* POSIX resolves `docs/` as `docs` — so they are stripped and stat decides
|
|
33
|
+
* the outcome (a directory tail lands on the caller's friendly
|
|
34
|
+
* is-a-directory failure, never on the grammar error that used to run
|
|
35
|
+
* before it). Accepts both `/` and `\` separators; callers own separator
|
|
36
|
+
* normalization. The reason field lets each capability map its own error
|
|
37
|
+
* wording. */
|
|
33
38
|
export function splitEndpointRelativeSegments(reference: string): EndpointRelativeSegments {
|
|
34
39
|
if (isAbsoluteShapedPath(reference)) {
|
|
35
40
|
return { ok: false, reason: "absolute" };
|
|
36
41
|
}
|
|
37
|
-
const segments = reference.split(/[\\/]/);
|
|
42
|
+
const segments = reference.replace(/[\\/]+$/, "").split(/[\\/]/);
|
|
38
43
|
if (segments.some((segment) => segment.length === 0)) {
|
|
39
44
|
return { ok: false, reason: "empty-segment" };
|
|
40
45
|
}
|