@nylorun/runtime 0.21.1-beta → 0.22.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 (44) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/adapters/sandbox/local.js +1 -1
  3. package/dist/adapters/sandbox/virtual.js +1 -1
  4. package/dist/api/components.d.ts +42 -2
  5. package/dist/gates/egress-credentials.d.ts +48 -0
  6. package/dist/gates/egress-credentials.js +147 -0
  7. package/dist/gates/egress.d.ts +3 -0
  8. package/dist/gates/egress.js +14 -1
  9. package/dist/gates/tenant-vaults.d.ts +4 -0
  10. package/dist/gates/tenant-vaults.js +2 -0
  11. package/dist/harness/calls.d.ts +2 -0
  12. package/dist/harness/calls.js +1 -0
  13. package/dist/harness/executors.js +1 -0
  14. package/dist/harness/main.js +4 -1
  15. package/dist/harness/pod.d.ts +2 -0
  16. package/dist/harness/pod.js +36 -1
  17. package/dist/harness-api/start.d.ts +2 -0
  18. package/dist/harness-api/start.js +4 -2
  19. package/dist/harness-api/ws-server.js +2 -2
  20. package/dist/host/gates.js +11 -0
  21. package/dist/keys/egress-ca.d.ts +15 -0
  22. package/dist/keys/egress-ca.js +66 -0
  23. package/dist/keys/keys.d.ts +8 -1
  24. package/dist/keys/keys.js +6 -0
  25. package/dist/keys/x509.d.ts +29 -0
  26. package/dist/keys/x509.js +160 -0
  27. package/dist/management-openapi.json +195 -9
  28. package/dist/openapi.json +2 -1
  29. package/dist/sandbox/join.d.ts +2 -0
  30. package/dist/sandbox/join.js +8 -3
  31. package/dist/sandbox/manager.d.ts +2 -0
  32. package/dist/sandbox/manager.js +1 -1
  33. package/dist/sandbox/tools.d.ts +3 -1
  34. package/dist/sandbox/tools.js +4 -2
  35. package/dist/sandbox/types.d.ts +5 -0
  36. package/dist/store/postgres/schema.d.ts +2 -2
  37. package/dist/store/postgres/schema.js +1 -1
  38. package/dist/tenant/sessions.js +26 -0
  39. package/dist/tenant/transcript.d.ts +8 -8
  40. package/dist/vault/service.d.ts +34 -0
  41. package/dist/vault/service.js +148 -15
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/package.json +3 -3
@@ -39,10 +39,12 @@ const failed = (code, message) => ({
39
39
  code,
40
40
  message,
41
41
  });
