@intx/hub-api 0.1.2

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.
Files changed (55) hide show
  1. package/README.md +29 -0
  2. package/package.json +28 -0
  3. package/src/app.test.ts +225 -0
  4. package/src/app.ts +382 -0
  5. package/src/auth.ts +21 -0
  6. package/src/context.ts +38 -0
  7. package/src/format.ts +9 -0
  8. package/src/git-http/advertise-refs.test.ts +459 -0
  9. package/src/git-http/advertise-refs.ts +226 -0
  10. package/src/git-http/pkt-line.test.ts +220 -0
  11. package/src/git-http/pkt-line.ts +235 -0
  12. package/src/git-http/receive-pack.test.ts +397 -0
  13. package/src/git-http/receive-pack.ts +261 -0
  14. package/src/git-http/side-band-64k.test.ts +181 -0
  15. package/src/git-http/side-band-64k.ts +134 -0
  16. package/src/git-http/upload-pack.test.ts +545 -0
  17. package/src/git-http/upload-pack.ts +396 -0
  18. package/src/index.ts +23 -0
  19. package/src/middleware/git-token-auth.test.ts +587 -0
  20. package/src/middleware/git-token-auth.ts +315 -0
  21. package/src/middleware/grant.ts +106 -0
  22. package/src/middleware/session.ts +13 -0
  23. package/src/middleware/tenant.test.ts +192 -0
  24. package/src/middleware/tenant.ts +101 -0
  25. package/src/openapi.ts +66 -0
  26. package/src/pagination.ts +117 -0
  27. package/src/routes/agent-data.ts +179 -0
  28. package/src/routes/agent-state-git.ts +562 -0
  29. package/src/routes/agents.test.ts +337 -0
  30. package/src/routes/agents.ts +704 -0
  31. package/src/routes/approvals.ts +130 -0
  32. package/src/routes/assets.test.ts +567 -0
  33. package/src/routes/assets.ts +592 -0
  34. package/src/routes/credentials.ts +435 -0
  35. package/src/routes/git-tokens.test.ts +709 -0
  36. package/src/routes/git-tokens.ts +771 -0
  37. package/src/routes/grants.ts +509 -0
  38. package/src/routes/instances.test.ts +1103 -0
  39. package/src/routes/instances.ts +1797 -0
  40. package/src/routes/me.ts +405 -0
  41. package/src/routes/oauth-clients.ts +349 -0
  42. package/src/routes/observability.ts +146 -0
  43. package/src/routes/offerings.ts +382 -0
  44. package/src/routes/principals.ts +515 -0
  45. package/src/routes/providers.ts +351 -0
  46. package/src/routes/roles.ts +452 -0
  47. package/src/routes/sidecars.ts +221 -0
  48. package/src/routes/tenant-federation.ts +225 -0
  49. package/src/routes/tenants.ts +369 -0
  50. package/src/routes/wallets.ts +370 -0
  51. package/src/session.ts +44 -0
  52. package/src/timeline-reconstruction.test.ts +786 -0
  53. package/src/timeline-reconstruction.ts +383 -0
  54. package/tsconfig.json +4 -0
  55. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,562 @@
