@nylorun/runtime 0.29.0-beta → 0.31.0-beta

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 (49) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/api/components.d.ts +240 -2
  3. package/dist/api/components.js +3 -1
  4. package/dist/api/http/routes/agent-manifests.d.ts +2 -1
  5. package/dist/api/http/routes/agent-manifests.js +62 -10
  6. package/dist/api/openapi.js +2 -0
  7. package/dist/gates/egress-credentials.js +2 -13
  8. package/dist/gates/handler.js +2 -1
  9. package/dist/gates/in-process.d.ts +9 -0
  10. package/dist/gates/in-process.js +21 -0
  11. package/dist/host/http.js +3 -1
  12. package/dist/host/main.js +2 -0
  13. package/dist/host/stack-config.d.ts +10 -0
  14. package/dist/host/stack-config.js +36 -0
  15. package/dist/management-openapi.json +361 -5
  16. package/dist/mcp/connect.d.ts +34 -5
  17. package/dist/mcp/connect.js +75 -14
  18. package/dist/mcp/gateway-outcomes.d.ts +14 -0
  19. package/dist/mcp/gateway-outcomes.js +46 -0
  20. package/dist/mcp/pool.js +5 -0
  21. package/dist/model/call-headers.d.ts +15 -0
  22. package/dist/model/call-headers.js +9 -0
  23. package/dist/model/models.d.ts +7 -0
  24. package/dist/model/models.js +3 -0
  25. package/dist/model/pi-model.d.ts +6 -0
  26. package/dist/model/pi-model.js +29 -3
  27. package/dist/openapi.json +654 -30
  28. package/dist/store/postgres/drizzle/0038_t6_agent_manifest_source.sql +1 -0
  29. package/dist/store/postgres/drizzle/meta/_journal.json +7 -0
  30. package/dist/store/postgres/schema.d.ts +21 -2
  31. package/dist/store/postgres/schema.js +7 -1
  32. package/dist/store/postgres/store.js +1 -0
  33. package/dist/store/types.d.ts +14 -0
  34. package/dist/tenant/agent-manifests.d.ts +18 -25
  35. package/dist/tenant/agent-manifests.js +136 -4
  36. package/dist/tenant/agents.js +7 -3
  37. package/dist/tenant/transcript.d.ts +8 -0
  38. package/dist/tenant/transcript.js +2 -0
  39. package/dist/vault/headers.d.ts +8 -0
  40. package/dist/vault/headers.js +25 -0
  41. package/dist/vault/host-model.js +8 -5
  42. package/dist/vault/service.d.ts +12 -5
  43. package/dist/vault/service.js +43 -6
  44. package/dist/vault/sources.js +22 -1
  45. package/dist/vault/workload-token.d.ts +16 -0
  46. package/dist/vault/workload-token.js +58 -0
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +3 -3
@@ -0,0 +1 @@
1
+ ALTER TABLE "nylorun"."agent_manifests" ADD COLUMN "source" json;
@@ -267,6 +267,13 @@
267
267
  "when": 1792238141315,
268
268
  "tag": "0037_model_usage_cost_split",
269
269
  "breakpoints": true
270
+ },
271
+ {
272
+ "idx": 38,
273
+ "version": "7",
274
+ "when": 1792484344984,
275
+ "tag": "0038_t6_agent_manifest_source",
276
+ "breakpoints": true
270
277
  }
271
278
  ]
272
279
  }
@@ -2569,6 +2569,25 @@ export declare const agentManifests: import("drizzle-orm/pg-core").PgTableWithCo
2569
2569
  }, {}, {
2570
2570
  pgColumnBuilderBrand: 'PgCustomColumnBuilderBrand';
2571
2571
  }>;
2572
+ source: import("drizzle-orm/pg-core").PgColumn<{
2573
+ name: "source";
2574
+ tableName: "agent_manifests";
2575
+ dataType: "custom";
2576
+ columnType: "PgCustomColumn";
2577
+ data: unknown;
2578
+ driverParam: unknown;
2579
+ notNull: false;
2580
+ hasDefault: false;
2581
+ isPrimaryKey: false;
2582
+ isAutoincrement: false;
2583
+ hasRuntimeDefault: false;
2584
+ enumValues: undefined;
2585
+ baseColumn: never;
2586
+ identity: undefined;
2587
+ generated: undefined;
2588
+ }, {}, {
2589
+ pgColumnBuilderBrand: 'PgCustomColumnBuilderBrand';
2590
+ }>;
2572
2591
  };
2573
2592
  dialect: 'pg';
2574
2593
  }>;
