@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.
- package/CHANGELOG.md +19 -0
- package/dist/adapters/sandbox/local.js +1 -1
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/api/components.d.ts +42 -2
- package/dist/gates/egress-credentials.d.ts +48 -0
- package/dist/gates/egress-credentials.js +147 -0
- package/dist/gates/egress.d.ts +3 -0
- package/dist/gates/egress.js +14 -1
- package/dist/gates/tenant-vaults.d.ts +4 -0
- package/dist/gates/tenant-vaults.js +2 -0
- package/dist/harness/calls.d.ts +2 -0
- package/dist/harness/calls.js +1 -0
- package/dist/harness/executors.js +1 -0
- package/dist/harness/main.js +4 -1
- package/dist/harness/pod.d.ts +2 -0
- package/dist/harness/pod.js +36 -1
- package/dist/harness-api/start.d.ts +2 -0
- package/dist/harness-api/start.js +4 -2
- package/dist/harness-api/ws-server.js +2 -2
- package/dist/host/gates.js +11 -0
- package/dist/keys/egress-ca.d.ts +15 -0
- package/dist/keys/egress-ca.js +66 -0
- package/dist/keys/keys.d.ts +8 -1
- package/dist/keys/keys.js +6 -0
- package/dist/keys/x509.d.ts +29 -0
- package/dist/keys/x509.js +160 -0
- package/dist/management-openapi.json +195 -9
- package/dist/openapi.json +2 -1
- package/dist/sandbox/join.d.ts +2 -0
- package/dist/sandbox/join.js +8 -3
- package/dist/sandbox/manager.d.ts +2 -0
- package/dist/sandbox/manager.js +1 -1
- package/dist/sandbox/tools.d.ts +3 -1
- package/dist/sandbox/tools.js +4 -2
- package/dist/sandbox/types.d.ts +5 -0
- package/dist/store/postgres/schema.d.ts +2 -2
- package/dist/store/postgres/schema.js +1 -1
- package/dist/tenant/sessions.js +26 -0
- package/dist/tenant/transcript.d.ts +8 -8
- package/dist/vault/service.d.ts +34 -0
- package/dist/vault/service.js +148 -15
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/dist/sandbox/tools.js
CHANGED
|
@@ -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);
|
package/dist/sandbox/types.d.ts
CHANGED
|
@@ -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(),
|
package/dist/tenant/sessions.js
CHANGED
|
@@ -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;
|
package/dist/vault/service.d.ts
CHANGED
|
@@ -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;
|
package/dist/vault/service.js
CHANGED
|
@@ -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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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,
|
|
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:
|
|
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,
|
|
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
|
-
|
|
808
|
-
|
|
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.
|
|
1
|
+
export declare const RUNTIME_VERSION = "0.22.0-beta";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/runtime",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
69
|
-
"@nylorun/harness": "0.25.
|
|
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",
|