42
- export async function runSandboxTool(handle, name, input, signal, report) {
42
+ export async function runSandboxTool(handle, name, input, signal, report,
43
+ /** Variables every command gets (R2c): secret names set to the sentinel, plain values. */
44
+ environment) {
43
45
  const workspace = handle.workspace ?? SANDBOX_WORKSPACE;
44
46
  const resolvePath = (path) => resolveSandboxPath(path, workspace);
45
- const exec = (command, timeoutMs = 60_000) => handle.exec({ command, cwd: workspace, timeoutMs }, signal);
47
+ const exec = (command, timeoutMs = 60_000) => handle.exec({ command, cwd: workspace, timeoutMs, ...(environment ? { env: environment } : {}) }, signal);
46
48
  switch (name) {
47
49
  case "bash": {
48
50
  const command = String(input.command);
@@ -36,6 +36,11 @@ export interface ExecRequest {
36
36
  readonly command: string;
37
37
  readonly cwd: string;
38
38
  readonly timeoutMs: number;
39
+ /**
40
+ * Variables this command gets on top of the backend's own (R2c): never a secret. The backend's
41
+ * proxy and trust variables win over them.
42
+ */
43
+ readonly env?: Readonly<Record<string, string>>;
39
44
  }
40
45
  export interface ExecResult {
41
46
  readonly exitCode: number;
@@ -1048,14 +1048,14 @@ export declare const vaultCredentials: import("drizzle-orm/pg-core").PgTableWith
1048
1048
  tableName: "vault_credentials";
1049
1049
  dataType: "string";
1050
1050
  columnType: "PgText";
1051
- data: "bearer" | "headers" | "model";
1051
+ data: "bearer" | "environment_secret" | "environment_variable" | "headers" | "model";
1052
1052
  driverParam: string;
1053
1053
  notNull: true;
1054
1054
  hasDefault: false;
1055
1055
  isPrimaryKey: false;
1056
1056
  isAutoincrement: false;
1057
1057
  hasRuntimeDefault: false;
1058
- enumValues: ["bearer", "headers", "model"];
1058
+ enumValues: ["bearer", "headers", "environment_secret", "environment_variable", "model"];
1059
1059
  baseColumn: never;
1060
1060
  identity: undefined;
1061
1061
  generated: undefined;
@@ -129,7 +129,7 @@ export const vaultCredentials = nylorun.table("vault_credentials", {
129
129
  id: textC().primaryKey(),
130
130
  vaultId: text().notNull(),
131
131
  name: text().notNull(),
132
- type: text({ enum: ["bearer", "headers", "model"] }).notNull(),
132
+ type: text({ enum: ["bearer", "headers", "environment_secret", "environment_variable", "model"] }).notNull(),
133
133
  /** JSON text of the non-secret binding: url (and via, identity, header names), or provider and model. */
134
134
  bindingJson: text().notNull(),
135
135
  expiresAt: text(),
@@ -136,7 +136,12 @@ export async function putSession(ctx, id, body, access, options = {}) {
136
136
  return prior;
137
137
  }
138
138
  await ctx.vault.assertAttachment(t, body.ownerUserId, vaultIds, credentialSelections, { opaque });
139
+ const environment = await ctx.vault.sessionEnvironment(t, vaultIds);
140
+ if (environment.conflict)
141
+ fail(409, environment.conflict, { code: "credential_conflict" });
139
142
  if (prior) {
143
+ if (prior.sandboxId !== undefined)
144
+ await assertSandboxSecrets(ctx, t, prior.sandboxId, id, body.ownerUserId, environment.secretHosts);
140
145
  if (sessionIdentity(prior.creation) !== sessionIdentity(body))
141
146
  fail(409, "Session already exists with different creation parameters");
142
147
  prior.vaultIds = vaultIds;
@@ -156,6 +161,8 @@ export async function putSession(ctx, id, body, access, options = {}) {
156
161
  checkPlacement(await readSandboxConfig(t), sandbox.kind);
157
162
  if (sandbox.kind === "pod")
158
163
  await requirePods(ctx.pods, `Sandbox ${sandbox.sandboxId}`);
164
+ if (sandbox.sandboxId !== undefined)
165
+ await assertSandboxSecrets(ctx, t, sandbox.sandboxId, id, body.ownerUserId, environment.secretHosts);
159
166
  const pinned = sandbox.manifest === undefined ? platformTools(definition) : sandbox;
160
167
  const created = {
161
168
  id,
@@ -186,6 +193,25 @@ export async function putSession(ctx, id, body, access, options = {}) {
186
193
  return created;
187
194
  });
188
195
  }
196
+ /**
197
+ * A sandbox's shell secrets must be unambiguous (R2c, D50): egress-gate sets the header from the
198
+ * one `environment_secret` bound to a host among the sessions using the sandbox. So while any of
199
+ * them has secrets, they all have one owner, and no host is bound to two credentials.
200
+ */
201
+ async function assertSandboxSecrets(ctx, t, sandboxId, sessionId, ownerUserId, secretHosts) {
202
+ const conflict = (message) => fail(409, message, { code: "credential_conflict" });
203
+ const mine = Object.keys(secretHosts).length > 0;
204
+ for (const other of await t.sessionsOnSandbox(sandboxId)) {
205
+ if (other.id === sessionId)
206
+ continue;
207
+ const theirs = (await ctx.vault.sessionEnvironment(t, other.vaultIds ?? [])).secretHosts;
208
+ if (other.ownerUserId !== ownerUserId && (mine || Object.keys(theirs).length > 0))
209
+ conflict(`Sandbox ${sandboxId} serves environment_secret credentials of one owner at a time`);
210
+ for (const [host, credentialId] of Object.entries(secretHosts))
211
+ if (theirs[host] !== undefined && theirs[host] !== credentialId)
212
+ conflict(`Two environment_secret credentials would be bound to ${host} in sandbox ${sandboxId}`);
213
+ }
214
+ }
189
215
  function isShare(value) {
190
216
  return typeof value === "object" && value !== null && "session" in value;
191
217
  }
@@ -103,6 +103,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown): {
103
103
  capabilityId: string;
104
104
  toolName: string;
105
105
  } | {
106
+ redacted?: undefined;
106
107
  agent: import("@nylorun/core/define").AgentRef;
107
108
  error: {
108
109
  code: string;
@@ -112,10 +113,10 @@ export declare function toolCompleted(request: HostEffect, value: unknown): {
112
113
  retryable?: boolean | undefined;
113
114
  };
114
115
  output?: undefined;
115
- redacted?: undefined;
116
116
  capabilityId: string;
117
117
  toolName: string;
118
118
  } | {
119
+ redacted?: undefined;
119
120
  agent?: undefined;
120
121
  error: {
121
122
  code: string;
@@ -125,7 +126,6 @@ export declare function toolCompleted(request: HostEffect, value: unknown): {
125
126
  retryable?: boolean | undefined;
126
127
  };
127
128
  output?: undefined;
128
- redacted?: undefined;
129
129
  capabilityId: string;
130
130
  toolName: string;
131
131
  } | {
@@ -143,17 +143,17 @@ export declare function toolCompleted(request: HostEffect, value: unknown): {
143
143
  capabilityId: string;
144
144
  toolName: string;
145
145
  } | {
146
+ redacted?: undefined;
146
147
  agent: import("@nylorun/core/define").AgentRef;
147
148
  error?: undefined;
148
149
  output: unknown;
149
- redacted?: undefined;
150
150
  capabilityId: string;
151
151
  toolName: string;
152
152
  } | {
153
+ redacted?: undefined;
153
154
  agent?: undefined;
154
155
  error?: undefined;
155
156
  output: unknown;
156
- redacted?: undefined;
157
157
  capabilityId: string;
158
158
  toolName: string;
159
159
  } | {
@@ -187,6 +187,7 @@ export declare function toolCompleted(request: HostEffect, value: unknown): {
187
187
  capabilityId: string;
188
188
  toolName: string;
189
189
  } | {
190
+ redacted?: undefined;
190
191
  callId: string;
191
192
  invocationId: string;
192
193
  agent: import("@nylorun/core/define").AgentRef;
@@ -198,10 +199,10 @@ export declare function toolCompleted(request: HostEffect, value: unknown): {
198
199
  retryable?: boolean | undefined;
199
200
  };
200
201
  output?: undefined;
201
- redacted?: undefined;
202
202
  capabilityId: string;
203
203
  toolName: string;
204
204
  } | {
205
+ redacted?: undefined;
205
206
  callId: string;
206
207
  invocationId: string;
207
208
  agent?: undefined;
@@ -213,7 +214,6 @@ export declare function toolCompleted(request: HostEffect, value: unknown): {
213
214
  retryable?: boolean | undefined;
214
215
  };
215
216
  output?: undefined;
216
- redacted?: undefined;
217
217
  capabilityId: string;
218
218
  toolName: string;
219
219
  } | {
@@ -235,21 +235,21 @@ export declare function toolCompleted(request: HostEffect, value: unknown): {
235
235
  capabilityId: string;
236
236
  toolName: string;
237
237
  } | {
238
+ redacted?: undefined;
238
239
  callId: string;
239
240
  invocationId: string;
240
241
  agent: import("@nylorun/core/define").AgentRef;
241
242
  error?: undefined;
242
243
  output: unknown;
243
- redacted?: undefined;
244
244
  capabilityId: string;
245
245
  toolName: string;
246
246
  } | {
247
+ redacted?: undefined;
247
248
  callId: string;
248
249
  invocationId: string;
249
250
  agent?: undefined;
250
251
  error?: undefined;
251
252
  output: unknown;
252
- redacted?: undefined;
253
253
  capabilityId: string;
254
254
  toolName: string;
255
255
  } | undefined;
@@ -76,6 +76,40 @@ export declare class VaultService {
76
76
  }): Promise<void>;
77
77
  /** Writes the `attach` audit row. Run in the session PUT transaction. */
78
78
  recordAttachment(t: Tx, sessionId: string, vaultIds: readonly string[]): Promise<void>;
79
+ /**
80
+ * The variables a session's sandbox commands get (R2c, D50): each `environment_secret`'s name
81
+ * set to the sentinel, each `environment_variable`'s value, and each host a secret is bound to
82
+ * with that secret's credential id. `conflict` says why they are ambiguous: two credentials with one variable name, or two
83
+ * secrets bound to one host. Reads rows only; no plaintext. Run in the caller's transaction.
84
+ */
85
+ sessionEnvironment(t: Tx, vaultIds: readonly string[]): Promise<{
86
+ environment: Record<string, string>;
87
+ secretHosts: Record<string, string>;
88
+ conflict?: string;
89
+ }>;
90
+ /**
91
+ * The header egress-gate sets on a request from `sandboxId` to `host` (R2c, D50): the
92
+ * `environment_secret` bound to `host` among the vaults of the sessions using the sandbox, its
93
+ * value unsealed and rendered into its format. `none` when no secret is bound to the host;
94
+ * `refused` when two are, or the sealed value cannot be read. One query and one unseal per
95
+ * call; nothing is cached (E3). Plaintext exists only in the returned header.
96
+ */
97
+ releaseEnvironmentSecret(input: {
98
+ sandboxId: string;
99
+ host: string;
100
+ }): Promise<{
101
+ status: "none";
102
+ } | {
103
+ status: "released";
104
+ header: string;
105
+ value: string;
106
+ credentialId: string;
107
+ } | {
108
+ status: "refused";
109
+ reason: "ambiguous" | "unreadable";
110
+ }>;
111
+ /** Whether any `environment_secret` of the sessions using `sandboxId` is bound to `host`. */
112
+ environmentSecretBound(sandboxId: string, host: string): Promise<boolean>;
79
113
  authorize(input: {
80
114
  /** The session the request is made for; absent for a tool preview (R2b C12). */
81
115
  sessionId?: string;
@@ -74,7 +74,7 @@
74
74
  * credential is chosen.
75
75
  */
76
76
  import { createHash, randomUUID } from "node:crypto";
77
- import { INSTALLATION_OWNER } from "@nylorun/core/contracts";
77
+ import { ENVIRONMENT_SECRET_DEFAULT_INJECT, ENVIRONMENT_SECRET_SENTINEL, INSTALLATION_OWNER, renderInjectFormat, } from "@nylorun/core/contracts";
78
78
  import { canonical } from "../store/canonical.js";
79
79
  import { decryptSecret, encryptSecret, VaultCryptoError } from "./crypto.js";
80
80
  import { VaultError } from "./error.js";
@@ -156,11 +156,18 @@ export class VaultService {
156
156
  return this.store.tx((t) => this.replay(t, `create-credential:${vaultId}:${body.idempotencyKey}`, body, async () => {
157
157
  await this.vaultInfo(t, vaultId);
158
158
  const id = randomUUID();
159
- const url = normalizeVaultUrl(body.auth.url);
160
159
  const type = body.auth.type;
161
- const payload = payloadOf(body.auth);
162
- const binding = bindingOf(url, payload, body.auth);
163
- const sealed = encryptSecret(kek, credentialAad(vaultId, id, type, url), Buffer.from(JSON.stringify(payload), "utf8"));
160
+ let payload;
161
+ let binding;
162
+ if (body.auth.type === "environment_secret" || body.auth.type === "environment_variable") {
163
+ ({ payload, binding } = environmentCredential(body.auth));
164
+ }
165
+ else {
166
+ const url = normalizeVaultUrl(body.auth.url);
167
+ payload = payloadOf(body.auth);
168
+ binding = bindingOf(url, payload, body.auth);
169
+ }
170
+ const sealed = encryptSecret(kek, credentialAad(vaultId, id, type, binding), Buffer.from(JSON.stringify(payload), "utf8"));
164
171
  const row = {
165
172
  id,
166
173
  vaultId,
@@ -201,11 +208,23 @@ export class VaultService {
201
208
  if (row.type !== body.auth.type)
202
209
  throw new VaultError(409, "Credential type cannot change");
203
210
  const current = credentialBinding(row);
204
- const next = payloadOf(body.auth);
205
- const binding = bindingOf(current.url, next, {
206
- via: body.auth.via === undefined ? current.via : body.auth.via,
207
- identity: body.auth.identity === undefined ? current.identity : body.auth.identity,
208
- });
211
+ let next;
212
+ let binding;
213
+ if (body.auth.type === "environment_secret") {
214
+ next = { value: body.auth.secretValue };
215
+ binding = current;
216
+ }
217
+ else if (body.auth.type === "environment_variable") {
218
+ next = {};
219
+ binding = { ...current, variableValue: body.auth.variableValue };
220
+ }
221
+ else {
222
+ next = payloadOf(body.auth);
223
+ binding = bindingOf(current.url, next, {
224
+ via: body.auth.via === undefined ? current.via : body.auth.via,
225
+ identity: body.auth.identity === undefined ? current.identity : body.auth.identity,
226
+ });
227
+ }
209
228
  const updated = await this.writePayload(t, kek, row, next, binding, new Date().toISOString());
210
229
  await this.audit(t, {
211
230
  actor: "application",
@@ -271,6 +290,65 @@ export class VaultService {
271
290
  outcome: "attached",
272
291
  });
273
292
  }
293
+ /**
294
+ * The variables a session's sandbox commands get (R2c, D50): each `environment_secret`'s name
295
+ * set to the sentinel, each `environment_variable`'s value, and each host a secret is bound to
296
+ * with that secret's credential id. `conflict` says why they are ambiguous: two credentials with one variable name, or two
297
+ * secrets bound to one host. Reads rows only; no plaintext. Run in the caller's transaction.
298
+ */
299
+ async sessionEnvironment(t, vaultIds) {
300
+ const environment = {};
301
+ const hosts = new Map();
302
+ let conflict;
303
+ for (const vaultId of vaultIds)
304
+ for (const row of await t.credentialsForVault(vaultId)) {
305
+ if (row.type !== "environment_secret" && row.type !== "environment_variable")
306
+ continue;
307
+ const binding = JSON.parse(row.bindingJson);
308
+ const name = row.type === "environment_secret" ? binding.secretName : binding.variableName;
309
+ if (name in environment)
310
+ conflict ??= `Two attached credentials set the variable ${name}`;
311
+ environment[name] = row.type === "environment_secret" ? ENVIRONMENT_SECRET_SENTINEL : binding.variableValue;
312
+ for (const host of row.type === "environment_secret" ? binding.allowedHosts ?? [] : []) {
313
+ if (hosts.has(host) && hosts.get(host) !== row.id)
314
+ conflict ??= `Two attached environment_secret credentials are bound to ${host}`;
315
+ hosts.set(host, row.id);
316
+ }
317
+ }
318
+ return { environment, secretHosts: Object.fromEntries([...hosts].sort()), ...(conflict ? { conflict } : {}) };
319
+ }
320
+ /**
321
+ * The header egress-gate sets on a request from `sandboxId` to `host` (R2c, D50): the
322
+ * `environment_secret` bound to `host` among the vaults of the sessions using the sandbox, its
323
+ * value unsealed and rendered into its format. `none` when no secret is bound to the host;
324
+ * `refused` when two are, or the sealed value cannot be read. One query and one unseal per
325
+ * call; nothing is cached (E3). Plaintext exists only in the returned header.
326
+ */
327
+ async releaseEnvironmentSecret(input) {
328
+ const found = await this.store.tx((t) => sandboxSecretsFor(t, input.sandboxId, input.host));
329
+ if (found.length === 0)
330
+ return { status: "none" };
331
+ if (found.length > 1)
332
+ return { status: "refused", reason: "ambiguous" };
333
+ const row = found[0];
334
+ let payload;
335
+ try {
336
+ payload = readPayload(this.kek(), row);
337
+ }
338
+ catch (error) {
339
+ if (!(error instanceof VaultCryptoError))
340
+ throw error;
341
+ return { status: "refused", reason: "unreadable" };
342
+ }
343
+ if (!payload.value)
344
+ return { status: "refused", reason: "unreadable" };
345
+ const inject = credentialBinding(row).inject ?? ENVIRONMENT_SECRET_DEFAULT_INJECT;
346
+ return { status: "released", header: inject.header, value: renderInjectFormat(inject.format, payload.value), credentialId: row.id };
347
+ }
348
+ /** Whether any `environment_secret` of the sessions using `sandboxId` is bound to `host`. */
349
+ async environmentSecretBound(sandboxId, host) {
350
+ return (await this.store.tx((t) => sandboxSecretsFor(t, sandboxId, host))).length > 0;
351
+ }
274
352
  // --- use ---------------------------------------------------------------------
275
353
  async authorize(input) {
276
354
  const url = normalizeVaultUrl(input.url);
@@ -502,7 +580,7 @@ export class VaultService {
502
580
  }
503
581
  /** Re-seals a user credential's payload with its new binding and returns the updated row. */
504
582
  async writePayload(t, kek, row, payload, binding, rotatedAt) {
505
- const sealed = encryptSecret(kek, credentialAad(row.vaultId, row.id, row.type, bindingUrl(row)), Buffer.from(JSON.stringify(payload), "utf8"));
583
+ const sealed = encryptSecret(kek, credentialAad(row.vaultId, row.id, row.type, binding), Buffer.from(JSON.stringify(payload), "utf8"));
506
584
  const patch = { rotatedAt, bindingJson: JSON.stringify(binding), ...sealed };
507
585
  if (!(await t.updateCredential(row.vaultId, row.id, patch)))
508
586
  throw new VaultError(404, "Credential not found");
@@ -649,7 +727,15 @@ function credentialInfoOf(row) {
649
727
  vaultId: row.vaultId,
650
728
  name: row.name,
651
729
  type: row.type,
652
- binding: { url: binding.url },
730
+ binding: row.type === "environment_secret"
731
+ ? {
732
+ secretName: binding.secretName,
733
+ allowedHosts: binding.allowedHosts,
734
+ inject: binding.inject ?? { ...ENVIRONMENT_SECRET_DEFAULT_INJECT },
735
+ }
736
+ : row.type === "environment_variable"
737
+ ? { variableName: binding.variableName, variableValue: binding.variableValue }
738
+ : { url: binding.url },
653
739
  ...(binding.headerNames === undefined ? {} : { headerNames: binding.headerNames }),
654
740
  ...(binding.via === undefined ? {} : { via: binding.via }),
655
741
  ...(binding.identity === undefined ? {} : { identity: binding.identity }),
@@ -658,7 +744,7 @@ function credentialInfoOf(row) {
658
744
  };
659
745
  }
660
746
  function readPayload(kek, row) {
661
- const aad = credentialAad(row.vaultId, row.id, row.type, bindingUrl(row));
747
+ const aad = credentialAad(row.vaultId, row.id, row.type, credentialBinding(row));
662
748
  const plaintext = decryptSecret(kek, aad, Buffer.from(row.nonce), Buffer.from(row.ciphertext), Buffer.from(row.wrappedDek), row.kekId);
663
749
  try {
664
750
  return JSON.parse(plaintext.toString("utf8"));
@@ -804,10 +890,57 @@ export function hostModelAad(provider, model) {
804
890
  model,
805
891
  }), "utf8");
806
892
  }
807
- function credentialAad(vaultId, credentialId, type, url) {
808
- return Buffer.from(canonical({ vaultId, credentialId, type, url }), "utf8");
893
+ /**
894
+ * What a user credential's ciphertext is bound to: its vault, id, type and URL; for an
895
+ * `environment_secret` (R2c) also its variable, hosts and header, so the value can never be sent
896
+ * anywhere else without a new seal.
897
+ */
898
+ function credentialAad(vaultId, credentialId, type, binding) {
899
+ return Buffer.from(canonical(type === "environment_secret"
900
+ ? {
901
+ vaultId,
902
+ credentialId,
903
+ type,
904
+ secretName: binding.secretName,
905
+ allowedHosts: binding.allowedHosts,
906
+ inject: binding.inject ?? null,
907
+ }
908
+ : { vaultId, credentialId, type, url: binding.url }), "utf8");
909
+ }
910
+ /** The sealed payload and unsealed binding of an environment credential (R2c). */
911
+ function environmentCredential(auth) {
912
+ if (auth.type === "environment_variable")
913
+ return { payload: {}, binding: { variableName: auth.variableName, variableValue: auth.variableValue } };
914
+ if (auth.inject)
915
+ checkCredentialHeaders([auth.inject.header.toLowerCase()], undefined);
916
+ return {
917
+ payload: { value: auth.secretValue },
918
+ binding: {
919
+ secretName: auth.secretName,
920
+ allowedHosts: auth.allowedHosts.map((host) => host.toLowerCase()),
921
+ ...(auth.inject ? { inject: { header: auth.inject.header, format: auth.inject.format } } : {}),
922
+ },
923
+ };
809
924
  }
810
925
  function requestHash(body) {
811
926
  const { requestId: _requestId, ...rest } = body;
812
927
  return canonical(rest);
813
928
  }
929
+ /**
930
+ * The `environment_secret` credentials bound to `host` in the vaults of every session on the
931
+ * sandbox (R2c). A session between turns counts: it can run again, and what it started may
932
+ * still run. Session open keeps them to one owner and one credential per host.
933
+ */
934
+ async function sandboxSecretsFor(t, sandboxId, host) {
935
+ const lower = host.toLowerCase();
936
+ const vaultIds = new Set();
937
+ for (const session of await t.sessionsOnSandbox(sandboxId))
938
+ for (const vaultId of session.vaultIds ?? [])
939
+ vaultIds.add(vaultId);
940
+ const matches = [];
941
+ for (const vaultId of vaultIds)
942
+ for (const row of await t.credentialsForVault(vaultId))
943
+ if (row.type === "environment_secret" && (credentialBinding(row).allowedHosts ?? []).includes(lower))
944
+ matches.push(row);
945
+ return matches;
946
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const RUNTIME_VERSION = "0.21.1-beta";
1
+ export declare const RUNTIME_VERSION = "0.22.0-beta";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Kept in step with package.json by runtime/scripts/check-package.mjs, which runs in npm run check.
2
2
  // release:prepare also rewrites this when it bumps @nylorun/runtime.
3
- export const RUNTIME_VERSION = "0.21.1-beta";
3
+ export const RUNTIME_VERSION = "0.22.0-beta";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/runtime",
3
- "version": "0.21.1-beta",
3
+ "version": "0.22.0-beta",
4
4
  "description": "Nylorun Runtime: the agent execution Host on Postgres, Restate and S2, and model providers.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -65,8 +65,8 @@
65
65
  "@hono/node-server": "^2.1.3",
66
66
  "@hono/zod-openapi": "^1.6.3",
67
67
  "@modelcontextprotocol/sdk": "^1.31.0",
68
- "@nylorun/core": "0.18.0-beta",
69
- "@nylorun/harness": "0.25.0-beta",
68
+ "@nylorun/core": "0.19.0-beta",
69
+ "@nylorun/harness": "0.25.1-beta",
70
70
  "@restatedev/restate-sdk": "1.17.2",
71
71
  "@s2-dev/streamstore": "0.28.0",
72
72
  "aws4fetch": "1.0.20",