hngr 0.1.0 → 0.2.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.
@@ -0,0 +1,3184 @@
1
+ // src/generated/catalog.ts
2
+ /** Projects one derived command onto the stable `commands --json` shape. */
3
+ function generatedEntry(spec) {
4
+ return {
5
+ name: spec.name,
6
+ summary: spec.summary,
7
+ group: spec.group,
8
+ args: spec.args.map((arg) => ({
9
+ name: arg.name,
10
+ required: arg.required,
11
+ description: arg.description
12
+ })),
13
+ flags: spec.flags.map((flag) => ({
14
+ name: flag.name,
15
+ type: catalogFlagType(flag),
16
+ description: flag.description
17
+ })),
18
+ destructive: spec.destructive,
19
+ dryRun: true
20
+ };
21
+ }
22
+ function catalogFlagType(flag) {
23
+ return flag.type === "boolean" ? "boolean" : "string";
24
+ }
25
+ // src/generated/overrides.ts
26
+ /**
27
+ * The explicit override table. Every entry that the mechanical naming rules
28
+ * cannot decide lives here, so the ambiguity is reviewable in one place
29
+ * instead of hidden in the derivation algorithm.
30
+ */
31
+ /** Endpoints that never appear in the command tree. */
32
+ const EXCLUDED_PATHS = [
33
+ "/api/v1/login",
34
+ "/api/v1/logout",
35
+ "/api/v1/me/password",
36
+ "/api/v1/me/sessions",
37
+ "/api/v1/me/sessions/{id}",
38
+ "/api/v1/capabilities/{capability_id}/versions/{version}/content",
39
+ "/api/v1/auth/methods",
40
+ "/api/v1/auth/oidc/login",
41
+ "/api/v1/auth/oidc/callback"
42
+ ];
43
+ /**
44
+ * Root-noun aliases. Aliases only affect parsing: help annotates the canonical
45
+ * line, while `commands --json`, `schema` and completion keep the canonical
46
+ * name and the generated catalog never gains an alias entry.
47
+ */
48
+ const COMMAND_ALIASES = { capabilities: ["caps", "cap"] };
49
+ /**
50
+ * Segments that name a sub-collection rather than an action, even when they
51
+ * follow a path parameter. Without this, `GET /capabilities/{id}/versions`
52
+ * would read as the action `capabilities versions`.
53
+ */
54
+ const SUBGROUP_SEGMENTS = /* @__PURE__ */ new Set([
55
+ "versions",
56
+ "members",
57
+ "member-candidates",
58
+ "tokens"
59
+ ]);
60
+ const COMMAND_OVERRIDES = [
61
+ {
62
+ method: "GET",
63
+ path: "/api/v1/me",
64
+ command: "auth whoami",
65
+ registeredExternally: true,
66
+ why: "/me is remapped to auth; whoami is hand-written."
67
+ },
68
+ {
69
+ method: "PATCH",
70
+ path: "/api/v1/me",
71
+ command: "auth profile update",
72
+ why: "PATCH /me is the caller's profile, not a principal lookup."
73
+ },
74
+ {
75
+ method: "GET",
76
+ path: "/api/v1/me/tokens",
77
+ command: "auth tokens list",
78
+ why: "/me/tokens belongs under auth, not a `me` noun."
79
+ },
80
+ {
81
+ method: "POST",
82
+ path: "/api/v1/me/tokens",
83
+ command: "auth tokens create",
84
+ why: "/me/tokens belongs under auth, not a `me` noun."
85
+ },
86
+ {
87
+ method: "DELETE",
88
+ path: "/api/v1/me/tokens/{token_id}",
89
+ command: "auth tokens revoke",
90
+ why: "The verb is revoke; the mechanical DELETE verb is delete."
91
+ },
92
+ {
93
+ method: "POST",
94
+ path: "/api/v1/capabilities",
95
+ bodyContentType: "application/json",
96
+ why: "The multipart Skill package variant is published from the Web Console."
97
+ },
98
+ {
99
+ method: "POST",
100
+ path: "/api/v1/capabilities/{capability_id}/versions",
101
+ bodyContentType: "application/json",
102
+ why: "The multipart Skill package variant is published from the Web Console."
103
+ }
104
+ ];
105
+ // src/generated/derive.ts
106
+ const API_PREFIX = "/api/v1/";
107
+ const METHODS = [
108
+ "GET",
109
+ "PUT",
110
+ "POST",
111
+ "PATCH",
112
+ "DELETE"
113
+ ];
114
+ const METHOD_KEY = {
115
+ GET: "get",
116
+ PUT: "put",
117
+ POST: "post",
118
+ PATCH: "patch",
119
+ DELETE: "delete"
120
+ };
121
+ /**
122
+ * Command words the mechanical rules cannot decide. Everything here is
123
+ * deterministic and test-covered; see overrides.ts for the reviewable table.
124
+ */
125
+ function deriveCommands(doc) {
126
+ const excluded = new Set(EXCLUDED_PATHS);
127
+ const overrides = new Map(COMMAND_OVERRIDES.map((item) => [overrideKey(item), item]));
128
+ const commands = [];
129
+ for (const [path, pathItem] of Object.entries(doc.paths)) {
130
+ if (excluded.has(path)) continue;
131
+ for (const method of METHODS) {
132
+ const operation = pathItem[METHOD_KEY[method]];
133
+ if (!operation) continue;
134
+ commands.push(deriveCommand(doc, path, pathItem, method, operation, overrides));
135
+ }
136
+ }
137
+ return commands;
138
+ }
139
+ function overrideKey(override) {
140
+ return `${override.method} ${override.path}`;
141
+ }
142
+ function deriveCommand(doc, path, pathItem, method, operation, overrides) {
143
+ const override = overrides.get(`${method} ${path}`);
144
+ const name = override?.command ?? deriveName(path, method);
145
+ const segments = pathSegments(path);
146
+ const last = segments.at(-1) ?? "";
147
+ const lastIsParam = isParam(last);
148
+ const isAction = !lastIsParam && !SUBGROUP_SEGMENTS.has(last) && segments.length > 1 && isParam(segments[segments.length - 2] ?? "");
149
+ const isCollection = !lastIsParam && !isAction;
150
+ const parameters = resolveOperationParameters(doc, pathItem, operation);
151
+ const pathParams = pathParameters(path, parameters);
152
+ const queryParams = parameters.filter((parameter) => parameter.in === "query" && parameter.name).map(toParameterSpec);
153
+ const headerParams = parameters.filter((parameter) => parameter.in === "header" && parameter.name).map(toParameterSpec);
154
+ const body = buildBody(doc, operation, override);
155
+ const paginated = isCollection && isPaginated(doc, operation, queryParams);
156
+ const flags = buildFlags(queryParams, headerParams, body, paginated);
157
+ const args = pathParams.map((parameter) => ({
158
+ name: parameter.name,
159
+ required: true,
160
+ description: parameter.description
161
+ }));
162
+ return {
163
+ name,
164
+ group: name.split(" ")[0] ?? name,
165
+ method,
166
+ path,
167
+ operationId: operation.operationId ?? name.replaceAll(" ", "_"),
168
+ summary: operation.summary ?? name,
169
+ args,
170
+ flags,
171
+ pathParams,
172
+ queryParams,
173
+ headerParams,
174
+ body,
175
+ paginated,
176
+ destructive: method === "DELETE" || isAction && isDestructiveAction(last),
177
+ registeredExternally: override?.registeredExternally === true
178
+ };
179
+ }
180
+ /** The root noun + subgroup + leaf-verb naming rules. Pure and independently testable. */
181
+ function deriveName(path, method) {
182
+ const segments = pathSegments(path);
183
+ const last = segments.at(-1) ?? "";
184
+ let verb;
185
+ let groupSegments;
186
+ if (isParam(last)) {
187
+ groupSegments = segments.slice(0, -1);
188
+ verb = itemVerb(method);
189
+ } else if (SUBGROUP_SEGMENTS.has(last)) {
190
+ groupSegments = segments;
191
+ verb = collectionVerb(method);
192
+ } else if (segments.length > 1 && isParam(segments[segments.length - 2] ?? "")) {
193
+ groupSegments = segments.slice(0, -1);
194
+ verb = last;
195
+ } else {
196
+ groupSegments = segments;
197
+ verb = collectionVerb(method);
198
+ }
199
+ return [...groupSegments.filter((segment) => !isParam(segment)), verb].join(" ");
200
+ }
201
+ function itemVerb(method) {
202
+ switch (method) {
203
+ case "GET": return "get";
204
+ case "PATCH":
205
+ case "PUT": return "update";
206
+ case "DELETE": return "delete";
207
+ case "POST": return "create";
208
+ }
209
+ }
210
+ function collectionVerb(method) {
211
+ switch (method) {
212
+ case "GET": return "list";
213
+ case "POST": return "create";
214
+ case "DELETE": return "delete";
215
+ case "PATCH":
216
+ case "PUT": return "update";
217
+ }
218
+ }
219
+ function isDestructiveAction(verb) {
220
+ return verb === "disable" || verb === "password" || verb === "role";
221
+ }
222
+ function pathSegments(path) {
223
+ return (path.startsWith(API_PREFIX) ? path.slice(8) : path.replace(/^\//, "")).split("/").filter((segment) => segment !== "");
224
+ }
225
+ function isParam(segment) {
226
+ return segment.startsWith("{") && segment.endsWith("}");
227
+ }
228
+ function paramName(segment) {
229
+ return segment.slice(1, -1);
230
+ }
231
+ function pathParameters(path, parameters) {
232
+ const declared = /* @__PURE__ */ new Map();
233
+ for (const parameter of parameters) if (parameter.in === "path" && parameter.name) declared.set(parameter.name, parameter);
234
+ return pathSegments(path).filter(isParam).map((segment) => {
235
+ const name = paramName(segment);
236
+ const parameter = declared.get(name);
237
+ return parameter ? toParameterSpec(parameter) : {
238
+ name,
239
+ flag: kebabCase(name),
240
+ required: true,
241
+ type: "string"
242
+ };
243
+ });
244
+ }
245
+ function toParameterSpec(parameter) {
246
+ const name = parameter.name ?? "";
247
+ return {
248
+ name,
249
+ flag: kebabCase(name),
250
+ required: parameter.in === "path" || parameter.required === true,
251
+ type: parameterType(parameter.schema),
252
+ description: parameter.description
253
+ };
254
+ }
255
+ function buildFlags(queryParams, headerParams, body, paginated) {
256
+ const flags = [];
257
+ for (const parameter of queryParams) flags.push({
258
+ name: parameter.flag,
259
+ type: parameter.type,
260
+ description: parameter.description,
261
+ source: "query",
262
+ target: parameter.name
263
+ });
264
+ for (const parameter of headerParams) flags.push({
265
+ name: parameter.flag,
266
+ type: parameter.type,
267
+ description: parameter.description,
268
+ source: "header",
269
+ target: parameter.name
270
+ });
271
+ for (const field of body?.fields ?? []) flags.push({
272
+ name: field.flag,
273
+ type: field.type,
274
+ description: field.description,
275
+ source: "body",
276
+ target: field.name
277
+ });
278
+ if (paginated) flags.push({
279
+ name: "all",
280
+ type: "boolean",
281
+ description: "Follow next_cursor to the end and print one merged list",
282
+ source: "query",
283
+ target: "all"
284
+ });
285
+ if (body) flags.push({
286
+ name: "body",
287
+ type: "string",
288
+ description: "JSON request body, or @file.json",
289
+ source: "body",
290
+ target: "body"
291
+ });
292
+ return dedupeFlags(flags);
293
+ }
294
+ function dedupeFlags(flags) {
295
+ const seen = /* @__PURE__ */ new Set();
296
+ return flags.filter((flag) => {
297
+ if (seen.has(flag.name)) return false;
298
+ seen.add(flag.name);
299
+ return true;
300
+ });
301
+ }
302
+ function buildBody(doc, operation, override) {
303
+ const requestBody = resolveRequestBody(doc, operation.requestBody);
304
+ if (!requestBody?.content) return null;
305
+ const chosen = chooseContent(requestBody, override?.bodyContentType);
306
+ if (!chosen) return null;
307
+ const schema = resolveSchema(doc, chosen.media.schema ?? {});
308
+ const requiredFields = (schema.required ?? []).filter((field) => typeof field === "string");
309
+ const required = new Set(requiredFields);
310
+ const fields = [];
311
+ for (const [name, property] of Object.entries(schema.properties ?? {})) {
312
+ const resolved = resolveSchema(doc, property);
313
+ if (!isPrimitiveSchema(resolved)) continue;
314
+ fields.push({
315
+ name,
316
+ flag: kebabCase(name),
317
+ required: required.has(name),
318
+ type: parameterType(resolved),
319
+ description: resolved.description
320
+ });
321
+ }
322
+ return {
323
+ contentType: chosen.contentType === "multipart/form-data" ? "multipart/form-data" : "application/json",
324
+ required: requestBody.required === true,
325
+ requiredFields,
326
+ fields,
327
+ schema
328
+ };
329
+ }
330
+ function chooseContent(requestBody, preferred) {
331
+ const content = requestBody.content;
332
+ if (!content) return;
333
+ const order = preferred ? [
334
+ preferred,
335
+ "application/json",
336
+ ...Object.keys(content)
337
+ ] : ["application/json", ...Object.keys(content)];
338
+ for (const contentType of order) {
339
+ const media = content[contentType];
340
+ if (media) return {
341
+ contentType,
342
+ media
343
+ };
344
+ }
345
+ }
346
+ function isPaginated(doc, operation, queryParams) {
347
+ if (queryParams.some((parameter) => parameter.name === "cursor")) return true;
348
+ for (const status of ["200", "201"]) {
349
+ const media = resolveResponse(doc, operation.responses?.[status])?.content?.["application/json"];
350
+ if (!media?.schema) continue;
351
+ if (resolveSchema(doc, media.schema).properties?.next_cursor !== void 0) return true;
352
+ }
353
+ return false;
354
+ }
355
+ function parameterType(schema) {
356
+ for (const type of schemaTypes(schema)) {
357
+ if (type === "integer" || type === "number") return "number";
358
+ if (type === "boolean") return "boolean";
359
+ }
360
+ return "string";
361
+ }
362
+ function isPrimitiveSchema(schema) {
363
+ const types = schemaTypes(schema);
364
+ if (types.length === 0) return Array.isArray(schema.enum);
365
+ return types.some((type) => type === "string" || type === "number" || type === "integer" || type === "boolean") && !types.includes("array") && !types.includes("object");
366
+ }
367
+ function schemaTypes(schema) {
368
+ if (!schema) return [];
369
+ return (typeof schema.type === "string" ? [schema.type] : Array.isArray(schema.type) ? schema.type.filter((type) => typeof type === "string") : []).filter((type) => type !== "null");
370
+ }
371
+ function resolveOperationParameters(doc, pathItem, operation) {
372
+ const merged = /* @__PURE__ */ new Map();
373
+ for (const raw of [...pathItem.parameters ?? [], ...operation.parameters ?? []]) {
374
+ const parameter = resolveParameter(doc, raw);
375
+ if (!parameter.name || !parameter.in) continue;
376
+ merged.set(`${parameter.in}:${parameter.name}`, parameter);
377
+ }
378
+ return [...merged.values()];
379
+ }
380
+ function resolveParameter(doc, parameter) {
381
+ let current = parameter;
382
+ const seen = /* @__PURE__ */ new Set();
383
+ for (;;) {
384
+ const ref = refOf(current);
385
+ if (!ref) return current;
386
+ if (seen.has(ref)) throw new Error(`Circular $ref: ${ref}`);
387
+ seen.add(ref);
388
+ current = resolvePointer(doc, ref);
389
+ }
390
+ }
391
+ function resolveRequestBody(doc, raw) {
392
+ if (!raw) return;
393
+ const ref = refOf(raw);
394
+ return ref ? resolvePointer(doc, ref) : raw;
395
+ }
396
+ function resolveResponse(doc, raw) {
397
+ if (!raw) return;
398
+ const ref = refOf(raw);
399
+ return ref ? resolvePointer(doc, ref) : raw;
400
+ }
401
+ /** Dereferences a schema for `hngr schema`; a cycle keeps its `$ref`. */
402
+ function resolveSchema(doc, schema, seen = /* @__PURE__ */ new Set()) {
403
+ const ref = refOf(schema);
404
+ if (ref) {
405
+ if (seen.has(ref)) return { $ref: ref };
406
+ const next = new Set(seen);
407
+ next.add(ref);
408
+ return resolveSchema(doc, resolvePointer(doc, ref), next);
409
+ }
410
+ const { $ref: _ignored, ...rest } = schema;
411
+ const resolved = { ...rest };
412
+ if (schema.properties) resolved.properties = Object.fromEntries(Object.entries(schema.properties).map(([name, property]) => [name, resolveSchema(doc, property, seen)]));
413
+ if (schema.items) resolved.items = resolveSchema(doc, schema.items, seen);
414
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") resolved.additionalProperties = resolveSchema(doc, schema.additionalProperties, seen);
415
+ return resolved;
416
+ }
417
+ function refOf(value) {
418
+ if (value === null || typeof value !== "object") return;
419
+ const ref = value.$ref;
420
+ return typeof ref === "string" ? ref : void 0;
421
+ }
422
+ function resolvePointer(doc, ref) {
423
+ if (!ref.startsWith("#/")) throw new Error(`Only local $ref values are supported: ${ref}`);
424
+ let current = doc;
425
+ for (const raw of ref.slice(2).split("/")) {
426
+ const key = raw.replaceAll("~1", "/").replaceAll("~0", "~");
427
+ if (current === null || typeof current !== "object" || !(key in current)) throw new Error(`Unresolvable $ref: ${ref}`);
428
+ current = current[key];
429
+ }
430
+ return current;
431
+ }
432
+ /** camelCase, snake_case and hyphenated OpenAPI names all become kebab-case flags. */
433
+ function kebabCase(name) {
434
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replaceAll("_", "-").toLowerCase();
435
+ }
436
+ /** The command tree derived from the snapshot at module load. */
437
+ const COMMANDS = deriveCommands({
438
+ openapi: "3.1.0",
439
+ info: {
440
+ "title": "Hangar Management API",
441
+ "version": "v1",
442
+ "description": "Management API contract for Hangar's `/api/v1`. The contract describes HTTP\nshape, media types, status codes, headers, and authentication declarations\nonly; permissions, business validation, transactions, ZIP safety, and\nstorage consistency are implemented by the domain and infrastructure layers.\n"
443
+ },
444
+ servers: [{ "url": "/" }],
445
+ paths: {
446
+ "/api/v1/login": { "post": {
447
+ "operationId": "login",
448
+ "summary": "Sign in with a username and password and receive a Session cookie.",
449
+ "description": "Every credential failure returns the same 401 problem, whether the\nusername is unknown, the password is wrong, or the Principal is\ndisabled. Attempts are rate-limited per client address and username\nbefore the password is verified.\n",
450
+ "security": [],
451
+ "requestBody": {
452
+ "required": true,
453
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoginRequest" } } }
454
+ },
455
+ "responses": {
456
+ "204": {
457
+ "description": "Signed in. The Session lasts 12 hours and never slides.",
458
+ "headers": { "Set-Cookie": {
459
+ "description": "`hangar_session=<value>; Path=/; HttpOnly; Secure; SameSite=Lax`",
460
+ "schema": { "type": "string" }
461
+ } }
462
+ },
463
+ "400": { "$ref": "#/components/responses/Problem" },
464
+ "401": { "$ref": "#/components/responses/Problem" },
465
+ "403": { "$ref": "#/components/responses/Problem" },
466
+ "413": { "$ref": "#/components/responses/Problem" },
467
+ "429": { "$ref": "#/components/responses/Problem" },
468
+ "default": { "$ref": "#/components/responses/Problem" }
469
+ }
470
+ } },
471
+ "/api/v1/auth/methods": { "get": {
472
+ "operationId": "lookupAuthMethods",
473
+ "summary": "Report the login methods this deployment enables.",
474
+ "description": "Anonymous, so the console can choose which login controls to render\nbefore a Session exists. The password method has no switch and is\nalways enabled. `oidc.display_name` is the issuer host, or null when\nOIDC is disabled.\n",
475
+ "security": [],
476
+ "responses": {
477
+ "200": {
478
+ "description": "The enabled login methods.",
479
+ "headers": { "Cache-Control": {
480
+ "description": "Always `no-store`; deployment configuration may change the answer.",
481
+ "schema": { "type": "string" }
482
+ } },
483
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthMethods" } } }
484
+ },
485
+ "default": { "$ref": "#/components/responses/Problem" }
486
+ }
487
+ } },
488
+ "/api/v1/auth/oidc/login": { "get": {
489
+ "operationId": "startOidcLogin",
490
+ "summary": "Start an OpenID Connect login and redirect to the identity provider.",
491
+ "description": "Validates `return_to` to a local absolute path, falling back to `/`.\nAn already authenticated caller is redirected to `return_to` without\ncontacting the identity provider. Otherwise the response sets the\nsigned `hangar_oidc_state` cookie and redirects to the authorization\nendpoint. When OIDC is disabled the route answers 404; when discovery\nfails it redirects to `/login?oidc_error=provider_unavailable`.\n",
492
+ "security": [],
493
+ "parameters": [{
494
+ "name": "return_to",
495
+ "in": "query",
496
+ "required": false,
497
+ "description": "Local absolute path to return to after login; invalid values mean `/`.",
498
+ "schema": {
499
+ "type": "string",
500
+ "maxLength": 2048
501
+ }
502
+ }],
503
+ "responses": {
504
+ "302": {
505
+ "description": "Redirect to the identity provider, to `return_to` when already\nsigned in, or to `/login?oidc_error=<slug>` when discovery fails.\n",
506
+ "headers": {
507
+ "Location": {
508
+ "description": "The redirect target.",
509
+ "schema": { "type": "string" }
510
+ },
511
+ "Set-Cookie": {
512
+ "description": "`hangar_oidc_state=<value>; Path=/api/v1/auth/oidc; HttpOnly; SameSite=Lax; Max-Age=600`,\nwith `Secure` added only outside `development`; or an expired value that clears a stale one.\n",
513
+ "schema": { "type": "string" }
514
+ }
515
+ }
516
+ },
517
+ "404": { "$ref": "#/components/responses/Problem" },
518
+ "default": { "$ref": "#/components/responses/Problem" }
519
+ }
520
+ } },
521
+ "/api/v1/auth/oidc/callback": { "get": {
522
+ "operationId": "completeOidcLogin",
523
+ "summary": "Complete an OpenID Connect login and create a Session.",
524
+ "description": "Always answers 302 once OIDC is enabled, never a problem body: success\nredirects to `return_to` or `/` with a `hangar_session` cookie, and\nevery failure redirects to `/login?oidc_error=<slug>` and clears the\nstate cookie. Identity-provider error text is never returned. When\nOIDC is disabled the route answers 404.\n",
525
+ "security": [],
526
+ "parameters": [
527
+ {
528
+ "name": "code",
529
+ "in": "query",
530
+ "required": false,
531
+ "description": "The authorization code from the identity provider.",
532
+ "schema": { "type": "string" }
533
+ },
534
+ {
535
+ "name": "state",
536
+ "in": "query",
537
+ "required": false,
538
+ "description": "The state echoed by the identity provider; must match the state cookie.",
539
+ "schema": { "type": "string" }
540
+ },
541
+ {
542
+ "name": "error",
543
+ "in": "query",
544
+ "required": false,
545
+ "description": "The identity provider's error code, when it declined the request.",
546
+ "schema": { "type": "string" }
547
+ }
548
+ ],
549
+ "responses": {
550
+ "302": {
551
+ "description": "Redirect to `return_to` or `/` on success, or to\n`/login?oidc_error=<slug>` on failure.\n",
552
+ "headers": {
553
+ "Location": {
554
+ "description": "The redirect target; never carries identity-provider text.",
555
+ "schema": { "type": "string" }
556
+ },
557
+ "Set-Cookie": {
558
+ "description": "On success, `hangar_session=<value>` plus an expired\n`hangar_oidc_state`; on failure, only the expired state cookie.\n",
559
+ "schema": { "type": "string" }
560
+ }
561
+ }
562
+ },
563
+ "404": { "$ref": "#/components/responses/Problem" },
564
+ "default": { "$ref": "#/components/responses/Problem" }
565
+ }
566
+ } },
567
+ "/api/v1/logout": { "post": {
568
+ "operationId": "logout",
569
+ "summary": "Revoke the current Session and clear its cookie.",
570
+ "security": [{ "sessionCookie": [] }],
571
+ "responses": {
572
+ "204": {
573
+ "description": "Signed out.",
574
+ "headers": { "Set-Cookie": {
575
+ "description": "An expired `hangar_session` cookie that clears the browser value.",
576
+ "schema": { "type": "string" }
577
+ } }
578
+ },
579
+ "401": { "$ref": "#/components/responses/Problem" },
580
+ "403": { "$ref": "#/components/responses/Problem" },
581
+ "default": { "$ref": "#/components/responses/Problem" }
582
+ }
583
+ } },
584
+ "/api/v1/me": {
585
+ "get": {
586
+ "operationId": "lookupMe",
587
+ "summary": "Return the authenticated Principal.",
588
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
589
+ "responses": {
590
+ "200": {
591
+ "description": "The authenticated Principal.",
592
+ "headers": { "ETag": { "$ref": "#/components/headers/ETag" } },
593
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Principal" } } }
594
+ },
595
+ "400": { "$ref": "#/components/responses/Problem" },
596
+ "401": { "$ref": "#/components/responses/Problem" },
597
+ "default": { "$ref": "#/components/responses/Problem" }
598
+ }
599
+ },
600
+ "patch": {
601
+ "operationId": "updateMe",
602
+ "summary": "Update the authenticated Principal's profile.",
603
+ "description": "Omitted fields are left unchanged and an empty string clears a field.\nCredentials, the username and the platform role are not part of the\nprofile. Agents may change only their display name.\n\n`If-Match` must carry the current strong `ETag` of `GET /me`: a missing\nvalue answers 428 and a stale one 412.\n",
604
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
605
+ "parameters": [{ "$ref": "#/components/parameters/IfMatch" }],
606
+ "requestBody": {
607
+ "required": true,
608
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProfileUpdate" } } }
609
+ },
610
+ "responses": {
611
+ "200": {
612
+ "description": "The updated Principal.",
613
+ "headers": { "ETag": { "$ref": "#/components/headers/ETag" } },
614
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Principal" } } }
615
+ },
616
+ "400": { "$ref": "#/components/responses/Problem" },
617
+ "401": { "$ref": "#/components/responses/Problem" },
618
+ "403": { "$ref": "#/components/responses/Problem" },
619
+ "412": { "$ref": "#/components/responses/Problem" },
620
+ "413": { "$ref": "#/components/responses/Problem" },
621
+ "428": { "$ref": "#/components/responses/Problem" },
622
+ "default": { "$ref": "#/components/responses/Problem" }
623
+ }
624
+ }
625
+ },
626
+ "/api/v1/me/password": { "put": {
627
+ "operationId": "changeMyPassword",
628
+ "summary": "Replace the authenticated User's password.",
629
+ "description": "Requires the current password, which shares the login rate limit. A\nsuccessful change revokes every Session of the User, including the\ncurrent one, and leaves Access Tokens valid.\n",
630
+ "security": [{ "sessionCookie": [] }],
631
+ "requestBody": {
632
+ "required": true,
633
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PasswordChange" } } }
634
+ },
635
+ "responses": {
636
+ "204": {
637
+ "description": "Password changed and every Session revoked.",
638
+ "headers": { "Set-Cookie": {
639
+ "description": "An expired `hangar_session` cookie that clears the browser value.",
640
+ "schema": { "type": "string" }
641
+ } }
642
+ },
643
+ "400": { "$ref": "#/components/responses/Problem" },
644
+ "401": { "$ref": "#/components/responses/Problem" },
645
+ "403": { "$ref": "#/components/responses/Problem" },
646
+ "413": { "$ref": "#/components/responses/Problem" },
647
+ "429": { "$ref": "#/components/responses/Problem" },
648
+ "default": { "$ref": "#/components/responses/Problem" }
649
+ }
650
+ } },
651
+ "/api/v1/me/sessions": {
652
+ "get": {
653
+ "operationId": "listMySessions",
654
+ "summary": "List the authenticated User's active Sessions, newest first.",
655
+ "description": "Active Sessions are neither revoked nor expired. The list is ordered by\n`created_at` then `id`, both descending. A Session lives 12 hours, so\nthe list is returned whole rather than paginated.\n",
656
+ "security": [{ "sessionCookie": [] }],
657
+ "responses": {
658
+ "200": {
659
+ "description": "The active Sessions.",
660
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionList" } } }
661
+ },
662
+ "401": { "$ref": "#/components/responses/Problem" },
663
+ "default": { "$ref": "#/components/responses/Problem" }
664
+ }
665
+ },
666
+ "delete": {
667
+ "operationId": "revokeMySessions",
668
+ "summary": "Revoke every active Session of the authenticated User, including the current one.",
669
+ "security": [{ "sessionCookie": [] }],
670
+ "responses": {
671
+ "204": {
672
+ "description": "Every Session revoked.",
673
+ "headers": { "Set-Cookie": {
674
+ "description": "An expired `hangar_session` cookie that clears the browser value.",
675
+ "schema": { "type": "string" }
676
+ } }
677
+ },
678
+ "401": { "$ref": "#/components/responses/Problem" },
679
+ "403": { "$ref": "#/components/responses/Problem" },
680
+ "default": { "$ref": "#/components/responses/Problem" }
681
+ }
682
+ }
683
+ },
684
+ "/api/v1/me/sessions/{id}": { "delete": {
685
+ "operationId": "revokeMySession",
686
+ "summary": "Revoke one active Session of the authenticated User.",
687
+ "description": "Sessions of other Principals, and revoked or expired Sessions, answer\n404. Revoking the current Session also clears its cookie.\n",
688
+ "security": [{ "sessionCookie": [] }],
689
+ "parameters": [{
690
+ "name": "id",
691
+ "in": "path",
692
+ "required": true,
693
+ "schema": {
694
+ "type": "string",
695
+ "format": "uuid"
696
+ }
697
+ }],
698
+ "responses": {
699
+ "204": {
700
+ "description": "Session revoked.",
701
+ "headers": { "Set-Cookie": {
702
+ "description": "Present only when the current Session was revoked: an expired\n`hangar_session` cookie that clears the browser value.\n",
703
+ "schema": { "type": "string" }
704
+ } }
705
+ },
706
+ "400": { "$ref": "#/components/responses/Problem" },
707
+ "401": { "$ref": "#/components/responses/Problem" },
708
+ "403": { "$ref": "#/components/responses/Problem" },
709
+ "404": { "$ref": "#/components/responses/Problem" },
710
+ "default": { "$ref": "#/components/responses/Problem" }
711
+ }
712
+ } },
713
+ "/api/v1/me/tokens": {
714
+ "get": {
715
+ "operationId": "listMyAccessTokens",
716
+ "summary": "List the authenticated Principal's unrevoked Access Tokens.",
717
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
718
+ "responses": {
719
+ "200": { "$ref": "#/components/responses/AccessTokenList" },
720
+ "400": { "$ref": "#/components/responses/Problem" },
721
+ "401": { "$ref": "#/components/responses/Problem" },
722
+ "default": { "$ref": "#/components/responses/Problem" }
723
+ }
724
+ },
725
+ "post": {
726
+ "operationId": "createMyAccessToken",
727
+ "summary": "Create an Access Token for the authenticated Principal.",
728
+ "description": "A replay under `Idempotency-Key` answers 200 without the token value.",
729
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
730
+ "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
731
+ "requestBody": { "$ref": "#/components/requestBodies/CreateAccessToken" },
732
+ "responses": {
733
+ "200": { "$ref": "#/components/responses/ReplayedAccessToken" },
734
+ "201": { "$ref": "#/components/responses/CreatedAccessToken" },
735
+ "400": { "$ref": "#/components/responses/Problem" },
736
+ "401": { "$ref": "#/components/responses/Problem" },
737
+ "403": { "$ref": "#/components/responses/Problem" },
738
+ "409": { "$ref": "#/components/responses/Problem" },
739
+ "413": { "$ref": "#/components/responses/Problem" },
740
+ "default": { "$ref": "#/components/responses/Problem" }
741
+ }
742
+ }
743
+ },
744
+ "/api/v1/me/tokens/{token_id}": { "delete": {
745
+ "operationId": "revokeMyAccessToken",
746
+ "summary": "Revoke one of the authenticated Principal's Access Tokens.",
747
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
748
+ "parameters": [{ "$ref": "#/components/parameters/TokenID" }],
749
+ "responses": {
750
+ "204": { "description": "Revoked. The token stops authenticating immediately." },
751
+ "400": { "$ref": "#/components/responses/Problem" },
752
+ "401": { "$ref": "#/components/responses/Problem" },
753
+ "403": { "$ref": "#/components/responses/Problem" },
754
+ "404": { "$ref": "#/components/responses/Problem" },
755
+ "default": { "$ref": "#/components/responses/Problem" }
756
+ }
757
+ } },
758
+ "/api/v1/principals/{principal_id}/tokens": {
759
+ "parameters": [{ "$ref": "#/components/parameters/PrincipalID" }],
760
+ "get": {
761
+ "operationId": "listPrincipalAccessTokens",
762
+ "summary": "List a Principal's unrevoked Access Tokens.",
763
+ "description": "Allowed for the Principal itself, and for a Platform Admin when the Principal is an Agent.",
764
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
765
+ "responses": {
766
+ "200": { "$ref": "#/components/responses/AccessTokenList" },
767
+ "400": { "$ref": "#/components/responses/Problem" },
768
+ "401": { "$ref": "#/components/responses/Problem" },
769
+ "403": { "$ref": "#/components/responses/Problem" },
770
+ "404": { "$ref": "#/components/responses/Problem" },
771
+ "default": { "$ref": "#/components/responses/Problem" }
772
+ }
773
+ },
774
+ "post": {
775
+ "operationId": "createPrincipalAccessToken",
776
+ "summary": "Create an Access Token for a Principal.",
777
+ "description": "Allowed for the Principal itself, and for a Platform Admin when the\nPrincipal is an Agent. A replay under `Idempotency-Key` answers 200\nwithout the token value.\n",
778
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
779
+ "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
780
+ "requestBody": { "$ref": "#/components/requestBodies/CreateAccessToken" },
781
+ "responses": {
782
+ "200": { "$ref": "#/components/responses/ReplayedAccessToken" },
783
+ "201": { "$ref": "#/components/responses/CreatedAccessToken" },
784
+ "400": { "$ref": "#/components/responses/Problem" },
785
+ "401": { "$ref": "#/components/responses/Problem" },
786
+ "403": { "$ref": "#/components/responses/Problem" },
787
+ "404": { "$ref": "#/components/responses/Problem" },
788
+ "409": { "$ref": "#/components/responses/Problem" },
789
+ "413": { "$ref": "#/components/responses/Problem" },
790
+ "default": { "$ref": "#/components/responses/Problem" }
791
+ }
792
+ }
793
+ },
794
+ "/api/v1/principals/{principal_id}/tokens/{token_id}": {
795
+ "parameters": [{ "$ref": "#/components/parameters/PrincipalID" }, { "$ref": "#/components/parameters/TokenID" }],
796
+ "delete": {
797
+ "operationId": "revokePrincipalAccessToken",
798
+ "summary": "Revoke one of a Principal's Access Tokens.",
799
+ "description": "Allowed for the Principal itself, and for a Platform Admin when the Principal is an Agent.",
800
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
801
+ "responses": {
802
+ "204": { "description": "Revoked. The token stops authenticating immediately." },
803
+ "400": { "$ref": "#/components/responses/Problem" },
804
+ "401": { "$ref": "#/components/responses/Problem" },
805
+ "403": { "$ref": "#/components/responses/Problem" },
806
+ "404": { "$ref": "#/components/responses/Problem" },
807
+ "default": { "$ref": "#/components/responses/Problem" }
808
+ }
809
+ }
810
+ },
811
+ "/api/v1/principals": {
812
+ "get": {
813
+ "operationId": "listPrincipals",
814
+ "summary": "List the Platform's Principals, newest first.",
815
+ "description": "Only a Platform Admin may list Principals; every other authenticated\ncaller answers 403. Deleted Principals never appear; disabled\nPrincipals do, with `disabled_at` set, so the Platform Admin can\ninspect them before enabling them. Filters combine with AND. The list\nis ordered by `created_at` then `id`, both descending, and paged by\ncursor without a total.\n",
816
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
817
+ "parameters": [
818
+ {
819
+ "name": "kind",
820
+ "in": "query",
821
+ "schema": {
822
+ "type": "string",
823
+ "enum": ["user", "agent"]
824
+ }
825
+ },
826
+ {
827
+ "name": "q",
828
+ "in": "query",
829
+ "description": "Case-insensitive text found in the username or the display name. Empty means no filter.",
830
+ "schema": {
831
+ "type": "string",
832
+ "maxLength": 200
833
+ }
834
+ },
835
+ {
836
+ "name": "status",
837
+ "in": "query",
838
+ "schema": {
839
+ "type": "string",
840
+ "enum": ["enabled", "disabled"]
841
+ }
842
+ },
843
+ { "$ref": "#/components/parameters/Cursor" },
844
+ { "$ref": "#/components/parameters/Limit" }
845
+ ],
846
+ "responses": {
847
+ "200": {
848
+ "description": "One page of Principals.",
849
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PrincipalList" } } }
850
+ },
851
+ "400": { "$ref": "#/components/responses/Problem" },
852
+ "401": { "$ref": "#/components/responses/Problem" },
853
+ "403": { "$ref": "#/components/responses/Problem" },
854
+ "default": { "$ref": "#/components/responses/Problem" }
855
+ }
856
+ },
857
+ "post": {
858
+ "operationId": "createPrincipal",
859
+ "summary": "Create a User or an Agent together with its Namespace.",
860
+ "description": "Only a Platform Admin may create Principals. A User is created with\nits password credential; an Agent has neither a password nor a\nplatform role and authenticates with Access Tokens. A username held\nby a live Namespace, including `system`, returns 409.\n",
861
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
862
+ "requestBody": {
863
+ "required": true,
864
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreatePrincipalRequest" } } }
865
+ },
866
+ "responses": {
867
+ "201": {
868
+ "description": "The created Principal.",
869
+ "headers": { "Location": {
870
+ "description": "`/api/v1/principals/{id}`",
871
+ "schema": { "type": "string" }
872
+ } },
873
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Principal" } } }
874
+ },
875
+ "400": { "$ref": "#/components/responses/Problem" },
876
+ "401": { "$ref": "#/components/responses/Problem" },
877
+ "403": { "$ref": "#/components/responses/Problem" },
878
+ "409": { "$ref": "#/components/responses/Problem" },
879
+ "413": { "$ref": "#/components/responses/Problem" },
880
+ "default": { "$ref": "#/components/responses/Problem" }
881
+ }
882
+ }
883
+ },
884
+ "/api/v1/principals/{principal_id}": {
885
+ "parameters": [{ "$ref": "#/components/parameters/PrincipalID" }],
886
+ "get": {
887
+ "operationId": "lookupPrincipal",
888
+ "summary": "Return one Principal.",
889
+ "description": "Only a Platform Admin may look up Principals; every other authenticated\ncaller answers 403. Disabled Principals stay readable so the Platform\nAdmin can inspect them before enabling them. A deleted Principal\nanswers 404. The caller keeps using `/me` for its own profile.\n",
890
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
891
+ "responses": {
892
+ "200": {
893
+ "description": "The Principal.",
894
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Principal" } } }
895
+ },
896
+ "400": { "$ref": "#/components/responses/Problem" },
897
+ "401": { "$ref": "#/components/responses/Problem" },
898
+ "403": { "$ref": "#/components/responses/Problem" },
899
+ "404": { "$ref": "#/components/responses/Problem" },
900
+ "default": { "$ref": "#/components/responses/Problem" }
901
+ }
902
+ },
903
+ "delete": {
904
+ "operationId": "deletePrincipal",
905
+ "summary": "Irreversibly delete a Principal.",
906
+ "description": "Only a Platform Admin may delete Principals. Deletion revokes every\nSession and Access Token, releases the username and makes the\nPrincipal's Capabilities unavailable; nothing can restore it. Deleting\nthe last effective Platform Admin returns 409.\n",
907
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
908
+ "responses": {
909
+ "204": { "description": "Deleted." },
910
+ "400": { "$ref": "#/components/responses/Problem" },
911
+ "401": { "$ref": "#/components/responses/Problem" },
912
+ "403": { "$ref": "#/components/responses/Problem" },
913
+ "404": { "$ref": "#/components/responses/Problem" },
914
+ "409": { "$ref": "#/components/responses/Problem" },
915
+ "default": { "$ref": "#/components/responses/Problem" }
916
+ }
917
+ }
918
+ },
919
+ "/api/v1/principals/{principal_id}/disable": {
920
+ "parameters": [{ "$ref": "#/components/parameters/PrincipalID" }],
921
+ "post": {
922
+ "operationId": "disablePrincipal",
923
+ "summary": "Disable a Principal and revoke its credentials.",
924
+ "description": "Only a Platform Admin may disable Principals. Every Session and Access\nToken of the Principal is revoked and authentication is refused until\nit is enabled again. Disabling a disabled Principal changes nothing.\nDisabling the last effective Platform Admin returns 409.\n",
925
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
926
+ "responses": {
927
+ "204": { "description": "Disabled." },
928
+ "400": { "$ref": "#/components/responses/Problem" },
929
+ "401": { "$ref": "#/components/responses/Problem" },
930
+ "403": { "$ref": "#/components/responses/Problem" },
931
+ "404": { "$ref": "#/components/responses/Problem" },
932
+ "409": { "$ref": "#/components/responses/Problem" },
933
+ "default": { "$ref": "#/components/responses/Problem" }
934
+ }
935
+ }
936
+ },
937
+ "/api/v1/principals/{principal_id}/enable": {
938
+ "parameters": [{ "$ref": "#/components/parameters/PrincipalID" }],
939
+ "post": {
940
+ "operationId": "enablePrincipal",
941
+ "summary": "Enable a disabled Principal.",
942
+ "description": "Only a Platform Admin may enable Principals. Credentials revoked by\ndisabling stay revoked: the Principal signs in or is issued a token\nagain. Enabling an enabled Principal changes nothing.\n",
943
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
944
+ "responses": {
945
+ "204": { "description": "Enabled." },
946
+ "400": { "$ref": "#/components/responses/Problem" },
947
+ "401": { "$ref": "#/components/responses/Problem" },
948
+ "403": { "$ref": "#/components/responses/Problem" },
949
+ "404": { "$ref": "#/components/responses/Problem" },
950
+ "default": { "$ref": "#/components/responses/Problem" }
951
+ }
952
+ }
953
+ },
954
+ "/api/v1/principals/{principal_id}/password": {
955
+ "parameters": [{ "$ref": "#/components/parameters/PrincipalID" }],
956
+ "put": {
957
+ "operationId": "resetPrincipalPassword",
958
+ "summary": "Reset a User's password.",
959
+ "description": "Only a Platform Admin may reset passwords, and only Users have one. The\nreset revokes every Session of the User and leaves Access Tokens\nvalid.\n",
960
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
961
+ "requestBody": {
962
+ "required": true,
963
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PasswordReset" } } }
964
+ },
965
+ "responses": {
966
+ "204": { "description": "Password reset and every Session of the User revoked." },
967
+ "400": { "$ref": "#/components/responses/Problem" },
968
+ "401": { "$ref": "#/components/responses/Problem" },
969
+ "403": { "$ref": "#/components/responses/Problem" },
970
+ "404": { "$ref": "#/components/responses/Problem" },
971
+ "413": { "$ref": "#/components/responses/Problem" },
972
+ "default": { "$ref": "#/components/responses/Problem" }
973
+ }
974
+ }
975
+ },
976
+ "/api/v1/principals/{principal_id}/role": {
977
+ "parameters": [{ "$ref": "#/components/parameters/PrincipalID" }],
978
+ "put": {
979
+ "operationId": "updatePrincipalRole",
980
+ "summary": "Set a User's platform role.",
981
+ "description": "Only a Platform Admin may change platform roles, and only Users have\none. Demoting the last effective Platform Admin returns 409.\n",
982
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
983
+ "requestBody": {
984
+ "required": true,
985
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RoleUpdate" } } }
986
+ },
987
+ "responses": {
988
+ "204": { "description": "Role set." },
989
+ "400": { "$ref": "#/components/responses/Problem" },
990
+ "401": { "$ref": "#/components/responses/Problem" },
991
+ "403": { "$ref": "#/components/responses/Problem" },
992
+ "404": { "$ref": "#/components/responses/Problem" },
993
+ "409": { "$ref": "#/components/responses/Problem" },
994
+ "413": { "$ref": "#/components/responses/Problem" },
995
+ "default": { "$ref": "#/components/responses/Problem" }
996
+ }
997
+ }
998
+ },
999
+ "/api/v1/capabilities": {
1000
+ "get": {
1001
+ "operationId": "listCapabilities",
1002
+ "summary": "List the Capabilities the caller can read, newest first.",
1003
+ "description": "Reads follow the rules of `lookupCapability`. Filters combine with\nAND. The list is ordered by `created_at` then `id`, both descending,\nand paged by cursor without a total.\n",
1004
+ "security": [
1005
+ {},
1006
+ { "sessionCookie": [] },
1007
+ { "bearerToken": [] }
1008
+ ],
1009
+ "parameters": [
1010
+ {
1011
+ "name": "kind",
1012
+ "in": "query",
1013
+ "schema": {
1014
+ "type": "string",
1015
+ "enum": ["skill", "mcp"]
1016
+ }
1017
+ },
1018
+ {
1019
+ "name": "namespace",
1020
+ "in": "query",
1021
+ "description": "The name of the owning Namespace.",
1022
+ "schema": { "$ref": "#/components/schemas/Name" }
1023
+ },
1024
+ {
1025
+ "name": "visibility",
1026
+ "in": "query",
1027
+ "schema": { "$ref": "#/components/schemas/Visibility" }
1028
+ },
1029
+ {
1030
+ "name": "q",
1031
+ "in": "query",
1032
+ "description": "Case-insensitive text found in the name or the description. Empty means no filter.",
1033
+ "schema": {
1034
+ "type": "string",
1035
+ "maxLength": 200
1036
+ }
1037
+ },
1038
+ {
1039
+ "name": "tag",
1040
+ "in": "query",
1041
+ "description": "One or more normalized tags separated by commas. A Capability\nmatches when at least one of the tags is in its set; tag matches\ncombine with every other filter using AND. Tags cannot contain\ncommas, so the separator is unambiguous.\n",
1042
+ "schema": {
1043
+ "type": "string",
1044
+ "maxLength": 263
1045
+ }
1046
+ },
1047
+ { "$ref": "#/components/parameters/Cursor" },
1048
+ { "$ref": "#/components/parameters/Limit" }
1049
+ ],
1050
+ "responses": {
1051
+ "200": {
1052
+ "description": "One page of Capabilities.",
1053
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CapabilityList" } } }
1054
+ },
1055
+ "400": { "$ref": "#/components/responses/Problem" },
1056
+ "default": { "$ref": "#/components/responses/Problem" }
1057
+ }
1058
+ },
1059
+ "post": {
1060
+ "operationId": "createCapability",
1061
+ "summary": "Create a Capability together with its first Version.",
1062
+ "description": "An `application/json` body creates an MCP Capability; a\n`multipart/form-data` body creates a Skill Capability from its\nCapability Package. The Capability, its first Version and the Audit\nEvent are committed in one transaction. A rejected package returns 400\nwith a `detail` that names what to fix. The caller must manage the\ntarget Namespace: a Platform Admin for system, a member User of the Team for a Team Namespace, and\nthe owner for a principal Namespace. A live Capability with the same\nkind and name in the Namespace returns 409.\n",
1063
+ "security": [{ "sessionCookie": [] }],
1064
+ "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
1065
+ "requestBody": {
1066
+ "required": true,
1067
+ "content": {
1068
+ "application/json": { "schema": { "$ref": "#/components/schemas/CreateMCPCapabilityRequest" } },
1069
+ "multipart/form-data": {
1070
+ "schema": { "$ref": "#/components/schemas/CreateSkillCapabilityRequest" },
1071
+ "encoding": { "package": { "contentType": "application/zip" } }
1072
+ }
1073
+ }
1074
+ },
1075
+ "responses": {
1076
+ "201": {
1077
+ "description": "The created Capability.",
1078
+ "headers": { "Location": {
1079
+ "description": "`/api/v1/capabilities/{id}`",
1080
+ "schema": { "type": "string" }
1081
+ } },
1082
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Capability" } } }
1083
+ },
1084
+ "400": { "$ref": "#/components/responses/Problem" },
1085
+ "401": { "$ref": "#/components/responses/Problem" },
1086
+ "403": { "$ref": "#/components/responses/Problem" },
1087
+ "404": { "$ref": "#/components/responses/Problem" },
1088
+ "409": { "$ref": "#/components/responses/Problem" },
1089
+ "413": { "$ref": "#/components/responses/Problem" },
1090
+ "default": { "$ref": "#/components/responses/Problem" }
1091
+ }
1092
+ }
1093
+ },
1094
+ "/api/v1/capabilities/{capability_id}": {
1095
+ "parameters": [{ "$ref": "#/components/parameters/CapabilityID" }],
1096
+ "get": {
1097
+ "operationId": "lookupCapability",
1098
+ "summary": "Return a Capability the caller can read.",
1099
+ "description": "Anonymous callers read public Capabilities, authenticated Principals\nalso read internal ones, and the managers of the owning Namespace and\nPlatform Admins also read private ones. A Capability that is deleted,\nunavailable, owned by a disabled Team or Principal, or not readable by\nthe caller answers 404.\n\nThe `ETag` header covers management metadata only: it excludes\n`download_count` and any caller-relative field, so a download never\ninvalidates an editor's `If-Match`.\n",
1100
+ "security": [
1101
+ {},
1102
+ { "sessionCookie": [] },
1103
+ { "bearerToken": [] }
1104
+ ],
1105
+ "responses": {
1106
+ "200": {
1107
+ "description": "The Capability.",
1108
+ "headers": { "ETag": { "$ref": "#/components/headers/ETag" } },
1109
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Capability" } } }
1110
+ },
1111
+ "400": { "$ref": "#/components/responses/Problem" },
1112
+ "404": { "$ref": "#/components/responses/Problem" },
1113
+ "default": { "$ref": "#/components/responses/Problem" }
1114
+ }
1115
+ },
1116
+ "patch": {
1117
+ "operationId": "updateCapability",
1118
+ "summary": "Change a Capability's description or visibility.",
1119
+ "description": "Omitted fields are left unchanged. The change applies to every Version\nat once and publishes none. `kind`, `namespace` and `name` are\nimmutable, so sending any of them returns 400.\n\nThe caller must manage the Capability's Namespace, under the same\nrules as `createCapability`. A deleted or unavailable Capability, one\nin an unusable Namespace, and a private one the caller does not manage\nreturn 404; those checks come before the precondition. `If-Match`\nmust then carry the current strong `ETag` of `lookupCapability`: a\nmissing value answers 428 and a stale one 412. That `ETag` covers\nmanagement metadata only, excluding `download_count` and\ncaller-relative fields, so downloads do not cause spurious 412s.\n",
1120
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
1121
+ "parameters": [{ "$ref": "#/components/parameters/IfMatch" }],
1122
+ "requestBody": {
1123
+ "required": true,
1124
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CapabilityUpdate" } } }
1125
+ },
1126
+ "responses": {
1127
+ "200": {
1128
+ "description": "The updated Capability.",
1129
+ "headers": { "ETag": { "$ref": "#/components/headers/ETag" } },
1130
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Capability" } } }
1131
+ },
1132
+ "400": { "$ref": "#/components/responses/Problem" },
1133
+ "401": { "$ref": "#/components/responses/Problem" },
1134
+ "403": { "$ref": "#/components/responses/Problem" },
1135
+ "404": { "$ref": "#/components/responses/Problem" },
1136
+ "412": { "$ref": "#/components/responses/Problem" },
1137
+ "413": { "$ref": "#/components/responses/Problem" },
1138
+ "428": { "$ref": "#/components/responses/Problem" },
1139
+ "default": { "$ref": "#/components/responses/Problem" }
1140
+ }
1141
+ },
1142
+ "delete": {
1143
+ "operationId": "deleteCapability",
1144
+ "summary": "Soft-delete a Capability.",
1145
+ "description": "The Capability and all its Versions and Content disappear from every\nbusiness API, for Platform Admins too, and its Path becomes free for a\nnew, unrelated Capability. Deletion cannot be undone through the API.\nThe caller must manage the Capability's Namespace; the lifecycle and\nvisibility rules of `updateCapability` decide between 403 and 404.\n",
1146
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
1147
+ "responses": {
1148
+ "204": { "description": "The Capability is deleted." },
1149
+ "400": { "$ref": "#/components/responses/Problem" },
1150
+ "401": { "$ref": "#/components/responses/Problem" },
1151
+ "403": { "$ref": "#/components/responses/Problem" },
1152
+ "404": { "$ref": "#/components/responses/Problem" },
1153
+ "default": { "$ref": "#/components/responses/Problem" }
1154
+ }
1155
+ }
1156
+ },
1157
+ "/api/v1/capabilities/{capability_id}/versions": {
1158
+ "parameters": [{ "$ref": "#/components/parameters/CapabilityID" }],
1159
+ "get": {
1160
+ "operationId": "listCapabilityVersions",
1161
+ "summary": "List a readable Capability's Versions, newest first.",
1162
+ "description": "Versions follow the visibility and lifecycle of their Capability: one\nthe caller cannot look up answers 404. The list is ordered by\n`created_at` then `id`, both descending, and paged by cursor without a\ntotal. READMEs are left to `lookupCapabilityVersion`.\n",
1163
+ "security": [
1164
+ {},
1165
+ { "sessionCookie": [] },
1166
+ { "bearerToken": [] }
1167
+ ],
1168
+ "parameters": [{ "$ref": "#/components/parameters/Cursor" }, { "$ref": "#/components/parameters/Limit" }],
1169
+ "responses": {
1170
+ "200": {
1171
+ "description": "One page of Versions.",
1172
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VersionList" } } }
1173
+ },
1174
+ "400": { "$ref": "#/components/responses/Problem" },
1175
+ "404": { "$ref": "#/components/responses/Problem" },
1176
+ "default": { "$ref": "#/components/responses/Problem" }
1177
+ }
1178
+ },
1179
+ "post": {
1180
+ "operationId": "publishCapabilityVersion",
1181
+ "summary": "Publish a new Version of a Capability and make it current.",
1182
+ "description": "The media type must match the Capability's kind: `application/json`\nfor MCP and `multipart/form-data` for Skill; the other one returns\n400. The Version, the new current version and the Audit Event are\ncommitted in one transaction, and concurrent publishes each receive\ntheir own CalVer. The caller must manage the Capability's Namespace,\nunder the same rules as `createCapability`. A deleted or unavailable\nCapability, one in an unusable Namespace, and a private one the\ncaller does not manage return 404.\n",
1183
+ "security": [{ "sessionCookie": [] }],
1184
+ "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
1185
+ "requestBody": {
1186
+ "required": true,
1187
+ "content": {
1188
+ "application/json": { "schema": { "$ref": "#/components/schemas/PublishMCPVersionRequest" } },
1189
+ "multipart/form-data": {
1190
+ "schema": { "$ref": "#/components/schemas/PublishSkillVersionRequest" },
1191
+ "encoding": { "package": { "contentType": "application/zip" } }
1192
+ }
1193
+ }
1194
+ },
1195
+ "responses": {
1196
+ "201": {
1197
+ "description": "The Capability with its new current version.",
1198
+ "headers": { "Location": {
1199
+ "description": "`/api/v1/capabilities/{capability_id}/versions/{version}`",
1200
+ "schema": { "type": "string" }
1201
+ } },
1202
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Capability" } } }
1203
+ },
1204
+ "400": { "$ref": "#/components/responses/Problem" },
1205
+ "401": { "$ref": "#/components/responses/Problem" },
1206
+ "403": { "$ref": "#/components/responses/Problem" },
1207
+ "404": { "$ref": "#/components/responses/Problem" },
1208
+ "409": { "$ref": "#/components/responses/Problem" },
1209
+ "413": { "$ref": "#/components/responses/Problem" },
1210
+ "default": { "$ref": "#/components/responses/Problem" }
1211
+ }
1212
+ }
1213
+ },
1214
+ "/api/v1/capabilities/{capability_id}/versions/{version}": {
1215
+ "parameters": [{ "$ref": "#/components/parameters/CapabilityID" }, {
1216
+ "name": "version",
1217
+ "in": "path",
1218
+ "required": true,
1219
+ "schema": { "$ref": "#/components/schemas/CalVer" }
1220
+ }],
1221
+ "get": {
1222
+ "operationId": "lookupCapabilityVersion",
1223
+ "summary": "Return one Version of a readable Capability by its CalVer.",
1224
+ "description": "A Capability the caller cannot look up, or a CalVer it never\npublished, answers 404.\n",
1225
+ "security": [
1226
+ {},
1227
+ { "sessionCookie": [] },
1228
+ { "bearerToken": [] }
1229
+ ],
1230
+ "responses": {
1231
+ "200": {
1232
+ "description": "The Version.",
1233
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Version" } } }
1234
+ },
1235
+ "400": { "$ref": "#/components/responses/Problem" },
1236
+ "404": { "$ref": "#/components/responses/Problem" },
1237
+ "default": { "$ref": "#/components/responses/Problem" }
1238
+ }
1239
+ }
1240
+ },
1241
+ "/api/v1/capabilities/{capability_id}/versions/{version}/content": { "get": {
1242
+ "operationId": "lookupCapabilityContent",
1243
+ "summary": "Return the Content of one Version of a Capability.",
1244
+ "description": "An MCP Version's Content is its config in the RFC 8785 canonical form,\nexcept that integers keep their original digits, so values beyond\n±2^53 are not rounded. A Skill Version's Content is its package, the\nuploaded ZIP byte for byte, as `application/zip`. The strong `ETag` is\nthe quoted hex sha256 of the returned bytes. Range requests are not\nsupported; a `Range` header is ignored. A Skill package missing from\nstorage fails with 500 before any content is sent.\n\nAnonymous callers read public Capabilities, authenticated Principals\nalso internal ones, and private ones are read by the managers of the\nowning Namespace. A Capability the caller cannot read, a deleted or\nunavailable Capability, or one whose Namespace, Team or Principal is\ndisabled or deleted answers 404. These checks run before `If-None-Match`.\n",
1245
+ "security": [
1246
+ {},
1247
+ { "sessionCookie": [] },
1248
+ { "bearerToken": [] }
1249
+ ],
1250
+ "parameters": [
1251
+ { "$ref": "#/components/parameters/CapabilityID" },
1252
+ {
1253
+ "name": "version",
1254
+ "in": "path",
1255
+ "required": true,
1256
+ "schema": { "$ref": "#/components/schemas/CalVer" }
1257
+ },
1258
+ {
1259
+ "name": "If-None-Match",
1260
+ "in": "header",
1261
+ "description": "Entity tags the caller holds. A match, or `*`, answers 304.",
1262
+ "required": false,
1263
+ "schema": { "type": "string" }
1264
+ }
1265
+ ],
1266
+ "responses": {
1267
+ "200": {
1268
+ "description": "The Content.",
1269
+ "headers": {
1270
+ "ETag": { "$ref": "#/components/headers/ETag" },
1271
+ "Cache-Control": { "$ref": "#/components/headers/ContentCacheControl" }
1272
+ },
1273
+ "content": {
1274
+ "application/json": { "schema": { "$ref": "#/components/schemas/MCPConfig" } },
1275
+ "application/zip": { "schema": { "type": "string" } }
1276
+ }
1277
+ },
1278
+ "304": {
1279
+ "description": "The caller's copy is current. No body.",
1280
+ "headers": { "ETag": { "$ref": "#/components/headers/ETag" } }
1281
+ },
1282
+ "400": { "$ref": "#/components/responses/Problem" },
1283
+ "401": { "$ref": "#/components/responses/Problem" },
1284
+ "404": { "$ref": "#/components/responses/Problem" },
1285
+ "default": { "$ref": "#/components/responses/Problem" }
1286
+ }
1287
+ } },
1288
+ "/api/v1/tags": { "get": {
1289
+ "operationId": "listTags",
1290
+ "summary": "List the tags of the Capabilities the caller can read, with counts.",
1291
+ "description": "Counts only the live Capabilities the caller may read, under the same\nvisibility rules as `lookupCapability`; an anonymous caller counts\npublic Capabilities only. Tags are normalized, then ordered by count\ndescending and name ascending. The response is not paginated: tag\ncounts change with edits, so `truncated` reports that more tags exist\nthan `limit` instead of offering a cursor.\n",
1292
+ "security": [
1293
+ {},
1294
+ { "sessionCookie": [] },
1295
+ { "bearerToken": [] }
1296
+ ],
1297
+ "parameters": [{
1298
+ "name": "q",
1299
+ "in": "query",
1300
+ "description": "Case-insensitive text found in the tag name. Empty means no filter.",
1301
+ "schema": {
1302
+ "type": "string",
1303
+ "maxLength": 200
1304
+ }
1305
+ }, {
1306
+ "name": "limit",
1307
+ "in": "query",
1308
+ "description": "The most tags the response holds.",
1309
+ "schema": {
1310
+ "type": "integer",
1311
+ "minimum": 1,
1312
+ "maximum": 200,
1313
+ "default": 100
1314
+ }
1315
+ }],
1316
+ "responses": {
1317
+ "200": {
1318
+ "description": "The readable tags and their counts.",
1319
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TagList" } } }
1320
+ },
1321
+ "400": { "$ref": "#/components/responses/Problem" },
1322
+ "default": { "$ref": "#/components/responses/Problem" }
1323
+ }
1324
+ } },
1325
+ "/api/v1/audit-events": { "get": {
1326
+ "operationId": "listAuditEvents",
1327
+ "summary": "List the Platform's Audit Events, newest first.",
1328
+ "description": "Only a Platform Admin may read Audit Events; anonymous callers answer\n401 and every other authenticated caller 403. Filters combine with\nAND. `from` is inclusive and `to` is exclusive, both RFC 3339; a\n`from` later than `to` answers 400. An event whose `actor` is null is\na system event, and a deleted actor's username and display name are\nstill returned, so historical facts keep their attribution. Reading\nAudit Events writes no Audit Event. The list is ordered by\n`created_at` then `id`, both descending, and paged by cursor without\na total.\n",
1329
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
1330
+ "parameters": [
1331
+ {
1332
+ "name": "actor_principal_id",
1333
+ "in": "query",
1334
+ "schema": {
1335
+ "type": "string",
1336
+ "format": "uuid"
1337
+ }
1338
+ },
1339
+ {
1340
+ "name": "action",
1341
+ "in": "query",
1342
+ "description": "One action from the controlled vocabulary.",
1343
+ "schema": {
1344
+ "type": "string",
1345
+ "enum": [
1346
+ "access_token.created",
1347
+ "access_token.revoked",
1348
+ "capability.created",
1349
+ "capability.updated",
1350
+ "capability.deleted",
1351
+ "capability.version_published",
1352
+ "principal.created",
1353
+ "principal.disabled",
1354
+ "principal.enabled",
1355
+ "principal.deleted",
1356
+ "principal.profile_updated",
1357
+ "principal.password_changed",
1358
+ "principal.password_reset",
1359
+ "principal.role_changed",
1360
+ "oidc.identity_linked",
1361
+ "oidc.profile_synced",
1362
+ "session.revoked",
1363
+ "team.created",
1364
+ "team.disabled",
1365
+ "team.enabled",
1366
+ "team.deleted",
1367
+ "team.member_added",
1368
+ "team.member_role_changed",
1369
+ "team.member_removed"
1370
+ ]
1371
+ }
1372
+ },
1373
+ {
1374
+ "name": "resource_type",
1375
+ "in": "query",
1376
+ "schema": {
1377
+ "type": "string",
1378
+ "enum": [
1379
+ "access_token",
1380
+ "capability",
1381
+ "principal",
1382
+ "session",
1383
+ "team"
1384
+ ]
1385
+ }
1386
+ },
1387
+ {
1388
+ "name": "resource_id",
1389
+ "in": "query",
1390
+ "schema": {
1391
+ "type": "string",
1392
+ "format": "uuid"
1393
+ }
1394
+ },
1395
+ {
1396
+ "name": "from",
1397
+ "in": "query",
1398
+ "description": "The inclusive start of the time range, as RFC 3339.",
1399
+ "schema": {
1400
+ "type": "string",
1401
+ "format": "date-time"
1402
+ }
1403
+ },
1404
+ {
1405
+ "name": "to",
1406
+ "in": "query",
1407
+ "description": "The exclusive end of the time range, as RFC 3339.",
1408
+ "schema": {
1409
+ "type": "string",
1410
+ "format": "date-time"
1411
+ }
1412
+ },
1413
+ { "$ref": "#/components/parameters/Cursor" },
1414
+ { "$ref": "#/components/parameters/Limit" }
1415
+ ],
1416
+ "responses": {
1417
+ "200": {
1418
+ "description": "One page of Audit Events.",
1419
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuditEventList" } } }
1420
+ },
1421
+ "400": { "$ref": "#/components/responses/Problem" },
1422
+ "401": { "$ref": "#/components/responses/Problem" },
1423
+ "403": { "$ref": "#/components/responses/Problem" },
1424
+ "default": { "$ref": "#/components/responses/Problem" }
1425
+ }
1426
+ } },
1427
+ "/api/v1/namespaces": { "get": {
1428
+ "operationId": "listNamespaces",
1429
+ "summary": "List the Namespaces the caller can create Capabilities in, newest first.",
1430
+ "description": "A Platform Admin uses every Namespace; other Users use their own and\nthose of their Teams; Agents use only their own. Deleted Namespaces and\nthose of disabled Teams or Principals are never listed. The list is\nordered by `created_at` then `id`, both descending, and paged by cursor\nwithout a total.\n",
1431
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
1432
+ "parameters": [{ "$ref": "#/components/parameters/Cursor" }, { "$ref": "#/components/parameters/Limit" }],
1433
+ "responses": {
1434
+ "200": {
1435
+ "description": "One page of Namespaces.",
1436
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NamespaceList" } } }
1437
+ },
1438
+ "400": { "$ref": "#/components/responses/Problem" },
1439
+ "401": { "$ref": "#/components/responses/Problem" },
1440
+ "default": { "$ref": "#/components/responses/Problem" }
1441
+ }
1442
+ } },
1443
+ "/api/v1/teams": {
1444
+ "get": {
1445
+ "operationId": "listTeams",
1446
+ "summary": "List the live Teams the caller may see, newest first.",
1447
+ "description": "A Platform Admin sees every live Team; every other Principal sees only\nthe Teams it is a member of, including as an Agent. Disabled Teams stay\nvisible; deleted Teams never appear. `q` matches the Team's name or\ndisplay name case-insensitively, and `status` narrows the list to\nenabled or disabled Teams. The list is ordered by `created_at` then\n`id`, both descending, and paged by cursor without a total.\n",
1448
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
1449
+ "parameters": [
1450
+ {
1451
+ "name": "q",
1452
+ "in": "query",
1453
+ "description": "A case-insensitive substring of the name or display name. Empty does not filter.",
1454
+ "schema": {
1455
+ "type": "string",
1456
+ "maxLength": 200
1457
+ }
1458
+ },
1459
+ {
1460
+ "name": "status",
1461
+ "in": "query",
1462
+ "description": "Only Teams in this lifecycle state; omit for both.",
1463
+ "schema": {
1464
+ "type": "string",
1465
+ "enum": ["enabled", "disabled"]
1466
+ }
1467
+ },
1468
+ { "$ref": "#/components/parameters/Cursor" },
1469
+ { "$ref": "#/components/parameters/Limit" }
1470
+ ],
1471
+ "responses": {
1472
+ "200": {
1473
+ "description": "One page of Teams.",
1474
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TeamList" } } }
1475
+ },
1476
+ "400": { "$ref": "#/components/responses/Problem" },
1477
+ "401": { "$ref": "#/components/responses/Problem" },
1478
+ "default": { "$ref": "#/components/responses/Problem" }
1479
+ }
1480
+ },
1481
+ "post": {
1482
+ "operationId": "createTeam",
1483
+ "summary": "Create a Team with its Namespace and first Team Admins.",
1484
+ "description": "Only a Platform Admin may create a Team. The Team, its Namespace and\nthe admin memberships are committed in one transaction. Every listed\nadmin must be an enabled User: an unknown or deleted Principal answers\n404, an Agent 400 and a disabled User 409. A Namespace name already\nheld by a live Namespace, or `system`, answers 409.\n",
1485
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
1486
+ "requestBody": {
1487
+ "required": true,
1488
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateTeamRequest" } } }
1489
+ },
1490
+ "responses": {
1491
+ "201": {
1492
+ "description": "The created Team.",
1493
+ "headers": { "Location": {
1494
+ "description": "`/api/v1/teams/{id}`",
1495
+ "schema": { "type": "string" }
1496
+ } },
1497
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Team" } } }
1498
+ },
1499
+ "400": { "$ref": "#/components/responses/Problem" },
1500
+ "401": { "$ref": "#/components/responses/Problem" },
1501
+ "403": { "$ref": "#/components/responses/Problem" },
1502
+ "404": { "$ref": "#/components/responses/Problem" },
1503
+ "409": { "$ref": "#/components/responses/Problem" },
1504
+ "413": { "$ref": "#/components/responses/Problem" },
1505
+ "default": { "$ref": "#/components/responses/Problem" }
1506
+ }
1507
+ }
1508
+ },
1509
+ "/api/v1/teams/{team_id}": {
1510
+ "parameters": [{ "$ref": "#/components/parameters/TeamID" }],
1511
+ "get": {
1512
+ "operationId": "lookupTeam",
1513
+ "summary": "Read a live Team the caller may see.",
1514
+ "description": "A Platform Admin may read any live Team; every other Principal only a\nTeam it is a member of. A disabled Team remains readable. A deleted\nTeam, or one the caller cannot see, answers 404 without revealing\nwhether it exists.\n",
1515
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
1516
+ "responses": {
1517
+ "200": {
1518
+ "description": "The Team.",
1519
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Team" } } }
1520
+ },
1521
+ "400": { "$ref": "#/components/responses/Problem" },
1522
+ "401": { "$ref": "#/components/responses/Problem" },
1523
+ "404": { "$ref": "#/components/responses/Problem" },
1524
+ "default": { "$ref": "#/components/responses/Problem" }
1525
+ }
1526
+ },
1527
+ "delete": {
1528
+ "operationId": "deleteTeam",
1529
+ "summary": "Delete a Team irreversibly.",
1530
+ "description": "Allowed for an effective Team Admin of an enabled Team and for a\nPlatform Admin; a Team Admin of a disabled Team answers 409. The Team\nand its Namespace are soft-deleted, the Namespace name is released and\nthe Team's Capabilities become unavailable. A deleted Team answers 404\neverywhere.\n",
1531
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
1532
+ "responses": {
1533
+ "204": { "description": "The Team is deleted." },
1534
+ "400": { "$ref": "#/components/responses/Problem" },
1535
+ "401": { "$ref": "#/components/responses/Problem" },
1536
+ "403": { "$ref": "#/components/responses/Problem" },
1537
+ "404": { "$ref": "#/components/responses/Problem" },
1538
+ "409": { "$ref": "#/components/responses/Problem" },
1539
+ "default": { "$ref": "#/components/responses/Problem" }
1540
+ }
1541
+ }
1542
+ },
1543
+ "/api/v1/teams/{team_id}/members": {
1544
+ "parameters": [{ "$ref": "#/components/parameters/TeamID" }],
1545
+ "get": {
1546
+ "operationId": "listTeamMembers",
1547
+ "summary": "List a Team's roster, newest membership first.",
1548
+ "description": "Allowed for any member of the Team — a User or an Agent, a Team Admin\nor a member — and for any Platform Admin. A caller that is not a member\nanswers 404 without revealing whether the Team exists, as does a\ndeleted Team. A disabled Team stays readable: disabling freezes writes,\nnot reads. Deleted Principals are excluded; disabled Principals stay\nlisted with `disabled_at`. `role` narrows the roster. The list is\nordered by the membership's `created_at` then `principal_id`, both\ndescending, and paged by cursor without a total.\n",
1549
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
1550
+ "parameters": [
1551
+ {
1552
+ "name": "role",
1553
+ "in": "query",
1554
+ "description": "Only memberships with this Team role; omit for both.",
1555
+ "schema": {
1556
+ "type": "string",
1557
+ "enum": ["admin", "member"]
1558
+ }
1559
+ },
1560
+ { "$ref": "#/components/parameters/Cursor" },
1561
+ { "$ref": "#/components/parameters/Limit" }
1562
+ ],
1563
+ "responses": {
1564
+ "200": {
1565
+ "description": "One page of Team members.",
1566
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TeamMemberList" } } }
1567
+ },
1568
+ "400": { "$ref": "#/components/responses/Problem" },
1569
+ "401": { "$ref": "#/components/responses/Problem" },
1570
+ "404": { "$ref": "#/components/responses/Problem" },
1571
+ "default": { "$ref": "#/components/responses/Problem" }
1572
+ }
1573
+ }
1574
+ },
1575
+ "/api/v1/teams/{team_id}/member-candidates": {
1576
+ "parameters": [{ "$ref": "#/components/parameters/TeamID" }],
1577
+ "get": {
1578
+ "operationId": "listTeamMemberCandidates",
1579
+ "summary": "Search for Principals that could join a Team.",
1580
+ "description": "Allowed for an effective Team Admin and for a Platform Admin. A caller\nthat is not a member answers 404 without revealing whether the Team\nexists, as does a deleted Team; a member that is not an effective Team\nAdmin answers 403. `q` is required and must be 2–200 characters after\ntrimming; it matches the username or the display name\ncase-insensitively. This is a deliberate limit on the directory a Team\nAdmin may enumerate. Deleted Principals and existing members are\nexcluded; disabled Principals stay candidates. The list is ordered by\nthe Principal's `created_at` then `id`, both descending, and paged by\ncursor without a total.\n",
1581
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
1582
+ "parameters": [
1583
+ {
1584
+ "name": "q",
1585
+ "in": "query",
1586
+ "required": true,
1587
+ "description": "A case-insensitive substring of the username or display name; 2–200 characters after trimming.",
1588
+ "schema": {
1589
+ "type": "string",
1590
+ "minLength": 2,
1591
+ "maxLength": 200
1592
+ }
1593
+ },
1594
+ { "$ref": "#/components/parameters/Cursor" },
1595
+ { "$ref": "#/components/parameters/Limit" }
1596
+ ],
1597
+ "responses": {
1598
+ "200": {
1599
+ "description": "One page of candidates.",
1600
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TeamMemberCandidateList" } } }
1601
+ },
1602
+ "400": { "$ref": "#/components/responses/Problem" },
1603
+ "401": { "$ref": "#/components/responses/Problem" },
1604
+ "403": { "$ref": "#/components/responses/Problem" },
1605
+ "404": { "$ref": "#/components/responses/Problem" },
1606
+ "default": { "$ref": "#/components/responses/Problem" }
1607
+ }
1608
+ }
1609
+ },
1610
+ "/api/v1/teams/{team_id}/disable": {
1611
+ "parameters": [{ "$ref": "#/components/parameters/TeamID" }],
1612
+ "post": {
1613
+ "operationId": "disableTeam",
1614
+ "summary": "Disable a Team.",
1615
+ "description": "Allowed for an effective Team Admin and for a Platform Admin.\nDisabling freezes membership and the Team's Capabilities until the\nTeam is enabled again. Disabling a disabled Team changes nothing.\n",
1616
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
1617
+ "responses": {
1618
+ "204": { "description": "The Team is disabled." },
1619
+ "400": { "$ref": "#/components/responses/Problem" },
1620
+ "401": { "$ref": "#/components/responses/Problem" },
1621
+ "403": { "$ref": "#/components/responses/Problem" },
1622
+ "404": { "$ref": "#/components/responses/Problem" },
1623
+ "default": { "$ref": "#/components/responses/Problem" }
1624
+ }
1625
+ }
1626
+ },
1627
+ "/api/v1/teams/{team_id}/enable": {
1628
+ "parameters": [{ "$ref": "#/components/parameters/TeamID" }],
1629
+ "post": {
1630
+ "operationId": "enableTeam",
1631
+ "summary": "Enable a disabled Team.",
1632
+ "description": "Allowed for an effective Team Admin and for a Platform Admin. The Team\nmust have an effective Team Admin, or the request answers 409. Enabling\nrestores the original memberships and the Team's undeleted\nCapabilities. Enabling an enabled Team changes nothing.\n",
1633
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
1634
+ "responses": {
1635
+ "204": { "description": "The Team is enabled." },
1636
+ "400": { "$ref": "#/components/responses/Problem" },
1637
+ "401": { "$ref": "#/components/responses/Problem" },
1638
+ "403": { "$ref": "#/components/responses/Problem" },
1639
+ "404": { "$ref": "#/components/responses/Problem" },
1640
+ "409": { "$ref": "#/components/responses/Problem" },
1641
+ "default": { "$ref": "#/components/responses/Problem" }
1642
+ }
1643
+ }
1644
+ },
1645
+ "/api/v1/teams/{team_id}/members/{principal_id}": {
1646
+ "parameters": [{ "$ref": "#/components/parameters/TeamID" }, { "$ref": "#/components/parameters/PrincipalID" }],
1647
+ "put": {
1648
+ "operationId": "putTeamMember",
1649
+ "summary": "Add a Principal to a Team or change its Team role.",
1650
+ "description": "Allowed for an effective Team Admin of an enabled Team and for a\nPlatform Admin, who may also change a disabled Team so it has an\neffective Team Admin before it is enabled again. Only a User can be a\nTeam Admin; an Agent may be a member. Demoting the last effective Team\nAdmin of an enabled Team answers 409, and so does any change by a Team\nAdmin while the Team is disabled. An unknown or deleted Team or\nPrincipal answers 404.\n",
1651
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
1652
+ "requestBody": {
1653
+ "required": true,
1654
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TeamMemberRole" } } }
1655
+ },
1656
+ "responses": {
1657
+ "200": {
1658
+ "description": "The role was set on an existing membership.",
1659
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TeamMember" } } }
1660
+ },
1661
+ "201": {
1662
+ "description": "The Principal was added to the Team.",
1663
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TeamMember" } } }
1664
+ },
1665
+ "400": { "$ref": "#/components/responses/Problem" },
1666
+ "401": { "$ref": "#/components/responses/Problem" },
1667
+ "403": { "$ref": "#/components/responses/Problem" },
1668
+ "404": { "$ref": "#/components/responses/Problem" },
1669
+ "409": { "$ref": "#/components/responses/Problem" },
1670
+ "413": { "$ref": "#/components/responses/Problem" },
1671
+ "default": { "$ref": "#/components/responses/Problem" }
1672
+ }
1673
+ },
1674
+ "delete": {
1675
+ "operationId": "removeTeamMember",
1676
+ "summary": "Remove a Principal from a Team.",
1677
+ "description": "Allowed for the same callers as `putTeamMember`. Removing the last\neffective Team Admin of an enabled Team answers 409, and so does any\nremoval by a Team Admin while the Team is disabled. A Principal that\nis not a member answers 404.\n",
1678
+ "security": [{ "sessionCookie": [] }, { "bearerToken": [] }],
1679
+ "responses": {
1680
+ "204": { "description": "The Principal is no longer a member." },
1681
+ "400": { "$ref": "#/components/responses/Problem" },
1682
+ "401": { "$ref": "#/components/responses/Problem" },
1683
+ "403": { "$ref": "#/components/responses/Problem" },
1684
+ "404": { "$ref": "#/components/responses/Problem" },
1685
+ "409": { "$ref": "#/components/responses/Problem" },
1686
+ "default": { "$ref": "#/components/responses/Problem" }
1687
+ }
1688
+ }
1689
+ }
1690
+ },
1691
+ components: {
1692
+ "parameters": {
1693
+ "CapabilityID": {
1694
+ "name": "capability_id",
1695
+ "in": "path",
1696
+ "required": true,
1697
+ "schema": {
1698
+ "type": "string",
1699
+ "format": "uuid"
1700
+ }
1701
+ },
1702
+ "IdempotencyKey": {
1703
+ "name": "Idempotency-Key",
1704
+ "in": "header",
1705
+ "description": "Optional client key, 1–255 visible ASCII characters, that makes a\nretried create safe. For 24 hours, a request from the same Principal\nto the same operation with the same key and the same request replays\nthe first successful response's safe metadata instead of creating\nagain; the same key with a different request answers 409, and so does\na retry while the first request is still in progress\n(`/problems/in-progress`, retryable). Failed requests are not\nrecorded.\n",
1706
+ "required": false,
1707
+ "schema": {
1708
+ "type": "string",
1709
+ "minLength": 1,
1710
+ "maxLength": 255,
1711
+ "pattern": "^[!-~]+$"
1712
+ }
1713
+ },
1714
+ "Cursor": {
1715
+ "name": "cursor",
1716
+ "in": "query",
1717
+ "description": "The `next_cursor` of the previous page; omit it for the first page. A\ncursor is opaque and stays valid while the list changes: items created\nlater never appear on following pages and none is skipped or repeated.\n",
1718
+ "schema": {
1719
+ "type": "string",
1720
+ "maxLength": 512
1721
+ }
1722
+ },
1723
+ "Limit": {
1724
+ "name": "limit",
1725
+ "in": "query",
1726
+ "description": "The most items a page holds.",
1727
+ "schema": {
1728
+ "type": "integer",
1729
+ "minimum": 1,
1730
+ "maximum": 100,
1731
+ "default": 20
1732
+ }
1733
+ },
1734
+ "IfMatch": {
1735
+ "name": "If-Match",
1736
+ "in": "header",
1737
+ "description": "The current strong `ETag` of the resource. Not required by the schema because a missing value answers 428, not 400.",
1738
+ "required": false,
1739
+ "schema": { "type": "string" }
1740
+ },
1741
+ "PrincipalID": {
1742
+ "name": "principal_id",
1743
+ "in": "path",
1744
+ "required": true,
1745
+ "schema": {
1746
+ "type": "string",
1747
+ "format": "uuid"
1748
+ }
1749
+ },
1750
+ "TeamID": {
1751
+ "name": "team_id",
1752
+ "in": "path",
1753
+ "required": true,
1754
+ "schema": {
1755
+ "type": "string",
1756
+ "format": "uuid"
1757
+ }
1758
+ },
1759
+ "TokenID": {
1760
+ "name": "token_id",
1761
+ "in": "path",
1762
+ "required": true,
1763
+ "schema": {
1764
+ "type": "string",
1765
+ "format": "uuid"
1766
+ }
1767
+ }
1768
+ },
1769
+ "requestBodies": { "CreateAccessToken": {
1770
+ "required": true,
1771
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateAccessTokenRequest" } } }
1772
+ } },
1773
+ "headers": {
1774
+ "ETag": {
1775
+ "description": "A strong entity tag of the resource's management metadata: the fields\nthat describe how the resource is governed, such as a Capability's\ntags, excluding mutable counters like a Capability's `download_count`\nand any caller-relative field. A download therefore never invalidates\nan editor's `If-Match`, and every caller sees the same tag.\n",
1776
+ "required": true,
1777
+ "schema": { "type": "string" }
1778
+ },
1779
+ "ContentCacheControl": {
1780
+ "description": "`private, no-store` when the Content is not public to anonymous\nreaders; absent for public Content.\n",
1781
+ "required": false,
1782
+ "schema": { "type": "string" }
1783
+ }
1784
+ },
1785
+ "responses": {
1786
+ "Problem": {
1787
+ "description": "An RFC 9457 problem detail.",
1788
+ "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } }
1789
+ },
1790
+ "AccessTokenList": {
1791
+ "description": "Unrevoked Access Tokens, newest first. Token values are never listed.",
1792
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccessTokenList" } } }
1793
+ },
1794
+ "CreatedAccessToken": {
1795
+ "description": "The created Access Token. `token` is the only time the full value is returned.",
1796
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreatedAccessToken" } } }
1797
+ },
1798
+ "ReplayedAccessToken": {
1799
+ "description": "A replayed creation under `Idempotency-Key`: the first created Access Token's metadata, never its value.",
1800
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReplayedAccessToken" } } }
1801
+ }
1802
+ },
1803
+ "schemas": {
1804
+ "CreateAccessTokenRequest": {
1805
+ "type": "object",
1806
+ "required": ["name"],
1807
+ "properties": { "name": {
1808
+ "type": "string",
1809
+ "minLength": 1,
1810
+ "maxLength": 64
1811
+ } },
1812
+ "additionalProperties": false
1813
+ },
1814
+ "AccessToken": {
1815
+ "type": "object",
1816
+ "required": [
1817
+ "id",
1818
+ "name",
1819
+ "token_prefix",
1820
+ "created_at",
1821
+ "last_used_at"
1822
+ ],
1823
+ "properties": {
1824
+ "id": {
1825
+ "type": "string",
1826
+ "format": "uuid"
1827
+ },
1828
+ "name": { "type": "string" },
1829
+ "token_prefix": {
1830
+ "type": "string",
1831
+ "description": "The first 8 characters after `hng_`, for recognising a token. Not a credential."
1832
+ },
1833
+ "created_at": {
1834
+ "type": "string",
1835
+ "format": "date-time"
1836
+ },
1837
+ "last_used_at": {
1838
+ "type": ["string", "null"],
1839
+ "format": "date-time",
1840
+ "description": "Recorded at most once every 5 minutes; null until first use."
1841
+ }
1842
+ }
1843
+ },
1844
+ "AccessTokenList": {
1845
+ "type": "object",
1846
+ "required": ["items"],
1847
+ "properties": { "items": {
1848
+ "type": "array",
1849
+ "items": { "$ref": "#/components/schemas/AccessToken" }
1850
+ } }
1851
+ },
1852
+ "CreatedAccessToken": {
1853
+ "type": "object",
1854
+ "required": [
1855
+ "id",
1856
+ "name",
1857
+ "token_prefix",
1858
+ "token",
1859
+ "created_at"
1860
+ ],
1861
+ "properties": {
1862
+ "id": {
1863
+ "type": "string",
1864
+ "format": "uuid"
1865
+ },
1866
+ "name": { "type": "string" },
1867
+ "token_prefix": { "type": "string" },
1868
+ "token": {
1869
+ "type": "string",
1870
+ "description": "The full `hng_<value>` token. Use it as `Authorization: Bearer <token>`."
1871
+ },
1872
+ "created_at": {
1873
+ "type": "string",
1874
+ "format": "date-time"
1875
+ }
1876
+ }
1877
+ },
1878
+ "ReplayedAccessToken": {
1879
+ "type": "object",
1880
+ "required": [
1881
+ "id",
1882
+ "name",
1883
+ "token_prefix",
1884
+ "created_at"
1885
+ ],
1886
+ "properties": {
1887
+ "id": {
1888
+ "type": "string",
1889
+ "format": "uuid"
1890
+ },
1891
+ "name": { "type": "string" },
1892
+ "token_prefix": { "type": "string" },
1893
+ "created_at": {
1894
+ "type": "string",
1895
+ "format": "date-time"
1896
+ }
1897
+ }
1898
+ },
1899
+ "AuthMethods": {
1900
+ "type": "object",
1901
+ "required": ["password", "oidc"],
1902
+ "properties": {
1903
+ "password": {
1904
+ "type": "object",
1905
+ "required": ["enabled"],
1906
+ "properties": { "enabled": {
1907
+ "type": "boolean",
1908
+ "description": "Always true; the password method has no switch."
1909
+ } }
1910
+ },
1911
+ "oidc": {
1912
+ "type": "object",
1913
+ "required": ["enabled", "display_name"],
1914
+ "properties": {
1915
+ "enabled": { "type": "boolean" },
1916
+ "display_name": {
1917
+ "type": ["string", "null"],
1918
+ "description": "The identity provider's issuer host, or null when OIDC is disabled."
1919
+ }
1920
+ }
1921
+ }
1922
+ }
1923
+ },
1924
+ "LoginRequest": {
1925
+ "type": "object",
1926
+ "required": ["username", "password"],
1927
+ "properties": {
1928
+ "username": {
1929
+ "type": "string",
1930
+ "minLength": 1,
1931
+ "maxLength": 64
1932
+ },
1933
+ "password": {
1934
+ "type": "string",
1935
+ "minLength": 1,
1936
+ "maxLength": 1024
1937
+ }
1938
+ },
1939
+ "additionalProperties": false
1940
+ },
1941
+ "Principal": {
1942
+ "type": "object",
1943
+ "required": [
1944
+ "id",
1945
+ "kind",
1946
+ "username",
1947
+ "display_name",
1948
+ "disabled_at",
1949
+ "created_at",
1950
+ "has_password"
1951
+ ],
1952
+ "properties": {
1953
+ "id": {
1954
+ "type": "string",
1955
+ "format": "uuid"
1956
+ },
1957
+ "kind": {
1958
+ "type": "string",
1959
+ "enum": ["user", "agent"]
1960
+ },
1961
+ "username": {
1962
+ "type": "string",
1963
+ "description": "The name of the Principal's own Namespace."
1964
+ },
1965
+ "display_name": { "type": ["string", "null"] },
1966
+ "role": {
1967
+ "type": "string",
1968
+ "enum": ["admin", "normal"],
1969
+ "description": "The platform role. Present only for User Principals."
1970
+ },
1971
+ "email": {
1972
+ "type": "string",
1973
+ "description": "Omitted when unset and for Agent Principals."
1974
+ },
1975
+ "phone_number": {
1976
+ "type": "string",
1977
+ "description": "Omitted when unset and for Agent Principals."
1978
+ },
1979
+ "avatar": {
1980
+ "type": "string",
1981
+ "description": "Avatar URL. Omitted when unset and for Agent Principals."
1982
+ },
1983
+ "disabled_at": {
1984
+ "type": ["string", "null"],
1985
+ "format": "date-time",
1986
+ "description": "When the Principal was disabled; null while it is enabled."
1987
+ },
1988
+ "created_at": {
1989
+ "type": "string",
1990
+ "format": "date-time"
1991
+ },
1992
+ "has_password": {
1993
+ "type": "boolean",
1994
+ "description": "Whether the Principal has a password credential. Always false for Agents."
1995
+ }
1996
+ }
1997
+ },
1998
+ "AuditEventList": {
1999
+ "type": "object",
2000
+ "required": ["items", "next_cursor"],
2001
+ "properties": {
2002
+ "items": {
2003
+ "type": "array",
2004
+ "items": { "$ref": "#/components/schemas/AuditEvent" }
2005
+ },
2006
+ "next_cursor": { "$ref": "#/components/schemas/NextCursor" }
2007
+ }
2008
+ },
2009
+ "AuditEvent": {
2010
+ "type": "object",
2011
+ "required": [
2012
+ "id",
2013
+ "actor",
2014
+ "action",
2015
+ "resource_type",
2016
+ "resource_id",
2017
+ "metadata",
2018
+ "created_at"
2019
+ ],
2020
+ "properties": {
2021
+ "id": {
2022
+ "type": "string",
2023
+ "format": "uuid"
2024
+ },
2025
+ "actor": {
2026
+ "oneOf": [{ "$ref": "#/components/schemas/AuditActor" }, { "type": "null" }],
2027
+ "description": "The actor that performed the operation; null for a system event."
2028
+ },
2029
+ "action": {
2030
+ "type": "string",
2031
+ "description": "The controlled name of the audited operation. It is not an enum in\nthe response so a client can still show an action it does not yet\nknow instead of hiding the event.\n"
2032
+ },
2033
+ "resource_type": {
2034
+ "type": "string",
2035
+ "enum": [
2036
+ "access_token",
2037
+ "capability",
2038
+ "principal",
2039
+ "session",
2040
+ "team"
2041
+ ]
2042
+ },
2043
+ "resource_id": {
2044
+ "type": "string",
2045
+ "format": "uuid"
2046
+ },
2047
+ "metadata": {
2048
+ "type": "object",
2049
+ "description": "The non-sensitive fields defined for the action, verbatim.",
2050
+ "additionalProperties": { "type": "string" }
2051
+ },
2052
+ "created_at": {
2053
+ "type": "string",
2054
+ "format": "date-time"
2055
+ }
2056
+ },
2057
+ "additionalProperties": false
2058
+ },
2059
+ "AuditActor": {
2060
+ "type": "object",
2061
+ "description": "The Principal that performed an audited operation. A deleted Principal\nand its deleted Namespace still resolve, so historical events keep\ntheir actor facts.\n",
2062
+ "required": [
2063
+ "principal_id",
2064
+ "kind",
2065
+ "username",
2066
+ "display_name"
2067
+ ],
2068
+ "properties": {
2069
+ "principal_id": {
2070
+ "type": "string",
2071
+ "format": "uuid"
2072
+ },
2073
+ "kind": {
2074
+ "type": "string",
2075
+ "enum": ["user", "agent"]
2076
+ },
2077
+ "username": {
2078
+ "type": "string",
2079
+ "description": "The name of the actor's own Namespace."
2080
+ },
2081
+ "display_name": { "type": ["string", "null"] },
2082
+ "avatar": {
2083
+ "type": "string",
2084
+ "description": "Avatar URL. Omitted when unset and for Agent Principals."
2085
+ }
2086
+ },
2087
+ "additionalProperties": false
2088
+ },
2089
+ "PrincipalList": {
2090
+ "type": "object",
2091
+ "required": ["items", "next_cursor"],
2092
+ "properties": {
2093
+ "items": {
2094
+ "type": "array",
2095
+ "items": { "$ref": "#/components/schemas/Principal" }
2096
+ },
2097
+ "next_cursor": { "$ref": "#/components/schemas/NextCursor" }
2098
+ }
2099
+ },
2100
+ "PasswordChange": {
2101
+ "type": "object",
2102
+ "required": ["new_password"],
2103
+ "properties": {
2104
+ "current_password": {
2105
+ "type": "string",
2106
+ "minLength": 1,
2107
+ "maxLength": 1024,
2108
+ "description": "Required when the caller already has a password; ignored when it has\nnone, which sets the initial password.\n"
2109
+ },
2110
+ "new_password": {
2111
+ "type": "string",
2112
+ "minLength": 1,
2113
+ "maxLength": 1024,
2114
+ "description": "Must differ from the current password."
2115
+ }
2116
+ },
2117
+ "additionalProperties": false
2118
+ },
2119
+ "CreatePrincipalRequest": {
2120
+ "type": "object",
2121
+ "required": ["kind", "username"],
2122
+ "properties": {
2123
+ "kind": {
2124
+ "type": "string",
2125
+ "enum": ["user", "agent"]
2126
+ },
2127
+ "username": {
2128
+ "$ref": "#/components/schemas/Name",
2129
+ "description": "The name of the Principal's own Namespace. It never changes."
2130
+ },
2131
+ "display_name": {
2132
+ "type": "string",
2133
+ "maxLength": 128,
2134
+ "description": "Omitted or empty means no display name."
2135
+ },
2136
+ "password": {
2137
+ "type": "string",
2138
+ "minLength": 1,
2139
+ "maxLength": 1024,
2140
+ "description": "Required for a User; an Agent has no password."
2141
+ },
2142
+ "role": {
2143
+ "type": "string",
2144
+ "enum": ["admin", "normal"],
2145
+ "description": "The platform role of a User, `normal` when omitted. An Agent has no role."
2146
+ }
2147
+ },
2148
+ "additionalProperties": false
2149
+ },
2150
+ "PasswordReset": {
2151
+ "type": "object",
2152
+ "required": ["new_password"],
2153
+ "properties": { "new_password": {
2154
+ "type": "string",
2155
+ "minLength": 1,
2156
+ "maxLength": 1024
2157
+ } },
2158
+ "additionalProperties": false
2159
+ },
2160
+ "RoleUpdate": {
2161
+ "type": "object",
2162
+ "required": ["role"],
2163
+ "properties": { "role": {
2164
+ "type": "string",
2165
+ "enum": ["admin", "normal"]
2166
+ } },
2167
+ "additionalProperties": false
2168
+ },
2169
+ "Session": {
2170
+ "type": "object",
2171
+ "required": [
2172
+ "id",
2173
+ "current",
2174
+ "created_at",
2175
+ "last_seen_at",
2176
+ "expires_at"
2177
+ ],
2178
+ "properties": {
2179
+ "id": {
2180
+ "type": "string",
2181
+ "format": "uuid"
2182
+ },
2183
+ "current": {
2184
+ "type": "boolean",
2185
+ "description": "Whether this is the Session the request was made with."
2186
+ },
2187
+ "created_at": {
2188
+ "type": "string",
2189
+ "format": "date-time"
2190
+ },
2191
+ "last_seen_at": {
2192
+ "type": "string",
2193
+ "format": "date-time",
2194
+ "description": "Recorded at most once every 5 minutes."
2195
+ },
2196
+ "expires_at": {
2197
+ "type": "string",
2198
+ "format": "date-time"
2199
+ }
2200
+ }
2201
+ },
2202
+ "SessionList": {
2203
+ "type": "object",
2204
+ "required": ["items"],
2205
+ "properties": { "items": {
2206
+ "type": "array",
2207
+ "items": { "$ref": "#/components/schemas/Session" }
2208
+ } }
2209
+ },
2210
+ "ProfileUpdate": {
2211
+ "type": "object",
2212
+ "properties": {
2213
+ "display_name": {
2214
+ "type": "string",
2215
+ "maxLength": 128
2216
+ },
2217
+ "email": {
2218
+ "type": "string",
2219
+ "maxLength": 254,
2220
+ "description": "A User-only field; Agents cannot set it."
2221
+ },
2222
+ "phone_number": {
2223
+ "type": "string",
2224
+ "maxLength": 32,
2225
+ "description": "A User-only field; Agents cannot set it."
2226
+ },
2227
+ "avatar": {
2228
+ "type": "string",
2229
+ "maxLength": 2048,
2230
+ "description": "An absolute `http` or `https` URL. A User-only field; Agents cannot set it."
2231
+ }
2232
+ },
2233
+ "additionalProperties": false
2234
+ },
2235
+ "Name": {
2236
+ "type": "string",
2237
+ "description": "1–64 lowercase letters, digits and hyphens; no leading, trailing or\ndoubled hyphen.\n",
2238
+ "minLength": 1,
2239
+ "maxLength": 64,
2240
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
2241
+ },
2242
+ "Visibility": {
2243
+ "type": "string",
2244
+ "enum": [
2245
+ "public",
2246
+ "internal",
2247
+ "private"
2248
+ ]
2249
+ },
2250
+ "Description": {
2251
+ "type": "string",
2252
+ "description": "Required, at most 2,000 Unicode characters.",
2253
+ "minLength": 1,
2254
+ "maxLength": 2e3
2255
+ },
2256
+ "Readme": {
2257
+ "type": ["string", "null"],
2258
+ "description": "Markdown, at most 1 MiB of UTF-8. Omitted or null means no README; an\nempty string is kept as an empty README.\n"
2259
+ },
2260
+ "MCPConfig": {
2261
+ "type": "object",
2262
+ "description": "The MCP config. The top level must be a JSON object; duplicate member\nnames and invalid UTF-8 are rejected.\n",
2263
+ "additionalProperties": true,
2264
+ "x-go-type": "json.RawMessage"
2265
+ },
2266
+ "Tag": {
2267
+ "type": "string",
2268
+ "description": "A normalized tag: 1–32 Unicode characters after trimming, collapsing\ninternal whitespace and case-folding to lowercase. It cannot contain a\ncomma or a control character. Writers and readers normalize the same\nway, so a tag compares and filters by its normalized form.\n",
2269
+ "minLength": 1,
2270
+ "maxLength": 32,
2271
+ "pattern": "^[^,\\x00-\\x1F\\x7F]+$"
2272
+ },
2273
+ "CreateMCPCapabilityRequest": {
2274
+ "type": "object",
2275
+ "required": [
2276
+ "namespace",
2277
+ "name",
2278
+ "description",
2279
+ "visibility",
2280
+ "config"
2281
+ ],
2282
+ "properties": {
2283
+ "namespace": {
2284
+ "$ref": "#/components/schemas/Name",
2285
+ "description": "The name of the target Namespace."
2286
+ },
2287
+ "name": { "$ref": "#/components/schemas/Name" },
2288
+ "description": { "$ref": "#/components/schemas/Description" },
2289
+ "visibility": { "$ref": "#/components/schemas/Visibility" },
2290
+ "readme": { "$ref": "#/components/schemas/Readme" },
2291
+ "tags": {
2292
+ "type": "array",
2293
+ "description": "The Capability's tags. Omitted or empty means no tags. The server\nnormalizes, deduplicates and sorts them, and rejects more than\neight.\n",
2294
+ "maxItems": 8,
2295
+ "items": { "$ref": "#/components/schemas/Tag" }
2296
+ },
2297
+ "config": { "$ref": "#/components/schemas/MCPConfig" }
2298
+ },
2299
+ "additionalProperties": false
2300
+ },
2301
+ "CreateSkillCapabilityRequest": {
2302
+ "type": "object",
2303
+ "description": "Each field is sent once, and `package` must be the last part. The\npackage is a ZIP of at most 16 MiB with at most 2,000 entries, each\nentry at most 16 MiB unpacked, at most 128 MiB unpacked in total and a\ncompression ratio of at most 100:1. Entries must be regular files or\ndirectories with clean relative paths. Exactly one `SKILL.md` is\nallowed, at the package root or directly inside the single top-level\ndirectory that holds every entry.\n",
2304
+ "required": [
2305
+ "namespace",
2306
+ "name",
2307
+ "description",
2308
+ "visibility",
2309
+ "package"
2310
+ ],
2311
+ "properties": {
2312
+ "namespace": {
2313
+ "$ref": "#/components/schemas/Name",
2314
+ "description": "The name of the target Namespace."
2315
+ },
2316
+ "name": { "$ref": "#/components/schemas/Name" },
2317
+ "description": { "$ref": "#/components/schemas/Description" },
2318
+ "visibility": { "$ref": "#/components/schemas/Visibility" },
2319
+ "readme": {
2320
+ "type": "string",
2321
+ "description": "Markdown, at most 1 MiB of UTF-8. Omitted means no README."
2322
+ },
2323
+ "tags": {
2324
+ "type": "string",
2325
+ "description": "The Capability's tags as one comma-separated field, because a\nmultipart form cannot carry a JSON array. Tags themselves cannot\ncontain commas. Omitted or empty means no tags; the server\nnormalizes, deduplicates and sorts them, and rejects more than\neight.\n",
2326
+ "maxLength": 512
2327
+ },
2328
+ "package": {
2329
+ "type": "string",
2330
+ "description": "The Capability Package, sent as `application/zip`."
2331
+ }
2332
+ },
2333
+ "additionalProperties": false
2334
+ },
2335
+ "CapabilityUpdate": {
2336
+ "type": "object",
2337
+ "properties": {
2338
+ "description": { "$ref": "#/components/schemas/Description" },
2339
+ "visibility": { "$ref": "#/components/schemas/Visibility" },
2340
+ "tags": {
2341
+ "type": "array",
2342
+ "description": "When present, replaces the whole tag set. Omitted leaves it\nunchanged. Normalization and limits are those of create.\n",
2343
+ "maxItems": 8,
2344
+ "items": { "$ref": "#/components/schemas/Tag" }
2345
+ },
2346
+ "kind": {
2347
+ "description": "Immutable; any value returns 400.",
2348
+ "x-go-type": "json.RawMessage",
2349
+ "x-go-type-skip-optional-pointer": true
2350
+ },
2351
+ "namespace": {
2352
+ "description": "Immutable; any value returns 400.",
2353
+ "x-go-type": "json.RawMessage",
2354
+ "x-go-type-skip-optional-pointer": true
2355
+ },
2356
+ "name": {
2357
+ "description": "Immutable; any value returns 400.",
2358
+ "x-go-type": "json.RawMessage",
2359
+ "x-go-type-skip-optional-pointer": true
2360
+ }
2361
+ },
2362
+ "additionalProperties": false
2363
+ },
2364
+ "PublishMCPVersionRequest": {
2365
+ "type": "object",
2366
+ "required": ["config"],
2367
+ "properties": {
2368
+ "readme": { "$ref": "#/components/schemas/Readme" },
2369
+ "config": { "$ref": "#/components/schemas/MCPConfig" }
2370
+ },
2371
+ "additionalProperties": false
2372
+ },
2373
+ "PublishSkillVersionRequest": {
2374
+ "type": "object",
2375
+ "description": "Each field is sent once, and `package` must be the last part. The\npackage follows the rules of `CreateSkillCapabilityRequest`.\n",
2376
+ "required": ["package"],
2377
+ "properties": {
2378
+ "readme": {
2379
+ "type": "string",
2380
+ "description": "Markdown, at most 1 MiB of UTF-8. Omitted means no README."
2381
+ },
2382
+ "package": {
2383
+ "type": "string",
2384
+ "description": "The Capability Package, sent as `application/zip`."
2385
+ }
2386
+ },
2387
+ "additionalProperties": false
2388
+ },
2389
+ "Capability": {
2390
+ "type": "object",
2391
+ "required": [
2392
+ "id",
2393
+ "kind",
2394
+ "namespace",
2395
+ "namespace_kind",
2396
+ "namespace_display_name",
2397
+ "name",
2398
+ "path",
2399
+ "visibility",
2400
+ "description",
2401
+ "tags",
2402
+ "download_count",
2403
+ "current_version",
2404
+ "created_at",
2405
+ "updated_at"
2406
+ ],
2407
+ "properties": {
2408
+ "id": {
2409
+ "type": "string",
2410
+ "format": "uuid"
2411
+ },
2412
+ "kind": {
2413
+ "type": "string",
2414
+ "enum": ["skill", "mcp"]
2415
+ },
2416
+ "namespace": {
2417
+ "$ref": "#/components/schemas/Name",
2418
+ "description": "The name of the owning Namespace."
2419
+ },
2420
+ "namespace_kind": {
2421
+ "$ref": "#/components/schemas/NamespaceKind",
2422
+ "description": "The kind of the owning Namespace."
2423
+ },
2424
+ "namespace_display_name": {
2425
+ "type": ["string", "null"],
2426
+ "description": "The display name of the owning Team or Principal, when one is set."
2427
+ },
2428
+ "name": { "$ref": "#/components/schemas/Name" },
2429
+ "path": {
2430
+ "type": "string",
2431
+ "description": "Derived address: `/<skills|mcp>/<name>` for system,\n`/<skills|mcp>/<namespace>/<name>` otherwise. Not a permanent\nidentity.\n"
2432
+ },
2433
+ "visibility": { "$ref": "#/components/schemas/Visibility" },
2434
+ "description": { "$ref": "#/components/schemas/Description" },
2435
+ "tags": {
2436
+ "type": "array",
2437
+ "description": "Normalized tags, sorted lexicographically; empty, never null.",
2438
+ "maxItems": 8,
2439
+ "items": { "$ref": "#/components/schemas/Tag" }
2440
+ },
2441
+ "download_count": {
2442
+ "type": "integer",
2443
+ "format": "int64",
2444
+ "minimum": 0,
2445
+ "description": "The number of successful Content GETs, advisory and aggregate. It\ncounts neither HEAD, 304 nor failed requests, and is not part of\nthe `ETag` or the Audit Event vocabulary.\n"
2446
+ },
2447
+ "current_version": { "$ref": "#/components/schemas/CalVer" },
2448
+ "created_at": {
2449
+ "type": "string",
2450
+ "format": "date-time"
2451
+ },
2452
+ "updated_at": {
2453
+ "type": "string",
2454
+ "format": "date-time"
2455
+ }
2456
+ }
2457
+ },
2458
+ "TagCount": {
2459
+ "type": "object",
2460
+ "required": ["name", "count"],
2461
+ "properties": {
2462
+ "name": { "$ref": "#/components/schemas/Tag" },
2463
+ "count": {
2464
+ "type": "integer",
2465
+ "format": "int64",
2466
+ "minimum": 0
2467
+ }
2468
+ }
2469
+ },
2470
+ "TagList": {
2471
+ "type": "object",
2472
+ "required": ["items", "truncated"],
2473
+ "properties": {
2474
+ "items": {
2475
+ "type": "array",
2476
+ "items": { "$ref": "#/components/schemas/TagCount" }
2477
+ },
2478
+ "truncated": {
2479
+ "type": "boolean",
2480
+ "description": "More tags exist than the requested `limit`."
2481
+ }
2482
+ }
2483
+ },
2484
+ "CapabilityList": {
2485
+ "type": "object",
2486
+ "required": ["items", "next_cursor"],
2487
+ "properties": {
2488
+ "items": {
2489
+ "type": "array",
2490
+ "items": { "$ref": "#/components/schemas/Capability" }
2491
+ },
2492
+ "next_cursor": { "$ref": "#/components/schemas/NextCursor" }
2493
+ }
2494
+ },
2495
+ "VersionSummary": {
2496
+ "type": "object",
2497
+ "required": [
2498
+ "id",
2499
+ "version",
2500
+ "created_at"
2501
+ ],
2502
+ "properties": {
2503
+ "id": {
2504
+ "type": "string",
2505
+ "format": "uuid"
2506
+ },
2507
+ "version": { "$ref": "#/components/schemas/CalVer" },
2508
+ "created_at": {
2509
+ "type": "string",
2510
+ "format": "date-time"
2511
+ }
2512
+ }
2513
+ },
2514
+ "Version": {
2515
+ "type": "object",
2516
+ "required": [
2517
+ "id",
2518
+ "version",
2519
+ "readme",
2520
+ "created_at"
2521
+ ],
2522
+ "properties": {
2523
+ "id": {
2524
+ "type": "string",
2525
+ "format": "uuid"
2526
+ },
2527
+ "version": { "$ref": "#/components/schemas/CalVer" },
2528
+ "readme": {
2529
+ "type": ["string", "null"],
2530
+ "description": "The Version's Markdown README, or null when it has none."
2531
+ },
2532
+ "created_at": {
2533
+ "type": "string",
2534
+ "format": "date-time"
2535
+ },
2536
+ "published_by": {
2537
+ "oneOf": [{ "$ref": "#/components/schemas/VersionPublisher" }, { "type": "null" }],
2538
+ "description": "The Principal that published this Version, or null when the\nVersion has no recorded publisher. The publisher's Namespace name\nis returned even after the Principal or its Namespace is deleted,\nso historical Versions keep their attribution.\n"
2539
+ },
2540
+ "content": { "$ref": "#/components/schemas/VersionContent" },
2541
+ "config": {
2542
+ "type": ["object", "null"],
2543
+ "description": "The parsed MCP config for an MCP Version; null for a Skill Version.\nReading it does not count as a Content download.\n",
2544
+ "additionalProperties": true
2545
+ }
2546
+ }
2547
+ },
2548
+ "VersionPublisher": {
2549
+ "type": "object",
2550
+ "description": "The Principal that published a Version. A deleted Principal and its\ndeleted Namespace still resolve, so historical attribution survives.\n",
2551
+ "properties": {
2552
+ "principal_id": {
2553
+ "type": "string",
2554
+ "format": "uuid"
2555
+ },
2556
+ "username": {
2557
+ "type": "string",
2558
+ "description": "The name of the publisher's own Namespace."
2559
+ },
2560
+ "display_name": { "type": ["string", "null"] },
2561
+ "avatar": {
2562
+ "type": "string",
2563
+ "description": "Avatar URL. Omitted when unset and for Agent Principals."
2564
+ }
2565
+ },
2566
+ "required": [
2567
+ "principal_id",
2568
+ "username",
2569
+ "display_name"
2570
+ ],
2571
+ "additionalProperties": false
2572
+ },
2573
+ "VersionContent": {
2574
+ "type": "object",
2575
+ "description": "The stored form of a Version's Content. `sha256` is the hex digest of\nthe same bytes the Content endpoint serves, so it equals the hex\ninside that endpoint's strong `ETag`.\n",
2576
+ "properties": {
2577
+ "media_type": {
2578
+ "type": "string",
2579
+ "enum": ["application/zip", "application/json"]
2580
+ },
2581
+ "size": {
2582
+ "type": "integer",
2583
+ "format": "int64",
2584
+ "description": "The Content's size in bytes."
2585
+ },
2586
+ "sha256": {
2587
+ "type": "string",
2588
+ "pattern": "^[0-9a-f]{64}$",
2589
+ "description": "Lowercase hex sha256 of the Content bytes."
2590
+ }
2591
+ },
2592
+ "required": [
2593
+ "media_type",
2594
+ "size",
2595
+ "sha256"
2596
+ ],
2597
+ "additionalProperties": false
2598
+ },
2599
+ "VersionList": {
2600
+ "type": "object",
2601
+ "required": ["items", "next_cursor"],
2602
+ "properties": {
2603
+ "items": {
2604
+ "type": "array",
2605
+ "items": { "$ref": "#/components/schemas/VersionSummary" }
2606
+ },
2607
+ "next_cursor": { "$ref": "#/components/schemas/NextCursor" }
2608
+ }
2609
+ },
2610
+ "Namespace": {
2611
+ "type": "object",
2612
+ "required": [
2613
+ "id",
2614
+ "kind",
2615
+ "name",
2616
+ "namespace_display_name",
2617
+ "created_at"
2618
+ ],
2619
+ "properties": {
2620
+ "id": {
2621
+ "type": "string",
2622
+ "format": "uuid"
2623
+ },
2624
+ "kind": { "$ref": "#/components/schemas/NamespaceKind" },
2625
+ "name": { "$ref": "#/components/schemas/Name" },
2626
+ "namespace_display_name": {
2627
+ "type": ["string", "null"],
2628
+ "description": "The display name of the owning Team or Principal, when one is set."
2629
+ },
2630
+ "created_at": {
2631
+ "type": "string",
2632
+ "format": "date-time"
2633
+ }
2634
+ }
2635
+ },
2636
+ "NamespaceKind": {
2637
+ "type": "string",
2638
+ "description": "The kind of owner a Namespace belongs to.",
2639
+ "enum": [
2640
+ "system",
2641
+ "team",
2642
+ "principal"
2643
+ ]
2644
+ },
2645
+ "NamespaceList": {
2646
+ "type": "object",
2647
+ "required": ["items", "next_cursor"],
2648
+ "properties": {
2649
+ "items": {
2650
+ "type": "array",
2651
+ "items": { "$ref": "#/components/schemas/Namespace" }
2652
+ },
2653
+ "next_cursor": { "$ref": "#/components/schemas/NextCursor" }
2654
+ }
2655
+ },
2656
+ "NextCursor": {
2657
+ "type": ["string", "null"],
2658
+ "description": "Send as `cursor` to read the next page; null on the last page."
2659
+ },
2660
+ "CreateTeamRequest": {
2661
+ "type": "object",
2662
+ "required": ["name", "admins"],
2663
+ "properties": {
2664
+ "name": {
2665
+ "$ref": "#/components/schemas/Name",
2666
+ "description": "The name of the Team's Namespace."
2667
+ },
2668
+ "display_name": {
2669
+ "type": "string",
2670
+ "maxLength": 128
2671
+ },
2672
+ "admins": {
2673
+ "type": "array",
2674
+ "description": "The first Team Admins. Each must be an enabled User.",
2675
+ "minItems": 1,
2676
+ "uniqueItems": true,
2677
+ "items": {
2678
+ "type": "string",
2679
+ "format": "uuid"
2680
+ }
2681
+ }
2682
+ },
2683
+ "additionalProperties": false
2684
+ },
2685
+ "Team": {
2686
+ "type": "object",
2687
+ "required": [
2688
+ "id",
2689
+ "name",
2690
+ "display_name",
2691
+ "disabled_at",
2692
+ "created_at"
2693
+ ],
2694
+ "properties": {
2695
+ "id": {
2696
+ "type": "string",
2697
+ "format": "uuid"
2698
+ },
2699
+ "name": {
2700
+ "$ref": "#/components/schemas/Name",
2701
+ "description": "The name of the Team's Namespace."
2702
+ },
2703
+ "display_name": { "type": ["string", "null"] },
2704
+ "disabled_at": {
2705
+ "type": ["string", "null"],
2706
+ "format": "date-time"
2707
+ },
2708
+ "created_at": {
2709
+ "type": "string",
2710
+ "format": "date-time"
2711
+ }
2712
+ }
2713
+ },
2714
+ "TeamList": {
2715
+ "type": "object",
2716
+ "required": ["items", "next_cursor"],
2717
+ "properties": {
2718
+ "items": {
2719
+ "type": "array",
2720
+ "items": { "$ref": "#/components/schemas/Team" }
2721
+ },
2722
+ "next_cursor": { "$ref": "#/components/schemas/NextCursor" }
2723
+ }
2724
+ },
2725
+ "TeamRole": {
2726
+ "type": "string",
2727
+ "enum": ["admin", "member"],
2728
+ "description": "A Team Admin is also a member and alone governs the Team."
2729
+ },
2730
+ "TeamMemberRole": {
2731
+ "type": "object",
2732
+ "required": ["role"],
2733
+ "properties": { "role": { "$ref": "#/components/schemas/TeamRole" } },
2734
+ "additionalProperties": false
2735
+ },
2736
+ "TeamMember": {
2737
+ "type": "object",
2738
+ "required": [
2739
+ "principal_id",
2740
+ "kind",
2741
+ "username",
2742
+ "display_name",
2743
+ "disabled_at",
2744
+ "role"
2745
+ ],
2746
+ "properties": {
2747
+ "principal_id": {
2748
+ "type": "string",
2749
+ "format": "uuid"
2750
+ },
2751
+ "kind": {
2752
+ "type": "string",
2753
+ "enum": ["user", "agent"]
2754
+ },
2755
+ "username": {
2756
+ "type": "string",
2757
+ "description": "The name of the Principal's own Namespace."
2758
+ },
2759
+ "display_name": { "type": ["string", "null"] },
2760
+ "disabled_at": {
2761
+ "type": ["string", "null"],
2762
+ "format": "date-time"
2763
+ },
2764
+ "role": { "$ref": "#/components/schemas/TeamRole" },
2765
+ "avatar": {
2766
+ "type": "string",
2767
+ "description": "Avatar URL. Omitted when unset and for Agent Principals."
2768
+ }
2769
+ }
2770
+ },
2771
+ "TeamMemberList": {
2772
+ "type": "object",
2773
+ "required": ["items", "next_cursor"],
2774
+ "properties": {
2775
+ "items": {
2776
+ "type": "array",
2777
+ "items": { "$ref": "#/components/schemas/TeamMember" }
2778
+ },
2779
+ "next_cursor": { "$ref": "#/components/schemas/NextCursor" }
2780
+ }
2781
+ },
2782
+ "TeamMemberCandidate": {
2783
+ "type": "object",
2784
+ "description": "A Principal that is not a member of the Team and matches the required\nquery. Deleted Principals never appear; disabled ones do.\n",
2785
+ "required": [
2786
+ "id",
2787
+ "kind",
2788
+ "username",
2789
+ "display_name",
2790
+ "disabled_at"
2791
+ ],
2792
+ "properties": {
2793
+ "id": {
2794
+ "type": "string",
2795
+ "format": "uuid"
2796
+ },
2797
+ "kind": {
2798
+ "type": "string",
2799
+ "enum": ["user", "agent"]
2800
+ },
2801
+ "username": {
2802
+ "type": "string",
2803
+ "description": "The name of the Principal's own Namespace."
2804
+ },
2805
+ "display_name": { "type": ["string", "null"] },
2806
+ "disabled_at": {
2807
+ "type": ["string", "null"],
2808
+ "format": "date-time"
2809
+ },
2810
+ "avatar": {
2811
+ "type": "string",
2812
+ "description": "Avatar URL. Omitted when unset and for Agent Principals."
2813
+ }
2814
+ }
2815
+ },
2816
+ "TeamMemberCandidateList": {
2817
+ "type": "object",
2818
+ "required": ["items", "next_cursor"],
2819
+ "properties": {
2820
+ "items": {
2821
+ "type": "array",
2822
+ "items": { "$ref": "#/components/schemas/TeamMemberCandidate" }
2823
+ },
2824
+ "next_cursor": { "$ref": "#/components/schemas/NextCursor" }
2825
+ }
2826
+ },
2827
+ "CalVer": {
2828
+ "type": "string",
2829
+ "description": "`YYYY.MM.DD.N` in UTC; N counts from 0 per Capability per day.",
2830
+ "pattern": "^[0-9]{4}\\.[0-9]{2}\\.[0-9]{2}\\.(0|[1-9][0-9]*)$"
2831
+ },
2832
+ "ProblemDetails": {
2833
+ "type": "object",
2834
+ "description": "RFC 9457 problem detail, served as `application/problem+json`. `type`\nis a relative reference under `/problems/` (for example\n`/problems/unauthenticated`), or `about:blank` for unclassified errors.\n",
2835
+ "required": [
2836
+ "type",
2837
+ "title",
2838
+ "status"
2839
+ ],
2840
+ "properties": {
2841
+ "type": {
2842
+ "type": "string",
2843
+ "format": "uri-reference"
2844
+ },
2845
+ "title": { "type": "string" },
2846
+ "status": {
2847
+ "type": "integer",
2848
+ "minimum": 100,
2849
+ "maximum": 599
2850
+ },
2851
+ "detail": { "type": "string" },
2852
+ "instance": {
2853
+ "type": "string",
2854
+ "format": "uri-reference"
2855
+ }
2856
+ },
2857
+ "additionalProperties": true
2858
+ }
2859
+ },
2860
+ "securitySchemes": {
2861
+ "sessionCookie": {
2862
+ "type": "apiKey",
2863
+ "in": "cookie",
2864
+ "name": "hangar_session"
2865
+ },
2866
+ "bearerToken": {
2867
+ "type": "http",
2868
+ "scheme": "bearer"
2869
+ }
2870
+ }
2871
+ }
2872
+ });
2873
+ /** Collapses `schema capabilities list` into the catalog's single-space form. */
2874
+ function normalizeCommandName(raw) {
2875
+ return raw.trim().split(/\s+/).join(" ");
2876
+ }
2877
+ /** Rewrites a root alias to its canonical noun: `caps list` -> `capabilities list`. */
2878
+ function canonicalCommandName(raw) {
2879
+ const name = normalizeCommandName(raw);
2880
+ const [first = "", ...rest] = name.split(" ");
2881
+ const canonical = Object.entries(COMMAND_ALIASES).find(([, aliases]) => aliases.includes(first))?.[0];
2882
+ return canonical ? [canonical, ...rest].join(" ") : name;
2883
+ }
2884
+ function findCommand(raw) {
2885
+ const name = canonicalCommandName(raw);
2886
+ return COMMANDS.find((spec) => spec.name === name);
2887
+ }
2888
+ // src/catalog.ts
2889
+ /**
2890
+ * Stable machine-readable catalog of the workflow commands. OpenAPI-derived
2891
+ * commands are merged into the same shape by `commandCatalog`.
2892
+ */
2893
+ function workflowCatalog() {
2894
+ return [
2895
+ {
2896
+ name: "auth",
2897
+ summary: "Manage credentials (login, logout, status, token, whoami)",
2898
+ group: "auth",
2899
+ args: [],
2900
+ flags: [],
2901
+ destructive: false,
2902
+ dryRun: false
2903
+ },
2904
+ {
2905
+ name: "auth login",
2906
+ summary: "Log in with username and password and mint an access token",
2907
+ group: "auth",
2908
+ args: [],
2909
+ flags: [{
2910
+ name: "token",
2911
+ type: "boolean",
2912
+ description: "Read a token from stdin"
2913
+ }],
2914
+ destructive: false,
2915
+ dryRun: false
2916
+ },
2917
+ {
2918
+ name: "auth logout",
2919
+ summary: "Revoke the stored token and delete the local config",
2920
+ group: "auth",
2921
+ args: [],
2922
+ flags: [],
2923
+ destructive: true,
2924
+ dryRun: false
2925
+ },
2926
+ {
2927
+ name: "auth status",
2928
+ summary: "Show credential source, server, principal and token prefix",
2929
+ group: "auth",
2930
+ args: [],
2931
+ flags: [{
2932
+ name: "offline",
2933
+ type: "boolean",
2934
+ description: "Skip the GET /me check"
2935
+ }],
2936
+ destructive: false,
2937
+ dryRun: false
2938
+ },
2939
+ {
2940
+ name: "auth token",
2941
+ summary: "Print the access token to stdout and nothing else",
2942
+ group: "auth",
2943
+ args: [],
2944
+ flags: [],
2945
+ destructive: false,
2946
+ dryRun: false
2947
+ },
2948
+ {
2949
+ name: "auth whoami",
2950
+ summary: "Print the GET /me payload",
2951
+ group: "auth",
2952
+ args: [],
2953
+ flags: [],
2954
+ destructive: false,
2955
+ dryRun: false
2956
+ },
2957
+ {
2958
+ name: "skills add",
2959
+ summary: "Install skills from the catalog into agent targets",
2960
+ group: "skills",
2961
+ args: [{
2962
+ name: "path",
2963
+ required: true,
2964
+ variadic: true,
2965
+ description: "<name> or <namespace>/<name>, optionally @<version>"
2966
+ }],
2967
+ flags: [
2968
+ {
2969
+ name: "agent",
2970
+ type: "string",
2971
+ repeatable: true,
2972
+ description: "Agent target id"
2973
+ },
2974
+ {
2975
+ name: "all",
2976
+ type: "boolean",
2977
+ description: "Install to all known agents"
2978
+ },
2979
+ {
2980
+ name: "global",
2981
+ type: "boolean",
2982
+ description: "Install globally instead of project"
2983
+ },
2984
+ {
2985
+ name: "dir",
2986
+ type: "string",
2987
+ description: "Copy into this directory instead"
2988
+ },
2989
+ {
2990
+ name: "as",
2991
+ type: "string",
2992
+ description: "Override the target directory name"
2993
+ },
2994
+ {
2995
+ name: "copy",
2996
+ type: "boolean",
2997
+ description: "Copy instead of canonical + symlink"
2998
+ }
2999
+ ],
3000
+ destructive: false,
3001
+ dryRun: true
3002
+ },
3003
+ {
3004
+ name: "skills publish",
3005
+ summary: "Publish a local skill directory as a capability or a new version",
3006
+ group: "skills",
3007
+ args: [{
3008
+ name: "dir",
3009
+ required: true,
3010
+ variadic: true,
3011
+ description: "Local directory containing SKILL.md"
3012
+ }],
3013
+ flags: [
3014
+ {
3015
+ name: "namespace",
3016
+ type: "string",
3017
+ description: "Target namespace (default: system)"
3018
+ },
3019
+ {
3020
+ name: "name",
3021
+ type: "string",
3022
+ description: "Capability name (default: directory name)"
3023
+ },
3024
+ {
3025
+ name: "description",
3026
+ type: "string",
3027
+ description: "Required when creating"
3028
+ },
3029
+ {
3030
+ name: "visibility",
3031
+ type: "string",
3032
+ description: "public | internal | private (default: private)"
3033
+ },
3034
+ {
3035
+ name: "readme",
3036
+ type: "string",
3037
+ description: "Markdown file to attach"
3038
+ },
3039
+ {
3040
+ name: "tag",
3041
+ type: "string",
3042
+ repeatable: true,
3043
+ description: "Capability tag"
3044
+ },
3045
+ {
3046
+ name: "new",
3047
+ type: "boolean",
3048
+ description: "Fail when the capability already exists"
3049
+ }
3050
+ ],
3051
+ destructive: false,
3052
+ dryRun: true
3053
+ },
3054
+ {
3055
+ name: "mcp add",
3056
+ summary: "Merge MCP server entries from the catalog into agent configs",
3057
+ group: "mcp",
3058
+ args: [{
3059
+ name: "path",
3060
+ required: true,
3061
+ variadic: true,
3062
+ description: "<name> or <namespace>/<name>, optionally @<version>"
3063
+ }],
3064
+ flags: [
3065
+ {
3066
+ name: "agent",
3067
+ type: "string",
3068
+ repeatable: true,
3069
+ description: "Agent target id"
3070
+ },
3071
+ {
3072
+ name: "all",
3073
+ type: "boolean",
3074
+ description: "Install to all known agents"
3075
+ },
3076
+ {
3077
+ name: "global",
3078
+ type: "boolean",
3079
+ description: "Install globally instead of project"
3080
+ }
3081
+ ],
3082
+ destructive: false,
3083
+ dryRun: true
3084
+ },
3085
+ {
3086
+ name: "mcp publish",
3087
+ summary: "Publish an MCP snapshot as a capability or a new version",
3088
+ group: "mcp",
3089
+ args: [{
3090
+ name: "file",
3091
+ required: true,
3092
+ variadic: true,
3093
+ description: "Local JSON snapshot containing mcpServers"
3094
+ }],
3095
+ flags: [
3096
+ {
3097
+ name: "namespace",
3098
+ type: "string",
3099
+ description: "Target namespace (default: system)"
3100
+ },
3101
+ {
3102
+ name: "name",
3103
+ type: "string",
3104
+ description: "Capability name (default: file name without .json)"
3105
+ },
3106
+ {
3107
+ name: "description",
3108
+ type: "string",
3109
+ description: "Required when creating"
3110
+ },
3111
+ {
3112
+ name: "visibility",
3113
+ type: "string",
3114
+ description: "public | internal | private (default: private)"
3115
+ },
3116
+ {
3117
+ name: "readme",
3118
+ type: "string",
3119
+ description: "Markdown file to attach"
3120
+ },
3121
+ {
3122
+ name: "tag",
3123
+ type: "string",
3124
+ repeatable: true,
3125
+ description: "Capability tag"
3126
+ },
3127
+ {
3128
+ name: "new",
3129
+ type: "boolean",
3130
+ description: "Fail when the capability already exists"
3131
+ }
3132
+ ],
3133
+ destructive: false,
3134
+ dryRun: true
3135
+ },
3136
+ {
3137
+ name: "commands",
3138
+ summary: "Print the command catalog",
3139
+ group: "discovery",
3140
+ args: [],
3141
+ flags: [],
3142
+ destructive: false,
3143
+ dryRun: false
3144
+ },
3145
+ {
3146
+ name: "schema",
3147
+ summary: "Print the method, path, params and body schema of a command",
3148
+ group: "discovery",
3149
+ args: [{
3150
+ name: "command",
3151
+ required: true,
3152
+ description: "Command name, e.g. 'skills add'"
3153
+ }],
3154
+ flags: [],
3155
+ destructive: false,
3156
+ dryRun: false
3157
+ },
3158
+ {
3159
+ name: "completion",
3160
+ summary: "Print a shell completion script",
3161
+ group: "discovery",
3162
+ args: [{
3163
+ name: "shell",
3164
+ required: true,
3165
+ description: "bash | zsh | fish"
3166
+ }],
3167
+ flags: [],
3168
+ destructive: false,
3169
+ dryRun: false
3170
+ }
3171
+ ];
3172
+ }
3173
+ /**
3174
+ * The full catalog: workflow commands first, then OpenAPI-derived commands.
3175
+ * A derived command whose name a workflow command already owns appears once,
3176
+ * in the workflow group. Ordering is stable and deterministic so
3177
+ * `commands --json` diffing is safe.
3178
+ */
3179
+ function commandCatalog(generated = COMMANDS.map(generatedEntry)) {
3180
+ const workflow = workflowCatalog();
3181
+ const workflowNames = new Set(workflow.map((entry) => entry.name));
3182
+ return [...workflow, ...generated.filter((entry) => !workflowNames.has(entry.name))];
3183
+ }
3184
+ export { kebabCase as a, normalizeCommandName as i, COMMANDS as n, COMMAND_ALIASES as o, findCommand as r, commandCatalog as t };