@@ -2737,14 +2756,14 @@ export declare const vaultCredentials: import("drizzle-orm/pg-core").PgTableWith
2737
2756
  tableName: "vault_credentials";
2738
2757
  dataType: "string";
2739
2758
  columnType: "PgText";
2740
- data: "bearer" | "environment_secret" | "environment_variable" | "headers" | "model";
2759
+ data: "bearer" | "environment_secret" | "environment_variable" | "headers" | "model" | "workload";
2741
2760
  driverParam: string;
2742
2761
  notNull: true;
2743
2762
  hasDefault: false;
2744
2763
  isPrimaryKey: false;
2745
2764
  isAutoincrement: false;
2746
2765
  hasRuntimeDefault: false;
2747
- enumValues: ["bearer", "headers", "environment_secret", "environment_variable", "model"];
2766
+ enumValues: ["bearer", "headers", "workload", "environment_secret", "environment_variable", "model"];
2748
2767
  baseColumn: never;
2749
2768
  identity: undefined;
2750
2769
  generated: undefined;
@@ -326,6 +326,12 @@ export const agentManifests = nylorun.table("agent_manifests", {
326
326
  /** `{ actor }`: the subject the save acted for, else `application`. */
327
327
  savedBy: jsonText().notNull(),
328
328
  createdAt: textC().notNull(),
329
+ /**
330
+ * The agent folder the version was compiled from (track T6): `{ compiler, files, bytes,
331
+ * entries }`, each entry `{ path, size, sha256 }` of a definition file. Null for a version
332
+ * saved as JSON.
333
+ */
334
+ source: jsonText(),
329
335
  }, (t) => [primaryKey({ name: "agent_manifests_pkey", columns: [t.name, t.version] })]);
330
336
  /**
331
337
  * A vault (D56): the host model's (`id = 'host'`), or one Environment's, created and deleted with
@@ -353,7 +359,7 @@ export const vaultCredentials = nylorun.table("vault_credentials", {
353
359
  id: textC().primaryKey(),
354
360
  vaultId: text().notNull(),
355
361
  name: text().notNull(),
356
- type: text({ enum: ["bearer", "headers", "environment_secret", "environment_variable", "model"] }).notNull(),
362
+ type: text({ enum: ["bearer", "headers", "workload", "environment_secret", "environment_variable", "model"] }).notNull(),
357
363
  /** JSON text of the non-secret binding: url (and via, identity, header names), or provider and model. */
358
364
  bindingJson: text().notNull(),
359
365
  expiresAt: text(),
@@ -295,6 +295,7 @@ function agentManifestOf(row) {
295
295
  hash: row.hash,
296
296
  savedBy: row.savedBy,
297
297
  createdAt: row.createdAt,
298
+ ...(row.source === null || row.source === undefined ? {} : { source: row.source }),
298
299
  };
299
300
  }
300
301
  /** A pod patch as column values: ISO times become dates, `null` clears. */
@@ -204,6 +204,20 @@ export interface AgentManifestRow {
204
204
  actor: string;
205
205
  };
206
206
  createdAt: string;
207
+ /** The agent folder the version was compiled from (track T6); none for a version saved as JSON. */
208
+ source?: AgentManifestSourceRecord;
209
+ }
210
+ /** An agent folder as a version keeps it: its compiler, size and files, each a definition file. */
211
+ export interface AgentManifestSourceRecord {
212
+ compiler: string;
213
+ files: number;
214
+ bytes: number;
215
+ /** Sorted by path; `sha256` is lowercase hex, the file `sha256:<hex>` in the definition file store. */
216
+ entries: {
217
+ path: string;
218
+ size: number;
219
+ sha256: string;
220
+ }[];
207
221
  }
208
222
  /** A disk lease (R2d step 3): what holds the disk, since when, and until when unless renewed. */