1
+ /**
2
+ * Agent-state smart-HTTP route group.
3
+ *
4
+ * Two URL grammars exposed under two Hono sub-apps:
5
+ *
6
+ * /api/tenants/:tenantId/agents/instances/:insId/state.git/...
7
+ * -> RepoId { kind: "agent-state", id: insId }
8
+ * (per-instance runtime state, written by the sidecar's first
9
+ * state pack)
10
+ *
11
+ * /api/tenants/:tenantId/agents/definitions/:agtId/state.git/...
12
+ * -> RepoId { kind: "agent-state", id: agtId }
13
+ * (per-definition repo with hub-written deploy artifacts; the
14
+ * `deploy/` prefix is populated by writeDeployTree at instance
15
+ * launch)
16
+ *
17
+ * Both grammars are READ-ONLY over HTTP. Upload-pack
18
+ * (`info/refs?service=git-upload-pack` and `POST /git-upload-pack`)
19
+ * runs behind the same bearer middleware the asset routes use, with
20
+ * a pre-resolved authz verdict on the constructed UserPrincipal.
21
+ * Receive-pack
22
+ * (`info/refs?service=git-receive-pack` and `POST /git-receive-pack`)
23
+ * is denied at the edge with pkt-line-framed responses so a
24
+ * `git push -v` parses the protocol-level rejection even when no
25
+ * Authorization header is present. The receive-pack denial
26
+ * middleware is mounted BEFORE bearer middleware in the app layer;
27
+ * the substrate's `handleReceivePack` is NOT imported here — agent
28
+ * state never accepts writes over HTTP.
29
+ *
30
+ * Both resolvers verify the instance / definition row belongs to
31
+ * `:tenantId` and 404 otherwise. The per-instance repo is
32
+ * lazily-materialised: on a never-pushed instance, `listRefs`
33
+ * returns the empty list and the advertise layer emits the
34
+ * `capabilities^{}` empty-repo record so a stock `git clone`
35
+ * succeeds against an empty tree rather than 404ing.
36
+ */
37
+
38
+ import { and, eq } from "drizzle-orm";
39
+ import { Hono, type Context } from "hono";
40
+ import { createMiddleware } from "hono/factory";
41
+ import type { MiddlewareHandler } from "hono";
42
+
43
+ import { authorize } from "@intx/authz";
44
+ import { agent as agentTable, agentInstance } from "@intx/db/schema";
45
+ import type { DB } from "@intx/db";
46
+ import { repoActionToGrantVerb } from "@intx/hub-common";
47
+ import { getLogger } from "@intx/log";
48
+ import type {
49
+ RefEntry,
50
+ RepoId,
51
+ RepoStore,
52
+ UserPrincipal,
53
+ } from "@intx/hub-sessions";
54
+ import type { RepoAction } from "@intx/types/sidecar";
55
+ import type { ConditionRegistry, GrantStore } from "@intx/types/authz";
56
+
57
+ import type {
58
+ GitTokenClaims,
59
+ TenantGitTokenEnv,
60
+ } from "../middleware/git-token-auth";
61
+ import {
62
+ advertiseUploadPack,
63
+ type RefSource,
64
+ } from "../git-http/advertise-refs";
65
+ import {
66
+ handleUploadPack,
67
+ type UploadPackRepoStore,
68
+ } from "../git-http/upload-pack";
69
+ import { writePktLine, writeFlush } from "../git-http/pkt-line";
70
+
71
+ const log = getLogger(["hub", "agent-state-git"]);
72
+
73
+ // ----- Receive-pack denial: pkt-line responses -----------------------
74
+ //
75
+ // The advertise denial body is locked to:
76
+ //
77
+ // # service=git-receive-pack\n0000ERR agent-state is read-only over HTTP\n
78
+ //
79
+ // The leading `# service=` line and the `0000` flush packet mirror the
80
+ // shape stock git emits for a successful advertise; the trailing
81
+ // `ERR ...` substring is what `git push -v` surfaces as the visible
82
+ // rejection reason. The body is emitted verbatim (not pkt-line
83
+ // framed beyond the literal `0000` flush in the middle).
84
+ const RECEIVE_PACK_ADVERTISE_DENY_BODY =
85
+ "# service=git-receive-pack\n0000ERR agent-state is read-only over HTTP\n";
86
+
87
+ // The POST denial reason that surfaces in the `unpack` and per-ref
88
+ // `ng` pkt-lines. Short, stable, support-recognisable.
89
+ const RECEIVE_PACK_POST_DENY_REASON = "agent-state-readonly";
90
+
91
+ const RECEIVE_PACK_RESULT_CONTENT_TYPE =
92
+ "application/x-git-receive-pack-result";
93
+ const RECEIVE_PACK_ADVERTISEMENT_CONTENT_TYPE =
94
+ "application/x-git-receive-pack-advertisement";
95
+
96
+ function parseHex4(buf: Uint8Array, off: number): number {
97
+ let v = 0;
98
+ for (let i = 0; i < 4; i++) {
99
+ const c = buf[off + i];
100
+ if (c === undefined) {
101
+ throw new Error("truncated pkt-line: short header");
102
+ }
103
+ let d: number;
104
+ if (c >= 0x30 && c <= 0x39) {
105
+ d = c - 0x30;
106
+ } else if (c >= 0x61 && c <= 0x66) {
107
+ d = c - 0x61 + 10;
108
+ } else if (c >= 0x41 && c <= 0x46) {
109
+ d = c - 0x41 + 10;
110
+ } else {
111
+ throw new Error("malformed pkt-line length");
112
+ }
113
+ v = (v << 4) | d;
114
+ }
115
+ return v;
116
+ }
117
+
118
+ /**
119
+ * Minimal parse of the receive-pack request body to extract the list
120
+ * of ref names. The full request grammar includes old/new shas and
121
+ * optional capability tail; we only care about the ref name for the
122
+ * `ng <ref> <reason>` lines. Capabilities and packfile bytes that
123
+ * follow the flush packet are ignored.
124
+ */
125
+ function extractReceivePackRefs(body: Uint8Array): string[] {
126
+ const decoder = new TextDecoder();
127
+ const refs: string[] = [];
128
+ let off = 0;
129
+ while (off + 4 <= body.length) {
130
+ const length = parseHex4(body, off);
131
+ off += 4;
132
+ if (length === 0) {
133
+ // flush: end of commands
134
+ break;
135
+ }
136
+ if (length < 4) {
137
+ throw new Error(`reserved pkt-line length: ${length}`);
138
+ }
139
+ const bodyLen = length - 4;
140
+ if (off + bodyLen > body.length) {
141
+ throw new Error("truncated receive-pack pkt-line body");
142
+ }
143
+ const line = decoder.decode(body.subarray(off, off + bodyLen));
144
+ off += bodyLen;
145
+ // strip optional capability tail after \0 and trailing \n
146
+ const nulIdx = line.indexOf("\0");
147
+ const head = nulIdx === -1 ? line : line.substring(0, nulIdx);
148
+ const trimmed = head.endsWith("\n") ? head.slice(0, -1) : head;
149
+ const parts = trimmed.split(" ");
150
+ if (parts.length < 3) continue;
151
+ const ref = parts.slice(2).join(" ");
152
+ if (ref.length > 0) refs.push(ref);
153
+ }
154
+ return refs;
155
+ }
156
+
157
+ async function writeReceivePackDenyReport(
158
+ writer: WritableStreamDefaultWriter<Uint8Array>,
159
+ refs: readonly string[],
160
+ ): Promise<void> {
161
+ await writePktLine(writer, `unpack ${RECEIVE_PACK_POST_DENY_REASON}\n`);
162
+ for (const ref of refs) {
163
+ await writePktLine(writer, `ng ${ref} ${RECEIVE_PACK_POST_DENY_REASON}\n`);
164
+ }
165
+ await writeFlush(writer);
166
+ }
167
+
168
+ function buildReceivePackPostDenyStream(
169
+ refs: readonly string[],
170
+ ): ReadableStream<Uint8Array> {
171
+ return new ReadableStream<Uint8Array>({
172
+ async start(controller) {
173
+ const sink = new WritableStream<Uint8Array>({
174
+ write(chunk) {
175
+ controller.enqueue(chunk);
176
+ },
177
+ });
178
+ const writer = sink.getWriter();
179
+ try {
180
+ await writeReceivePackDenyReport(writer, refs);
181
+ await writer.close();
182
+ controller.close();
183
+ } catch (cause) {
184
+ await writer.abort(cause).catch(() => undefined);
185
+ controller.error(cause);
186
+ }
187
+ },
188
+ });
189
+ }
190
+
191
+ // ----- Receive-pack deny middleware ---------------------------------
192
+ //
193
+ // Mounted ahead of the bearer middleware. Intercepts:
194
+ // - GET .../info/refs?service=git-receive-pack -> 403 with locked body
195
+ // - POST .../git-receive-pack -> 200 pkt-line denial
196
+ // Other requests (upload-pack info/refs and POST) call next() so the
197
+ // subsequent bearer + upload-pack handlers run normally.
198
+
199
+ function urlPathEndsWith(path: string, suffix: string): boolean {
200
+ return path === suffix || path.endsWith(suffix);
201
+ }
202
+
203
+ /**
204
+ * Hono middleware that denies any receive-pack request reaching the
205
+ * agent-state route surface, regardless of bearer-token presence.
206
+ * Mount BEFORE `createGitTokenAuth` so unauthenticated `git push -v`
207
+ * sees the locked pkt-line ERR rather than a 401.
208
+ */
209
+ export function createAgentStateReceivePackDeny(): MiddlewareHandler {
210
+ return createMiddleware(async (c, next) => {
211
+ const method = c.req.method.toUpperCase();
212
+ const path = c.req.path;
213
+
214
+ if (method === "GET" && urlPathEndsWith(path, "/info/refs")) {
215
+ const service = c.req.query("service");
216
+ if (service === "git-receive-pack") {
217
+ log.info("receive-pack advertise denied {path}", { path });
218
+ return new Response(RECEIVE_PACK_ADVERTISE_DENY_BODY, {
219
+ status: 403,
220
+ headers: {
221
+ "content-type": RECEIVE_PACK_ADVERTISEMENT_CONTENT_TYPE,
222
+ "cache-control": "no-cache",
223
+ },
224
+ });
225
+ }
226
+ // upload-pack advertise: fall through.
227
+ await next();
228
+ return;
229
+ }
230
+
231
+ if (method === "POST" && urlPathEndsWith(path, "/git-receive-pack")) {
232
+ log.info("receive-pack POST denied {path}", { path });
233
+ let refs: string[] = [];
234
+ try {
235
+ const body = new Uint8Array(await c.req.raw.arrayBuffer());
236
+ refs = extractReceivePackRefs(body);
237
+ } catch (err) {
238
+ // Malformed body still gets a deny report with no per-ref lines.
239
+ log.info("receive-pack POST: body parse failed {err}", {
240
+ err: err instanceof Error ? err.message : String(err),
241
+ });
242
+ refs = [];
243
+ }
244
+ const stream = buildReceivePackPostDenyStream(refs);
245
+ return new Response(stream, {
246
+ status: 200,
247
+ headers: {
248
+ "content-type": RECEIVE_PACK_RESULT_CONTENT_TYPE,
249
+ "cache-control": "no-cache",
250
+ },
251
+ });
252
+ }
253
+
254
+ await next();
255
+ });
256
+ }
257
+
258
+ // ----- Pre-resolved authz + UserPrincipal construction --------------
259
+
260
+ function dateToNumber(d: Date): number {
261
+ return d.getTime();
262
+ }
263
+
264
+ async function resolveAuthzVerdict(args: {
265
+ grantStore: GrantStore;
266
+ conditionRegistry: ConditionRegistry;
267
+ principalId: string;
268
+ tenantId: string;
269
+ agentStateId: string;
270
+ action: RepoAction;
271
+ }): Promise<UserPrincipal["authz"]> {
272
+ const resource = `agent-state:${args.agentStateId}`;
273
+ const grantVerb = repoActionToGrantVerb(args.action);
274
+ const verdict = await authorize(
275
+ args.grantStore,
276
+ args.principalId,
277
+ args.tenantId,
278
+ resource,
279
+ grantVerb,
280
+ args.conditionRegistry,
281
+ );
282
+ return {
283
+ effect: verdict.effect === "allow" ? "allow" : "deny",
284
+ resource,
285
+ grantVerb,
286
+ };
287
+ }
288
+
289
+ function buildUserPrincipal(args: {
290
+ principalId: string;
291
+ tenantId: string;
292
+ authz: UserPrincipal["authz"];
293
+ claims: GitTokenClaims;
294
+ }): UserPrincipal {
295
+ return {
296
+ kind: "user",
297
+ principalId: args.principalId,
298
+ tenantId: args.tenantId,
299
+ authz: args.authz,
300
+ tokenClaims: {
301
+ refPattern: args.claims.refPattern,
302
+ actions: args.claims.actions,
303
+ expiresAt: dateToNumber(args.claims.expiresAt),
304
+ },
305
+ };
306
+ }
307
+
308
+ // ----- Substrate adapters -------------------------------------------
309
+
310
+ function makeRefSource(
311
+ repoStore: RepoStore,
312
+ principal: UserPrincipal,
313
+ ): RefSource {
314
+ return {
315
+ async listRefs(_p, repoId): Promise<RefEntry[]> {
316
+ return repoStore.listRefs(principal, repoId);
317
+ },
318
+ async resolveHead(_p, repoId) {
319
+ return repoStore.resolveHead(principal, repoId);
320
+ },
321
+ };
322
+ }
323
+
324
+ function makeUploadPackStore(
325
+ repoStore: RepoStore,
326
+ principal: UserPrincipal,
327
+ ): UploadPackRepoStore {
328
+ return {
329
+ async listRefs(_p, repoId): Promise<RefEntry[]> {
330
+ return repoStore.listRefs(principal, repoId);
331
+ },
332
+ async getRepoDir(_p, repoId): Promise<string> {
333
+ return repoStore.getRepoDir(repoId);
334
+ },
335
+ };
336
+ }
337
+
338
+ // ----- Resolver shape -----------------------------------------------
339
+
340
+ type AgentStateRouteMode = "instance" | "definition";
341
+
342
+ type SmartHttpResolved = {
343
+ principal: UserPrincipal;
344
+ repoId: RepoId;
345
+ };
346
+
347
+ type ResolveResult =
348
+ | { ok: true; resolved: SmartHttpResolved }
349
+ | {
350
+ ok: false;
351
+ status: 400 | 403 | 404;
352
+ code: string;
353
+ message: string;
354
+ };
355
+
356
+ async function resolveAgentStateId(
357
+ db: DB["db"],
358
+ mode: AgentStateRouteMode,
359
+ tenantId: string,
360
+ paramId: string,
361
+ ): Promise<{ ok: true; id: string } | { ok: false; reason: string }> {
362
+ if (mode === "instance") {
363
+ const row = await db.query.agentInstance.findFirst({
364
+ where: and(
365
+ eq(agentInstance.id, paramId),
366
+ eq(agentInstance.tenantId, tenantId),
367
+ ),
368
+ });
369
+ if (row === undefined) {
370
+ return { ok: false, reason: `no instance ${paramId} in tenant` };
371
+ }
372
+ return { ok: true, id: row.id };
373
+ }
374
+ const row = await db.query.agent.findFirst({
375
+ where: and(eq(agentTable.id, paramId), eq(agentTable.tenantId, tenantId)),
376
+ });
377
+ if (row === undefined) {
378
+ return { ok: false, reason: `no agent definition ${paramId} in tenant` };
379
+ }
380
+ return { ok: true, id: row.id };
381
+ }
382
+
383
+ type ResolveSmartHttpDeps = {
384
+ db: DB["db"];
385
+ grantStore: GrantStore;
386
+ conditionRegistry: ConditionRegistry;
387
+ };
388
+
389
+ async function resolveSmartHttp(
390
+ deps: ResolveSmartHttpDeps,
391
+ c: Context<TenantGitTokenEnv>,
392
+ mode: AgentStateRouteMode,
393
+ action: RepoAction,
394
+ ): Promise<ResolveResult> {
395
+ const tenantRow = c.get("tenant");
396
+ const principalRow = c.get("principal");
397
+ const claims: GitTokenClaims = c.get("git-token-claims");
398
+ // The typed env makes this unreachable today, but if the route
399
+ // module is ever mounted without the bearer middleware ahead of
400
+ // it, surface a misconfiguration rather than a downstream
401
+ // TypeError. A 401 would imply the client was unauthenticated;
402
+ // a missing claims object means the server is misconfigured.
403
+ if (claims === undefined) {
404
+ throw new Error(
405
+ "smart-HTTP route handler invoked without bearer middleware; check the mount order in app.ts",
406
+ );
407
+ }
408
+ if (!claims.actions.includes(action)) {
409
+ return {
410
+ ok: false,
411
+ status: 403,
412
+ code: "forbidden",
413
+ message: `token claims do not include action ${action}`,
414
+ };
415
+ }
416
+ const paramName = mode === "instance" ? "instanceId" : "agentId";
417
+ const paramId = c.req.param(paramName);
418
+ if (paramId === undefined) {
419
+ return {
420
+ ok: false,
421
+ status: 400,
422
+ code: "bad_request",
423
+ message: `missing :${paramName} in URL`,
424
+ };
425
+ }
426
+ const tenantId = tenantRow.id;
427
+ const resolved = await resolveAgentStateId(deps.db, mode, tenantId, paramId);
428
+ if (!resolved.ok) {
429
+ return {
430
+ ok: false,
431
+ status: 404,
432
+ code: "not_found",
433
+ message: resolved.reason,
434
+ };
435
+ }
436
+ const authz = await resolveAuthzVerdict({
437
+ grantStore: deps.grantStore,
438
+ conditionRegistry: deps.conditionRegistry,
439
+ principalId: principalRow.id,
440
+ tenantId,
441
+ agentStateId: resolved.id,
442
+ action,
443
+ });
444
+ if (authz.effect !== "allow") {
445
+ log.info(
446
+ "agent-state authz denied {tenantId} {mode}={id} principal={principalId}",
447
+ {
448
+ tenantId,
449
+ mode,
450
+ id: resolved.id,
451
+ principalId: principalRow.id,
452
+ },
453
+ );
454
+ return {
455
+ ok: false,
456
+ status: 403,
457
+ code: "forbidden",
458
+ message: "authz denied",
459
+ };
460
+ }
461
+ const principal = buildUserPrincipal({
462
+ principalId: principalRow.id,
463
+ tenantId,
464
+ authz,
465
+ claims,
466
+ });
467
+ const repoId: RepoId = { kind: "agent-state", id: resolved.id };
468
+ return { ok: true, resolved: { principal, repoId } };
469
+ }
470
+
471
+ // ----- Upload-pack route factory ------------------------------------
472
+
473
+ export type CreateAgentStateGitRoutesDeps = {
474
+ db: DB["db"];
475
+ repoStore: RepoStore;
476
+ grantStore: GrantStore;
477
+ conditionRegistry: ConditionRegistry;
478
+ };
479
+
480
+ function createAgentStateGitRoutes(
481
+ deps: CreateAgentStateGitRoutesDeps,
482
+ mode: AgentStateRouteMode,
483
+ ): Hono<TenantGitTokenEnv> {
484
+ const app = new Hono<TenantGitTokenEnv>();
485
+ const paramSeg = mode === "instance" ? ":instanceId" : ":agentId";
486
+
487
+ app.get(`/${paramSeg}/state.git/info/refs`, async (c) => {
488
+ const service = c.req.query("service");
489
+ if (service !== "git-upload-pack") {
490
+ // The receive-pack case is handled by the deny middleware above;
491
+ // anything else is a bad request.
492
+ return c.json(
493
+ {
494
+ error: {
495
+ code: "bad_request",
496
+ message: "info/refs requires service=git-upload-pack",
497
+ },
498
+ },
499
+ 400,
500
+ );
501
+ }
502
+ const r = await resolveSmartHttp(deps, c, mode, "resolveRef");
503
+ if (!r.ok) {
504
+ return c.json({ error: { code: r.code, message: r.message } }, r.status);
505
+ }
506
+ const refSource = makeRefSource(deps.repoStore, r.resolved.principal);
507
+ const stream = await advertiseUploadPack(
508
+ refSource,
509
+ r.resolved.principal,
510
+ r.resolved.repoId,
511
+ );
512
+ return new Response(stream, {
513
+ status: 200,
514
+ headers: {
515
+ "content-type": "application/x-git-upload-pack-advertisement",
516
+ "cache-control": "no-cache",
517
+ },
518
+ });
519
+ });
520
+
521
+ app.post(`/${paramSeg}/state.git/git-upload-pack`, async (c) => {
522
+ const r = await resolveSmartHttp(deps, c, mode, "createPack");
523
+ if (!r.ok) {
524
+ return c.json({ error: { code: r.code, message: r.message } }, r.status);
525
+ }
526
+ return handleUploadPack(
527
+ makeUploadPackStore(deps.repoStore, r.resolved.principal),
528
+ r.resolved.principal,
529
+ r.resolved.repoId,
530
+ c.req.raw,
531
+ );
532
+ });
533
+
534
+ return app;
535
+ }
536
+
537
+ export function createAgentStateInstanceGitRoutes(
538
+ deps: CreateAgentStateGitRoutesDeps,
539
+ ): Hono<TenantGitTokenEnv> {
540
+ return createAgentStateGitRoutes(deps, "instance");
541
+ }
542
+
543
+ export function createAgentStateDefinitionGitRoutes(
544
+ deps: CreateAgentStateGitRoutesDeps,
545
+ ): Hono<TenantGitTokenEnv> {
546
+ return createAgentStateGitRoutes(deps, "definition");
547
+ }
548
+
549
+ /**
550
+ * Smart-HTTP paths excluded from the OpenAPI document. The agent-state
551
+ * routes serve binary git wire vocabulary; advertising them in the
552
+ * generated spec would invite client codegen to treat them as JSON
553
+ * endpoints.
554
+ */
555
+ export const AGENT_STATE_OPENAPI_EXCLUDE_GLOBS = [
556
+ "/api/tenants/*/agents/instances/*/state.git/info/refs",
557
+ "/api/tenants/*/agents/instances/*/state.git/git-upload-pack",
558
+ "/api/tenants/*/agents/instances/*/state.git/git-receive-pack",
559
+ "/api/tenants/*/agents/definitions/*/state.git/info/refs",
560
+ "/api/tenants/*/agents/definitions/*/state.git/git-upload-pack",
561
+ "/api/tenants/*/agents/definitions/*/state.git/git-receive-pack",
562
+ ] as const;