209
223
  export interface DiskOperation {
@@ -1,28 +1,4 @@
1
- /**
2
- * Agent manifests (protocol 16, track T2): `/v1/agent-manifests/{name}`, named and versioned
3
- * documents with what runs inside an agent's loop (`harness`) and what the Runtime provides
4
- * around it (`runtime`). They replace agent definitions (`PUT /v1/agents/:id` before protocol
5
- * 16) and environment manifests.
6
- *
7
- * - **Versions.** A save whose manifest hashes the same as the latest answers it unchanged; any
8
- * other writes the next version. A version is never changed or deleted alone.
9
- * - **Tracking Agents move.** Saving a version moves every Agent that tracks the manifest to it
10
- * (track T3, `moveAgent`), in the save's transaction: at once, at its pod's next start, or once
11
- * its required secrets are set. A pinned Agent stays, and so does one the version cannot move
12
- * (a fixed field) or whose start failed on it. The plan counts both.
13
- * - **Both sections at once.** The Runtime sees both sections, so it checks them against each
14
- * other (`sectionGaps`): every gap is named in one `422 manifest_inconsistent`, which is what
15
- * `requires` checked when a session opened before protocol 16. Each section's own checks run
16
- * too: the harness's (no code tools, no approvals, HTTP tool timeouts, the skill files) and the
17
- * runtime's (the Tenant's limits, its files).
18
- * - **Plan and drift.** `?dryRun=true` answers what a save would do and saves nothing: the
19
- * changes from the latest by section, every reason the Runtime would refuse it, warnings, the
20
- * Agents it moves and how many stay on older versions. `If-Match: <version>` refuses
21
- * (`412 manifest_changed`) a save whose latest version moved since the client read it; `0` asks
22
- * for a manifest that does not exist yet.
23
- * - **Every client is equal** (Q12): a version records who saved it (`savedBy`).
24
- */
25
- import { type AgentManifestChange, type AgentManifestPlan, type AgentManifestV8, type AgentManifestVersions, type AgentManifestView, type PutAgentManifestRequest, type SaveAgentManifestResponse } from "@nylorun/core/contracts";
1
+ import { type AgentManifestChange, type AgentManifestPlan, type AgentManifestSource, type AgentManifestV8, type AgentManifestVersions, type AgentManifestView, type PutAgentFolderRequest, type PutAgentManifestRequest, type SaveAgentManifestResponse } from "@nylorun/core/contracts";
26
2
  import { type RecipeRequest } from "../sandbox/resolve.js";
27
3
  import type { AuthScope, TenantContext } from "./context.js";
28
4
  /** What a manifest's runtime section asks of an Agent's recipe, with the harness its loop names. */
@@ -54,6 +30,23 @@ export declare function saveAgentManifest(ctx: TenantContext, name: string, body
54
30
  dryRun?: false;
55
31
  ifMatch?: number;
56
32
  }): Promise<SaveAgentManifestResponse>;
33
+ /**
34
+ * `PUT /v1/agent-manifests/{name}` with an agent folder by hash (track T6): reads the files the
35
+ * compiler needs from the definition file store, compiles the folder (`422 folder_invalid` when
36
+ * it does not), then saves or plans the manifest as a JSON save does, keeping the folder as the
37
+ * version's source.
38
+ */
39
+ export declare function saveAgentFolder(ctx: TenantContext, name: string, body: PutAgentFolderRequest, scope: AuthScope, options?: {
40
+ dryRun?: boolean;
41
+ ifMatch?: number;
42
+ }): Promise<AgentManifestPlan | SaveAgentManifestResponse>;
43
+ /** `GET /v1/agent-manifests/{name}/versions/{v}/source`: the folder a version was compiled from. */
44
+ export declare function getAgentManifestSource(ctx: TenantContext, name: string, version: number): Promise<AgentManifestSource>;
45
+ /** `GET .../versions/{v}/source/files/{path}`: one file of a version's source, streamed. */
46
+ export declare function readAgentManifestSourceFile(ctx: TenantContext, name: string, version: number, path: string): Promise<{
47
+ body: ReadableStream<Uint8Array>;
48
+ size: number;
49
+ }>;
57
50
  /** `GET /v1/agent-manifests/{name}` (the latest) and `.../versions/{n}`. */
58
51
  export declare function getAgentManifest(ctx: TenantContext, name: string, version?: number): Promise<AgentManifestView>;
59
52
  /** `GET /v1/agent-manifests`: each manifest's latest version. */
@@ -21,7 +21,15 @@
21
21
  * (`412 manifest_changed`) a save whose latest version moved since the client read it; `0` asks
22
22
  * for a manifest that does not exist yet.
23
23
  * - **Every client is equal** (Q12): a version records who saved it (`savedBy`).
24
+ * - **Agent folders** (track T6, D79): the same route takes a folder by hash
25
+ * (`application/vnd.nylorun.folder+json`). The Runtime reads the files the compiler needs from
26
+ * the definition file store, compiles them with `@nylorun/core/folder` (a folder that does not
27
+ * compile is `422 folder_invalid`, every diagnostic in `details.diagnostics`), and saves the
28
+ * manifest as above, with the folder kept as the version's source (`source`, its files recorded
29
+ * as the version's definition files). A folder whose manifest is unchanged saves no new
30
+ * version, so its source is not saved either.
24
31
  */
32
+ import { compileAgentFolder, folderTextPaths, AGENT_FOLDER_LIMITS, } from "@nylorun/core/folder";
25
33
  import { definitionFilesOf, } from "@nylorun/core/contracts";
26
34
  import { hashManifest } from "@nylorun/core/compatibility";
27
35
  import { approvalRefusal, codeToolRefusal, harnessIdOf, loopManifestOf, runtimeOf, sectionGaps, } from "@nylorun/core/define";
@@ -35,7 +43,7 @@ import { readRuntimeLimits } from "./runtime-limits.js";
35
43
  import { effectiveSandboxConfig, readSandboxConfig } from "../sandbox/tenant-config.js";
36
44
  import { actorOf, fileErrors } from "./agents.js";
37
45
  import { moveAgent, planMove } from "./agent-versions.js";
38
- import { definitionFileIssues } from "./definition-files.js";
46
+ import { definitionFileIssues, openDefinitionFile, readDefinitionFile } from "./definition-files.js";
39
47
  import { fail } from "./http.js";
40
48
  /** What a manifest's runtime section asks of an Agent's recipe, with the harness its loop names. */
41
49
  export function manifestRecipeRequest(manifest) {
@@ -73,6 +81,9 @@ function viewOf(row, counts) {
73
81
  savedBy: row.savedBy,
74
82
  createdAt: row.createdAt,
75
83
  agents: counts.get(row.version) ?? 0,
84
+ ...(row.source === undefined
85
+ ? {}
86
+ : { source: { compiler: row.source.compiler, files: row.source.files, bytes: row.source.bytes } }),
76
87
  };
77
88
  }
78
89
  /** Each field that differs between `before` and `after`, by path; arrays compare whole. */
@@ -237,6 +248,9 @@ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
237
248
  const { requestId: _, ...manifest } = body;
238
249
  if (manifest.name !== name)
239
250
  fail(400, `The manifest is named ${manifest.name}, and the path names ${name}: they must match.`);
251
+ return saveManifest(ctx, name, manifest, scope, options);
252
+ }
253
+ async function saveManifest(ctx, name, manifest, scope, options, folder) {
240
254
  const hash = agentManifestHash(manifest);
241
255
  const gaps = sectionGaps(manifest);
242
256
  return ctx.store.tx(async (t) => {
@@ -248,12 +262,33 @@ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
248
262
  : `Agent manifest ${name} is at version ${latest.version}, not ${options.ifMatch}: someone saved it since you read it. Read it again, or save without If-Match to replace it.`, { code: "manifest_changed", details: { latest: latest?.version ?? 0 } });
249
263
  const unchanged = latest?.hash === hash;
250
264
  const to = unchanged ? latest.version : (latest?.version ?? 0) + 1;
265
+ // A folder whose manifest is unchanged keeps the latest version, and with it that version's source.
266
+ const notes = folder === undefined
267
+ ? undefined
268
+ : unchanged && canonical(latest?.source?.entries ?? null) !== canonical(folder.source.entries)
269
+ ? [
270
+ ...folder.diagnostics,
271
+ {
272
+ severity: "note",
273
+ code: "source_not_saved",
274
+ message: "Source files changed that are not part of the agent; the manifest is unchanged, so no version is saved",
275
+ },
276
+ ]
277
+ : folder.diagnostics;
251
278
  const counts = await t.agentsOfManifest(name);
252
279
  const checked = await errorsOf(t, manifest);
253
280
  // The version as it would be saved, for the plan of the Agents it moves.
254
281
  const row = unchanged
255
282
  ? latest
256
- : { name, version: to, body: manifest, hash, savedBy: { actor: actorOf(scope) }, createdAt: new Date().toISOString() };
283
+ : {
284
+ name,
285
+ version: to,
286
+ body: manifest,
287
+ hash,
288
+ savedBy: { actor: actorOf(scope) },
289
+ createdAt: new Date().toISOString(),
290
+ ...(folder === undefined ? {} : { source: folder.source }),
291
+ };
257
292
  if (options.dryRun) {
258
293
  const errors = [...gaps, ...checked.errors];
259
294
  // A save with errors moves nothing; one that changes nothing moves no Agent either.
@@ -268,6 +303,7 @@ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
268
303
  errors,
269
304
  behind: moves?.behind ?? (await agentsBehind(t, name, to)).length,
270
305
  moves: { now: moves?.now ?? 0, nextStart: moves?.nextStart ?? 0, secrets: moves?.secrets ?? 0 },
306
+ ...(folder === undefined ? {} : { manifest, diagnostics: [...notes], compiler: folder.compiler }),
271
307
  };
272
308
  }
273
309
  if (gaps.length > 0)
@@ -287,8 +323,9 @@ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
287
323
  });
288
324
  if (checked.errors.length > 0)
289
325
  fail(400, checked.errors.join(" "));
326
+ const folderNotes = notes === undefined ? {} : { diagnostics: [...notes] };
290
327
  if (unchanged)
291
- return { manifest: viewOf(latest, counts), unchanged: true, warnings: warningsOf(ctx, manifest, to, undefined) };
328
+ return { manifest: viewOf(latest, counts), unchanged: true, warnings: warningsOf(ctx, manifest, to, undefined), ...folderNotes };
292
329
  await t.insertAgentManifest(row);
293
330
  // Its files by hash, skills' and runtime files' alike, for the sweep that deletes unused ones.
294
331
  const shas = new Set(definitionFilesOf(loopManifestOf(manifest)));
@@ -296,13 +333,108 @@ export async function saveAgentManifest(ctx, name, body, scope, options = {}) {
296
333
  if ("files" in file)
297
334
  for (const sha of Object.values(file.files))
298
335
  shas.add(sha);
336
+ // A folder's every file too: the version keeps its source.
337
+ for (const entry of folder?.source.entries ?? [])
338
+ shas.add(`sha256:${entry.sha256}`);
299
339
  if (shas.size > 0)
300
340
  await t.insertDefinitionFileUses(manifestFileOwner(name), hash, [...shas]);
301
341
  // Every Agent that tracks the manifest moves to the new version (track T3).
302
342
  const moves = await moveTracking(t, row, true);
303
- return { manifest: viewOf(row, await t.agentsOfManifest(name)), unchanged: false, warnings: warningsOf(ctx, manifest, to, moves) };
343
+ return {
344
+ manifest: viewOf(row, await t.agentsOfManifest(name)),
345
+ unchanged: false,
346
+ warnings: warningsOf(ctx, manifest, to, moves),
347
+ ...folderNotes,
348
+ };
349
+ });
350
+ }
351
+ /**
352
+ * `PUT /v1/agent-manifests/{name}` with an agent folder by hash (track T6): reads the files the
353
+ * compiler needs from the definition file store, compiles the folder (`422 folder_invalid` when
354
+ * it does not), then saves or plans the manifest as a JSON save does, keeping the folder as the
355
+ * version's source.
356
+ */
357
+ export async function saveAgentFolder(ctx, name, body, scope, options = {}) {
358
+ const entries = [...body.entries].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
359
+ if (entries.length > AGENT_FOLDER_LIMITS.files)
360
+ fail(413, `An agent folder holds at most ${AGENT_FOLDER_LIMITS.files} files; this one holds ${entries.length}`, {
361
+ code: "limit_exceeded",
362
+ details: { limit: AGENT_FOLDER_LIMITS.files },
363
+ });
364
+ // Every file must be held: the version keeps them all as its source.
365
+ const shas = [...new Set(entries.map((entry) => `sha256:${entry.sha256}`))].sort();
366
+ const held = await ctx.store.tx((t) => t.heldDefinitionFiles(shas));
367
+ const missing = entries.filter((entry) => !held.has(`sha256:${entry.sha256}`));
368
+ if (missing.length > 0)
369
+ fail(400, `The folder names ${missing.length} file(s) the Runtime does not hold; upload each with PUT /v1/files/sha256:<hex> first: ${missing
370
+ .slice(0, 20)
371
+ .map((entry) => entry.path)
372
+ .join(", ")}`, { code: "definition_files_missing", details: { missing: missing.map((entry) => `sha256:${entry.sha256}`) } });
373
+ for (const entry of entries)
374
+ if (held.get(`sha256:${entry.sha256}`) !== entry.size)
375
+ fail(400, `${entry.path} is listed with ${entry.size} bytes, and the Runtime holds ${held.get(`sha256:${entry.sha256}`)} for its hash`);
376
+ // The files the compiler reads, each at most the text file limit (it reports a larger one).
377
+ const files = new Map();
378
+ for (const path of folderTextPaths(entries)) {
379
+ const entry = entries.find((item) => item.path === path);
380
+ if (entry.size > AGENT_FOLDER_LIMITS.textFileBytes)
381
+ continue;
382
+ const bytes = await readDefinitionFile(ctx.blobs, `sha256:${entry.sha256}`);
383
+ if (bytes)
384
+ files.set(path, bytes);
385
+ }
386
+ const compiled = compileAgentFolder({ entries, files, name });
387
+ if (!compiled.manifest) {
388
+ const errors = compiled.diagnostics.filter((item) => item.severity === "error");
389
+ fail(422, `Agent folder ${name} does not compile: ${errors.length} error(s), the first: ${located(errors[0])}`, {
390
+ code: "folder_invalid",
391
+ details: { diagnostics: compiled.diagnostics },
392
+ });
393
+ }
394
+ const source = {
395
+ compiler: compiled.compiler,
396
+ files: entries.length,
397
+ bytes: entries.reduce((sum, entry) => sum + entry.size, 0),
398
+ entries: entries.map((entry) => ({ path: entry.path, size: entry.size, sha256: entry.sha256 })),
399
+ };
400
+ return saveManifest(ctx, name, compiled.manifest, scope, options, {
401
+ source,
402
+ diagnostics: compiled.diagnostics,
403
+ compiler: compiled.compiler,
304
404
  });
305
405
  }
406
+ /** A diagnostic as a person reads it: `file:line:column message`. */
407
+ function located(diagnostic) {
408
+ if (!diagnostic)
409
+ return "unknown";
410
+ const where = diagnostic.file
411
+ ? `${diagnostic.file}${diagnostic.line ? `:${diagnostic.line}${diagnostic.column ? `:${diagnostic.column}` : ""}` : ""} `
412
+ : "";
413
+ return `${where}${diagnostic.message}`;
414
+ }
415
+ /** The source of version `version` of manifest `name`, or a `404` for one saved as JSON. */
416
+ async function sourceOf(ctx, name, version) {
417
+ const row = (await ctx.store.tx((t) => t.agentManifest(name, version))) ?? notFound(name, version);
418
+ if (!row.source)
419
+ fail(404, `Version ${version} of agent manifest ${name} was saved as JSON: it has no source.`, { code: "not_found" });
420
+ return row.source;
421
+ }
422
+ /** `GET /v1/agent-manifests/{name}/versions/{v}/source`: the folder a version was compiled from. */
423
+ export async function getAgentManifestSource(ctx, name, version) {
424
+ const source = await sourceOf(ctx, name, version);
425
+ return { name, version, ...source };
426
+ }
427
+ /** `GET .../versions/{v}/source/files/{path}`: one file of a version's source, streamed. */
428
+ export async function readAgentManifestSourceFile(ctx, name, version, path) {
429
+ const source = await sourceOf(ctx, name, version);
430
+ const entry = source.entries.find((item) => item.path === path);
431
+ if (!entry)
432
+ fail(404, `Version ${version} of agent manifest ${name} has no file ${path}.`, { code: "not_found" });
433
+ const body = await openDefinitionFile(ctx.blobs, `sha256:${entry.sha256}`);
434
+ if (!body)
435
+ fail(404, `The Runtime no longer holds ${path} of version ${version} of agent manifest ${name}.`, { code: "not_found" });
436
+ return { body: body, size: entry.size };
437
+ }
306
438
  /** `GET /v1/agent-manifests/{name}` (the latest) and `.../versions/{n}`. */
307
439
  export async function getAgentManifest(ctx, name, version) {
308
440
  return ctx.store.tx(async (t) => {
@@ -562,15 +562,19 @@ async function parseSecretRequest(ctx, id, name, raw) {
562
562
  const full = slotRequest(slot);
563
563
  if (raw !== null && typeof raw === "object" && "type" in raw) {
564
564
  const body = PutAgentSecretRequestSchema.parse(raw);
565
- const { requestId: _, value: _v, headers: _h, ...binding } = body;
565
+ // The gateway's tool prefix is this installation's to add (`gateway-credentials`).
566
+ const { requestId: _, value: _v, headers: _h, toolPrefix: _p, ...binding } = body;
566
567
  if (canonical(binding) !== canonical(full))
567
568
  fail(409, `Secret ${name}'s type and binding come from Agent ${id}'s manifest: send only its value ({ "value": "…" }).`, {
568
569
  code: "secret_not_in_manifest",
569
570
  });
570
571
  return body;
571
572
  }
572
- const { requestId, value, headers } = PutAgentSecretValueRequestSchema.parse(raw);
573
- const withId = requestId === undefined ? {} : { requestId };
573
+ const { requestId, value, headers, toolPrefix } = PutAgentSecretValueRequestSchema.parse(raw);
574
+ const withId = {
575
+ ...(requestId === undefined ? {} : { requestId }),
576
+ ...(toolPrefix === undefined ? {} : { toolPrefix }),
577
+ };
574
578
  if (slot.type === "headers") {
575
579
  if (headers === undefined)
576
580
  fail(400, `Secret ${name} is a headers credential: send { "headers": { … } }.`);
@@ -186,6 +186,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
186
186
  code: string;
187
187
  message: string;
188
188
  server?: string | undefined;
189
+ gateway?: string | undefined;
189
190
  retryable?: boolean | undefined;
190
191
  };
191
192
  output?: undefined;
@@ -208,6 +209,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
208
209
  code: string;
209
210
  message: string;
210
211
  server?: string | undefined;
212
+ gateway?: string | undefined;
211
213
  retryable?: boolean | undefined;
212
214
  };
213
215
  output?: undefined;
@@ -230,6 +232,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
230
232
  code: string;
231
233
  message: string;
232
234
  server?: string | undefined;
235
+ gateway?: string | undefined;
233
236
  retryable?: boolean | undefined;
234
237
  };
235
238
  output?: undefined;
@@ -252,6 +255,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
252
255
  code: string;
253
256
  message: string;
254
257
  server?: string | undefined;
258
+ gateway?: string | undefined;
255
259
  retryable?: boolean | undefined;
256
260
  };
257
261
  output?: undefined;
@@ -344,6 +348,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
344
348
  code: string;
345
349
  message: string;
346
350
  server?: string | undefined;
351
+ gateway?: string | undefined;
347
352
  retryable?: boolean | undefined;
348
353
  };
349
354
  output?: undefined;
@@ -368,6 +373,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
368
373
  code: string;
369
374
  message: string;
370
375
  server?: string | undefined;
376
+ gateway?: string | undefined;
371
377
  retryable?: boolean | undefined;
372
378
  };
373
379
  output?: undefined;
@@ -392,6 +398,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
392
398
  code: string;
393
399
  message: string;
394
400
  server?: string | undefined;
401
+ gateway?: string | undefined;
395
402
  retryable?: boolean | undefined;
396
403
  };
397
404
  output?: undefined;
@@ -416,6 +423,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown, facts
416
423
  code: string;
417
424
  message: string;
418
425
  server?: string | undefined;
426
+ gateway?: string | undefined;
419
427
  retryable?: boolean | undefined;
420
428
  };
421
429
  output?: undefined;
@@ -151,6 +151,8 @@ export function toolCompleted(request, value, facts) {
151
151
  message: String(outcome.message ?? ""),
152
152
  // `credential_rejected` (R2b C1): which server.
153
153
  ...(typeof outcome.server === "string" ? { server: outcome.server } : {}),
154
+ // And the gateway that refused the Runtime's workload token (#496).
155
+ ...(typeof outcome.gateway === "string" ? { gateway: outcome.gateway } : {}),
154
156
  ...(typeof outcome.retryable === "boolean" ? { retryable: outcome.retryable } : {}),
155
157
  },
156
158
  }
@@ -1,3 +1,11 @@
1
+ /** Headers that describe one hop and are never forwarded or set by a credential or a setting. */
2
+ export declare const HOP_BY_HOP_HEADERS: ReadonlySet<string>;
3
+ /**
4
+ * Why a custom model endpoint's header (`settings.headers`, `settings.callHeaders`, #493) may not
5
+ * be `name`, or undefined when it may: the API key's `authorization`, a hop-by-hop header, a
6
+ * transport header or `Nylorun-*`.
7
+ */
8
+ export declare function modelHeaderError(name: string): string | undefined;
1
9
  /**
2
10
  * The secret values of a credential's `headers`, as a server could echo them (R2b C8, Q16): each
3
11
  * value of at least 8 characters, and the token of a `Bearer …` value. The identity header names
@@ -18,6 +18,31 @@ const RESERVED_HEADERS = new Set([
18
18
  "last-event-id",
19
19
  "idempotency-key",
20
20
  ]);
21
+ /** Headers that describe one hop and are never forwarded or set by a credential or a setting. */
22
+ export const HOP_BY_HOP_HEADERS = new Set([
23
+ "connection",
24
+ "keep-alive",
25
+ "proxy-authenticate",
26
+ "proxy-authorization",
27
+ "proxy-connection",
28
+ "te",
29
+ "trailer",
30
+ "transfer-encoding",
31
+ "upgrade",
32
+ ]);
33
+ /**
34
+ * Why a custom model endpoint's header (`settings.headers`, `settings.callHeaders`, #493) may not
35
+ * be `name`, or undefined when it may: the API key's `authorization`, a hop-by-hop header, a
36
+ * transport header or `Nylorun-*`.
37
+ */
38
+ export function modelHeaderError(name) {
39
+ const lower = name.toLowerCase();
40
+ if (lower === "authorization")
41
+ return "A model header may not set authorization: the model's auth sends it";
42
+ if (HOP_BY_HOP_HEADERS.has(lower) || isReservedHeader(lower))
43
+ return `A model header may not set ${lower}`;
44
+ return undefined;
45
+ }
21
46
  /**
22
47
  * The secret values of a credential's `headers`, as a server could echo them (R2b C8, Q16): each
23
48
  * value of at least 8 characters, and the token of a `Bearer …` value. The identity header names
@@ -24,11 +24,14 @@ export class HostModelVault {
24
24
  const payload = readHostPayload(this.kek(), row, binding);
25
25
  const credential = binding.authType === "oauth"
26
26
  ? { type: "oauth", ...payload.oauth }
27
- : {
28
- type: "api_key",
29
- key: payload.apiKey,
30
- ...(payload.env ? { env: payload.env } : {}),
31
- };
27
+ : binding.authType === "workload"
28
+ ? // The workload token is read from its file at each call (#496), never stored.
29
+ { type: "workload" }
30
+ : {
31
+ type: "api_key",
32
+ key: payload.apiKey,
33
+ ...(payload.env ? { env: payload.env } : {}),
34
+ };
32
35
  return {
33
36
  provider: binding.provider,
34
37
  model: binding.model,
@@ -1,4 +1,4 @@
1
- import type { CredentialCoverage, CredentialCoverageRequest, CredentialIdentity, EnvironmentSecretSlot, AgentSecretView, HostModelProviderInfo, HostModelView, PutAgentSecretRequest, PutHostModelRequest, CustomModelSettings, ModelInput, SelectHostModelRequest } from "@nylorun/core/contracts";
1
+ import type { CredentialCoverage, CredentialCoverageRequest, CredentialIdentity, EnvironmentSecretSlot, AgentSecretView, HostModelAuthType, HostModelProviderInfo, HostModelView, PutAgentSecretRequest, PutHostModelRequest, CustomModelSettings, ModelInput, SelectHostModelRequest } from "@nylorun/core/contracts";
2
2
  import type { AgentManifest, WorkflowManifest } from "@nylorun/core/define";
3
3
  import type { SessionStore, Tx, VaultCredentialRow } from "../store/types.js";
4
4
  export declare const HOST_VAULT_ID = "host";
@@ -10,9 +10,9 @@ export type HostModelSecret = {
10
10
  settings?: CustomModelSettings;
11
11
  /** The inputs the operator added to catalog models, by `<provider>/<model>` (protocol 12). */
12
12
  addedInputs?: Readonly<Record<string, readonly ModelInput[]>>;
13
- authType: "api_key" | "oauth";
13
+ authType: HostModelAuthType;
14
14
  credential: {
15
- type: "api_key" | "oauth";
15
+ type: HostModelAuthType;
16
16
  key?: string;
17
17
  env?: Record<string, string>;
18
18
  refresh?: string;
@@ -22,7 +22,7 @@ export type HostModelSecret = {
22
22
  };
23
23
  };
24
24
  type UserCredentialRow = VaultCredentialRow & {
25
- type: "bearer" | "headers" | "environment_secret" | "environment_variable";
25
+ type: "bearer" | "headers" | "workload" | "environment_secret" | "environment_variable";
26
26
  };
27
27
  /**
28
28
  * A fork's secrets sealed for its new vault (`sealEnvironmentSecretCopies`): each copy's
@@ -54,6 +54,13 @@ export type AuthorizeResult = {
54
54
  headers: Record<string, string>;
55
55
  /** Where the request goes instead of `url` (a gateway), when the credential has one. */
56
56
  via?: string;
57
+ /** How the gateway at `via` prefixes the server's tool names, when it does. */
58
+ toolPrefix?: string;
59
+ /**
60
+ * A `workload` credential (#496): `sessionCredentials` adds the Runtime's workload token as
61
+ * the bearer, on the gate side, where `via`'s origin is one the operator lists.
62
+ */
63
+ workload?: true;
57
64
  /** The header that names the session owner; `sessionCredentials` adds it. */
58
65
  identity?: CredentialIdentity;
59
66
  } | {
@@ -195,7 +202,7 @@ type ModelBinding = {
195
202
  model: string;
196
203
  baseUrl?: string;
197
204
  settings?: CustomModelSettings;
198
- authType: "api_key" | "oauth";
205
+ authType: HostModelAuthType;
199
206
  };
200
207
  type HostSecretPayload = {
201
208
  apiKey?: string;