@nylorun/runtime 0.21.0-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 +41 -0
- package/README.md +4 -2
- package/dist/adapters/execution/restate.js +26 -1
- 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/core/flow-host.d.ts +2 -3
- package/dist/core/flow-host.js +10 -7
- package/dist/execution/types.d.ts +13 -5
- package/dist/execution/types.js +13 -5
- 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/create-host.d.ts +1 -1
- package/dist/host/create-host.js +2 -1
- package/dist/host/gates.js +11 -0
- package/dist/host/http.d.ts +0 -1
- package/dist/host/http.js +0 -6
- package/dist/host/stack-config.d.ts +2 -0
- package/dist/host/stack-config.js +42 -73
- 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/drizzle/0017_wake_outbox.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +7 -0
- package/dist/store/postgres/schema.d.ts +105 -2
- package/dist/store/postgres/schema.js +17 -1
- package/dist/store/postgres/store.js +54 -4
- package/dist/store/types.d.ts +46 -5
- package/dist/tenant/advance.js +1 -4
- package/dist/tenant/commands.js +5 -5
- package/dist/tenant/context.d.ts +10 -7
- package/dist/tenant/runtime.js +2 -1
- package/dist/tenant/sessions.js +26 -0
- package/dist/tenant/sweep.d.ts +14 -0
- package/dist/tenant/sweep.js +69 -10
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.22.0-beta
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 212364b: **Credentials for skills (R2c).** A skill's CLI in a pod sandbox authenticates with a vault credential whose value never enters the sandbox.
|
|
8
|
+
|
|
9
|
+
- `@nylorun/core`: vault credentials gain `environment_secret` (`secretName`, `secretValue`, `allowedHosts`, optional `inject: { header, format }`) and `environment_variable` (`variableName`, `variableValue`), in create, rotate and `CredentialInfo` (whose `binding.url` is now optional). `ENVIRONMENT_SECRET_SENTINEL` (`nylorun-managed`), `ENVIRONMENT_SECRET_DEFAULT_INJECT`, `isInjectFormat`, `renderInjectFormat`, `EnvironmentNameSchema` and `RESERVED_ENVIRONMENT_NAMES` are exported; `ERROR_CODES` gains `credential_conflict`; the Harness API's `RunRouting.sandbox` gains `environment`.
|
|
10
|
+
- `@nylorun/runtime`: every sandbox command gets each secret's name set to `nylorun-managed` and each variable's value. egress-gate terminates TLS for port 443 of a host an `environment_secret` of the sandbox's sessions is bound to, with a fresh leaf signed by the installation's egress CA (held by keys; its certificate reaches the pod in the join answer, and the engine points `SSL_CERT_FILE`, `NODE_EXTRA_CA_CERTS`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`, `GIT_SSL_CAINFO` and `AWS_CA_BUNDLE` at a bundle with it), and sets the credential's header on every request from the vault, uncached. Every other host stays an opaque tunnel. Session open answers `409 credential_conflict` for two credentials with one variable name or one host, or two owners' secrets on one sandbox. No protocol change.
|
|
11
|
+
- `@nylorun/studio`: the Credentials page lists shell credentials (their variable and hosts) and deletes them; create and rotate them with the Management API.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Pin core to the tested release.
|
|
16
|
+
- Pin harness to the tested release.
|
|
17
|
+
- Updated dependencies [212364b]
|
|
18
|
+
- Updated dependencies
|
|
19
|
+
- @nylorun/core@0.19.0-beta
|
|
20
|
+
- @nylorun/harness@0.25.1-beta
|
|
21
|
+
|
|
22
|
+
## 0.21.1-beta
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- 070674d: **One source of truth for the manifest and Harness API schemas.**
|
|
27
|
+
|
|
28
|
+
- `@nylorun/core`: `Agent.from` (`agentFrom`) checks a manifest with `AgentManifestSchema`, the Runtime's own check, instead of a validator of its own, and refuses with `agent.build-failed` and the schema's messages (each prefixed with its path, such as `capabilities.0.tools.0.name: …`). It now refuses what the Runtime refuses at registration and it let through, such as unknown fields (it dropped them) or a tool or MCP server name that not every model provider accepts. The schema names a top-level `model` or `schemaVersion` and a capability's `model` as before, and the skill whose `files` are wrong (`Skill 'triage' files must include SKILL.md`). The rebuilt manifest is a frozen copy; the object passed in is no longer frozen. `AgentManifestSchema` and `WorkflowManifestSchema` are no longer force-cast to their types: the build checks that each accepts exactly `AgentManifest` and `WorkflowManifest`. `@nylorun/core/contracts` exports `McpServerManifestSchema`, `HEADER_NAME_PATTERN` and `LOOPBACK_HOSTS`. The Harness API's types (`@nylorun/core/harness-api`) are inferred from the schemas that validate its frames, so they match them: `TurnStart.manifest`, `TurnStart.checkpoint` and `RunRouting.rootManifest` are `object`, `WorkspaceCall.tool` is a sandbox tool name, `ABORT_REASONS` is a tuple, and properties are no longer `readonly` (arrays that were stay so).
|
|
29
|
+
- `@nylorun/harness`: `runDurable` checks the manifest once, when it rebuilds the agent, instead of twice. The unpublished, stale `schemas/manifest.schema.json` (manifest `schemaVersion` 2) is gone; `z.toJSONSchema(AgentManifestSchema)` is the manifest's JSON Schema.
|
|
30
|
+
- `@nylorun/agents`: a plugin's MCP servers are checked with core's `McpServerManifestSchema`, header-name pattern and loopback hosts. A server whose key is not a name every model provider accepts is skipped with a `plugin.mcp-server-skipped` warning, since the Runtime would refuse the manifest declaring it.
|
|
31
|
+
- `@nylorun/runtime`: the stack configuration reads its four listeners (`NYLORUN_LISTEN_*`, `NYLORUN_GATES_LISTEN_*`, `NYLORUN_HARNESS_LISTEN_*`, `NYLORUN_EGRESS_LISTEN_*`) with one parser; the variables and their errors are unchanged.
|
|
32
|
+
|
|
33
|
+
- 34d1a81: A commit can no longer lose its wake. A transaction that asks for an advance writes the wake to the Session Store's wake outbox (`Tx.wake`, migration `0017_wake_outbox` adds `nylorun.wakes`) and sends it to Durable Session Execution after commit, with an idempotency key (the wake's dedupe key, or one made from its row), deleting the row once Restate accepted it. A send that fails, or that a crash between the commit and the send cut off, is sent again by the Tenant sweep under the same key: on a process's first pass for any row, then for rows older than 2 s. Restate deduplicates the repeat, so a wake whose answer was lost runs one advance. The sweep's orphan scan no longer delivers lost wakes: it runs on a process's first pass and then once a minute, as the backstop for what the execution itself lost (Restate state wiped, an invocation killed, an in-process execution's queue). Restate owns when a session runs (wakes, one advance per session, retries, timers); the Session Store owns what happened (checkpoint, effect journal, events, the lease's epoch and the outbox), as `src/CONTEXT.md` and `adapters/execution/restate.ts` now describe. A Runtime of the previous beta refuses the migrated database (`schema-too-new`); while processes of both run, the old ones send wakes as before.
|
|
34
|
+
- Pin core to the tested release.
|
|
35
|
+
- Pin harness to the tested release.
|
|
36
|
+
- Updated dependencies [417f336]
|
|
37
|
+
- Updated dependencies [6fecd4c]
|
|
38
|
+
- Updated dependencies [7780b2f]
|
|
39
|
+
- Updated dependencies [070674d]
|
|
40
|
+
- Updated dependencies
|
|
41
|
+
- @nylorun/core@0.18.0-beta
|
|
42
|
+
- @nylorun/harness@0.25.0-beta
|
|
43
|
+
|
|
3
44
|
## 0.21.0-beta
|
|
4
45
|
|
|
5
46
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -42,8 +42,10 @@ whose key the admin key derives (the only derived key). Management keys come fro
|
|
|
42
42
|
rotates and deletes the Tenant's application keys (`PUT /v1/tenant/keys/{keyId}`); a rotated
|
|
43
43
|
or deleted key stops authenticating at once. A database written by a Runtime that kept several Tenants in one
|
|
44
44
|
database (`tenant_<id>` schemas), or by a pre-release build of one Tenant per database
|
|
45
|
-
(`schema_version` tables), is refused: this release starts fresh on a new database. Restate runs one advance of a session at a time
|
|
46
|
-
|
|
45
|
+
(`schema_version` tables), is refused: this release starts fresh on a new database. Restate runs one advance of a session at a time, delivers its wakes and holds
|
|
46
|
+
its timers and the Tenant's sweep (Durable Session Execution), and journals no effect:
|
|
47
|
+
checkpoints, effects, events and the wake outbox are the Session Store's, and a wake is
|
|
48
|
+
written to the outbox in the transaction that causes it, so a commit never loses it; every session's events are relayed from the record to
|
|
47
49
|
its own S2 stream, which history and SSE read (Durable Streams). One image runs every **service**,
|
|
48
50
|
and `--service` picks what a process runs: `core` serves the Runtime API, the
|
|
49
51
|
Management API and SSE and runs the stream relay, `loop` runs advances (the Worker), and
|
|
@@ -25,7 +25,32 @@
|
|
|
25
25
|
* What a run answers arms its timers and its retry.
|
|
26
26
|
*
|
|
27
27
|
* Wakes, sandbox reconciles, timers and sweep arming go through the ingress as one-way
|
|
28
|
-
* sends, so an API node can call them without ever calling `start`.
|
|
28
|
+
* sends, so an API node can call them without ever calling `start`. A wake a commit asked for
|
|
29
|
+
* comes from the Session Store's wake outbox (`Tx.wake`) with an idempotency key, so a send
|
|
30
|
+
* repeated after its answer was lost causes no second advance.
|
|
31
|
+
*
|
|
32
|
+
* ## What Restate owns, and what the Session Store owns
|
|
33
|
+
*
|
|
34
|
+
* Restate owns when a session runs: it delivers each wake at least once, deduped by its
|
|
35
|
+
* key for `dedupeRetentionMs`; runs at most one invocation per key at a time; retries an
|
|
36
|
+
* invocation whose attempt failed, so an advance whose Worker died runs again and takes the
|
|
37
|
+
* session over (§11.4); and keeps every timer: the busy re-wake, `NylorunTimer`,
|
|
38
|
+
* `NylorunSandbox` and the `NylorunTenant` sweep chain. The Session Store (Postgres) owns
|
|
39
|
+
* what happened: the checkpoint, the effect journal (`harness-api/record.ts`), the events
|
|
40
|
+
* (the Record), and the wake outbox that hands each committed wake to Restate. Restate
|
|
41
|
+
* journals no effect: an effect's intent and outcome commit with the session's events in
|
|
42
|
+
* one transaction, under the advance's lease, and remote harnesses and pod engines write
|
|
43
|
+
* them too, over the Harness API.
|
|
44
|
+
*
|
|
45
|
+
* Two Session Store mechanisms overlap Restate on purpose:
|
|
46
|
+
*
|
|
47
|
+
* - **The lease and its epoch** (§10.6). Restate's one invocation per key holds for an
|
|
48
|
+
* attempt, not for the code it started: an attempt that ends (abort timeout, a dropped
|
|
49
|
+
* connection) is retried while the previous attempt's handler may still run on its Worker,
|
|
50
|
+
* and a remote harness or pod engine may still hold the run. The epoch fences their writes,
|
|
51
|
+
* and `busy` answers the overlap.
|
|
52
|
+
* - **The advance deadline** (`tenant/worker.ts`). Restate's abort timeout does not stop a
|
|
53
|
+
* running handler, so the Worker bounds each advance itself, below `timeouts`.
|
|
29
54
|
*/
|
|
30
55
|
import { createServer } from "node:http2";
|
|
31
56
|
import * as restate from "@restatedev/restate-sdk";
|
|
@@ -57,7 +57,7 @@ export function localBackend(options) {
|
|
|
57
57
|
return reject(signal.reason ?? new Error("Turn cancelled"));
|
|
58
58
|
const child = spawn("/bin/sh", ["-c", request.command], {
|
|
59
59
|
cwd: request.cwd,
|
|
60
|
-
env: { ...env, ...(options.proxyEnv?.() ?? {}) },
|
|
60
|
+
env: { ...env, ...(request.env ?? {}), ...(options.proxyEnv?.() ?? {}) },
|
|
61
61
|
stdio: ["ignore", "pipe", "pipe"],
|
|
62
62
|
// Its own process group, so a timeout or a cancel kills everything it started.
|
|
63
63
|
detached: true,
|
package/dist/api/components.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare const Rejected: z.ZodObject<{
|
|
|
10
10
|
status: z.ZodLiteral<"rejected">;
|
|
11
11
|
code: z.ZodEnum<{
|
|
12
12
|
connection_missing: "connection_missing";
|
|
13
|
+
credential_conflict: "credential_conflict";
|
|
13
14
|
credential_invalid: "credential_invalid";
|
|
14
15
|
credential_rejected: "credential_rejected";
|
|
15
16
|
credential_required: "credential_required";
|
|
@@ -8376,6 +8377,19 @@ export declare const CreateCredentialRequest: z.ZodObject<{
|
|
|
8376
8377
|
type: z.ZodLiteral<"headers">;
|
|
8377
8378
|
url: z.ZodString;
|
|
8378
8379
|
headers: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
8380
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
8381
|
+
type: z.ZodLiteral<"environment_secret">;
|
|
8382
|
+
secretName: z.ZodString;
|
|
8383
|
+
secretValue: z.ZodString;
|
|
8384
|
+
allowedHosts: z.ZodArray<z.ZodString>;
|
|
8385
|
+
inject: z.ZodOptional<z.ZodObject<{
|
|
8386
|
+
header: z.ZodString;
|
|
8387
|
+
format: z.ZodString;
|
|
8388
|
+
}, z.core.$strict>>;
|
|
8389
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
8390
|
+
type: z.ZodLiteral<"environment_variable">;
|
|
8391
|
+
variableName: z.ZodString;
|
|
8392
|
+
variableValue: z.ZodString;
|
|
8379
8393
|
}, z.core.$strict>], "type">;
|
|
8380
8394
|
}, z.core.$strict>;
|
|
8381
8395
|
export declare const CredentialInfo: z.ZodObject<{
|
|
@@ -8384,10 +8398,20 @@ export declare const CredentialInfo: z.ZodObject<{
|
|
|
8384
8398
|
name: z.ZodString;
|
|
8385
8399
|
type: z.ZodEnum<{
|
|
8386
8400
|
bearer: "bearer";
|
|
8401
|
+
environment_secret: "environment_secret";
|
|
8402
|
+
environment_variable: "environment_variable";
|
|
8387
8403
|
headers: "headers";
|
|
8388
8404
|
}>;
|
|
8389
8405
|
binding: z.ZodObject<{
|
|
8390
|
-
url: z.ZodString
|
|
8406
|
+
url: z.ZodOptional<z.ZodString>;
|
|
8407
|
+
secretName: z.ZodOptional<z.ZodString>;
|
|
8408
|
+
allowedHosts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
8409
|
+
inject: z.ZodOptional<z.ZodObject<{
|
|
8410
|
+
header: z.ZodString;
|
|
8411
|
+
format: z.ZodString;
|
|
8412
|
+
}, z.core.$strict>>;
|
|
8413
|
+
variableName: z.ZodOptional<z.ZodString>;
|
|
8414
|
+
variableValue: z.ZodOptional<z.ZodString>;
|
|
8391
8415
|
}, z.core.$strict>;
|
|
8392
8416
|
headerNames: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
8393
8417
|
via: z.ZodOptional<z.ZodString>;
|
|
@@ -8404,10 +8428,20 @@ export declare const ListCredentialsResponse: z.ZodObject<{
|
|
|
8404
8428
|
name: z.ZodString;
|
|
8405
8429
|
type: z.ZodEnum<{
|
|
8406
8430
|
bearer: "bearer";
|
|
8431
|
+
environment_secret: "environment_secret";
|
|
8432
|
+
environment_variable: "environment_variable";
|
|
8407
8433
|
headers: "headers";
|
|
8408
8434
|
}>;
|
|
8409
8435
|
binding: z.ZodObject<{
|
|
8410
|
-
url: z.ZodString
|
|
8436
|
+
url: z.ZodOptional<z.ZodString>;
|
|
8437
|
+
secretName: z.ZodOptional<z.ZodString>;
|
|
8438
|
+
allowedHosts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
8439
|
+
inject: z.ZodOptional<z.ZodObject<{
|
|
8440
|
+
header: z.ZodString;
|
|
8441
|
+
format: z.ZodString;
|
|
8442
|
+
}, z.core.$strict>>;
|
|
8443
|
+
variableName: z.ZodOptional<z.ZodString>;
|
|
8444
|
+
variableValue: z.ZodOptional<z.ZodString>;
|
|
8411
8445
|
}, z.core.$strict>;
|
|
8412
8446
|
headerNames: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
8413
8447
|
via: z.ZodOptional<z.ZodString>;
|
|
@@ -8435,6 +8469,12 @@ export declare const RotateCredentialRequest: z.ZodObject<{
|
|
|
8435
8469
|
}, z.core.$strict>>>;
|
|
8436
8470
|
type: z.ZodLiteral<"headers">;
|
|
8437
8471
|
headers: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
8472
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
8473
|
+
type: z.ZodLiteral<"environment_secret">;
|
|
8474
|
+
secretValue: z.ZodString;
|
|
8475
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
8476
|
+
type: z.ZodLiteral<"environment_variable">;
|
|
8477
|
+
variableValue: z.ZodString;
|
|
8438
8478
|
}, z.core.$strict>], "type">;
|
|
8439
8479
|
}, z.core.$strict>;
|
|
8440
8480
|
export declare const DeletedResponse: z.ZodObject<{
|
package/dist/core/flow-host.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { EffectOutcome } from "@nylorun/core/contracts";
|
|
2
2
|
import { type JsonValue, type WorkflowManifest } from "@nylorun/core/define";
|
|
3
3
|
import type { HostEffect } from "@nylorun/harness/run";
|
|
4
|
-
import type {
|
|
5
|
-
import type { EffectDoc, Tx } from "../store/types.js";
|
|
4
|
+
import type { EffectDoc, Tx, WakeDelivery } from "../store/types.js";
|
|
6
5
|
import { type FlowLimits } from "./limits.js";
|
|
7
6
|
/** Deterministic agent session id: derive(workflowSessionId, path, …parts). */
|
|
8
7
|
export declare function deriveSessionId(workflowSessionId: string, path: string, ...parts: string[]): string;
|
|
@@ -99,7 +98,7 @@ export type CascadeCancelPlan = {
|
|
|
99
98
|
readonly agentSessionIds: string[];
|
|
100
99
|
};
|
|
101
100
|
/** Wakes a session after commit (`DurableExecution.wake` through the Tenant context). */
|
|
102
|
-
type Schedule =
|
|
101
|
+
type Schedule = WakeDelivery;
|
|
103
102
|
/** Effect documents as the flow host writes them. */
|
|
104
103
|
export type FlowEffect = EffectDoc & {
|
|
105
104
|
agentSessionId?: string;
|
package/dist/core/flow-host.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Every function that touches state takes the caller's transaction `t: Tx` and
|
|
5
5
|
* is async. Nothing here publishes, notifies or schedules directly (seam rule
|
|
6
6
|
* 1): events go through `t.event(...)`, which the store publishes after
|
|
7
|
-
* commit, and wakes go through `t.
|
|
7
|
+
* commit, and wakes go through `t.wake(id, wake, schedule)`, the wake outbox, which
|
|
8
|
+
* calls `schedule` after commit. Functions
|
|
8
9
|
* that rewrite a session-scoped document lock that session first with
|
|
9
10
|
* `t.lockSession` (a no-op when the caller already holds it).
|
|
10
11
|
*
|
|
@@ -35,8 +36,9 @@
|
|
|
35
36
|
* - `foreignInteractionConflict({ t, workflowSessionId, interactionId }): Promise<{ status: 409; message; ownerSessionId } | undefined>`
|
|
36
37
|
* - `wakeForQueuedEffects({ t, workflowSessionId, turnId, limits, schedule }): Promise<boolean>`
|
|
37
38
|
*
|
|
38
|
-
* `schedule
|
|
39
|
-
* `wake` carries the reason (`linked`, `flow`) and, where the cause has one, a dedupe key.
|
|
39
|
+
* `schedule` (a `WakeDelivery`, the Tenant's `ctx.wake`) runs after commit, never inside `t`;
|
|
40
|
+
* `wake` carries the reason (`linked`, `flow`) and, where the cause has one, a dedupe key. A
|
|
41
|
+
* wake `schedule` does not deliver stays in the outbox for the Tenant sweep (`Tx.wake`).
|
|
40
42
|
*
|
|
41
43
|
* An `agent` effect settles only from the linked turn it started. A linked agent session is
|
|
42
44
|
* reused by every iteration of a Loop (its id derives from the workflow and the path), so its
|
|
@@ -138,8 +140,9 @@ export async function recordVerdict(t, request, value) {
|
|
|
138
140
|
...(verdict.data !== undefined ? { data: verdict.data } : {}),
|
|
139
141
|
});
|
|
140
142
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
/** Writes the wake to the outbox; `schedule` delivers it after commit. */
|
|
144
|
+
function requestWake(t, schedule, id, wake) {
|
|
145
|
+
return t.wake(id, wake, schedule);
|
|
143
146
|
}
|
|
144
147
|
/** Active agent turns + tool nodes in flight for one workflow turn. */
|
|
145
148
|
export async function countActiveFlowWork(t, workflowSessionId, turnId) {
|
|
@@ -242,7 +245,7 @@ export async function wakeLinkedWorkflow(input) {
|
|
|
242
245
|
await settleAgentEffect({ t, effect, outcome });
|
|
243
246
|
workflow.status = "runnable";
|
|
244
247
|
await t.put("sessions", workflow.id, workflow);
|
|
245
|
-
|
|
248
|
+
await requestWake(t, input.schedule, workflow.id, {
|
|
246
249
|
reason: "linked",
|
|
247
250
|
dedupeKey: `linked:${link.turnId}:${link.effectId}`,
|
|
248
251
|
});
|
|
@@ -471,7 +474,7 @@ export async function wakeForQueuedEffects(input) {
|
|
|
471
474
|
workflow.status = "runnable";
|
|
472
475
|
await t.put("sessions", workflow.id, workflow);
|
|
473
476
|
}
|
|
474
|
-
|
|
477
|
+
await requestWake(t, input.schedule, input.workflowSessionId, {
|
|
475
478
|
reason: "flow",
|
|
476
479
|
});
|
|
477
480
|
return true;
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Durable Session Execution decides when to look at a session again. It never
|
|
5
5
|
* holds session state: every outcome lives in the Session Store, and `advance`
|
|
6
|
-
* is safe to repeat (§10.1).
|
|
6
|
+
* is safe to repeat (§10.1). It owns the schedule (wake delivery, one advance per
|
|
7
|
+
* key, retries, timers, the sweep chain); the Session Store owns the record (the
|
|
8
|
+
* checkpoint, the effect journal, the events) and the wake outbox that bridges the
|
|
9
|
+
* two. Restate is the supported implementation
|
|
7
10
|
* (`adapters/execution/restate.ts`); `execution/memory.ts` is the in-process
|
|
8
11
|
* implementation used by unit tests, `startEphemeralRuntime` and a Host
|
|
9
12
|
* started without Restate endpoints.
|
|
@@ -15,11 +18,14 @@
|
|
|
15
18
|
* Different keys may run concurrently.
|
|
16
19
|
* - **At least once after a wake.** When `wake` resolves, an `advance` for the
|
|
17
20
|
* key will start after that point (possibly merged with other wakes). Wakes
|
|
18
|
-
* are sent after the Session Store transaction that caused them commits
|
|
19
|
-
* (`Tx.
|
|
21
|
+
* are sent after the Session Store transaction that caused them commits, never
|
|
22
|
+
* inside it: the transaction writes the wake to its outbox (`Tx.wake`), and the
|
|
23
|
+
* wake is sent after commit, or by the Tenant sweep when that send failed or a
|
|
24
|
+
* crash cut it off. A wake may therefore be sent more than once, always with the
|
|
25
|
+
* same `dedupeKey`.
|
|
20
26
|
* - **Dedupe.** Wakes carrying the same `dedupeKey` for the same key cause at
|
|
21
27
|
* most one advance for that cause (within the implementation's retention
|
|
22
|
-
* window, at least 24 hours for Restate).
|
|
28
|
+
* window, at least 24 hours for Restate). Every wake from the outbox has one.
|
|
23
29
|
* - **Busy re-wake.** An advance that returns `busy` is re-run for the same key
|
|
24
30
|
* after `retryAfterMs`.
|
|
25
31
|
* - **Retries on infrastructure errors.** A handler that throws is retried with
|
|
@@ -27,7 +33,9 @@
|
|
|
27
33
|
* becomes `uncertain` is settled in the Session Store and `advance` returns
|
|
28
34
|
* `done`.
|
|
29
35
|
* - **Sweeps.** `armSweep` arms one self-re-arming sweep per Tenant; arming an
|
|
30
|
-
* armed Tenant is a no-op. The sweep
|
|
36
|
+
* armed Tenant is a no-op. The sweep sends the outbox's undelivered wakes and,
|
|
37
|
+
* as a backstop for what an implementation lost, re-wakes orphaned sessions
|
|
38
|
+
* (`tenant/sweep.ts`).
|
|
31
39
|
* - **One reconcile per sandbox at a time** (F7.2). `sandbox` runs
|
|
32
40
|
* `WorkerHandlers.sandbox` for the key `<tenantId>:<sandboxId>` at least once
|
|
33
41
|
* after a `reconcile` resolves, never overlapping another run for the key. A
|
package/dist/execution/types.js
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Durable Session Execution decides when to look at a session again. It never
|
|
5
5
|
* holds session state: every outcome lives in the Session Store, and `advance`
|
|
6
|
-
* is safe to repeat (§10.1).
|
|
6
|
+
* is safe to repeat (§10.1). It owns the schedule (wake delivery, one advance per
|
|
7
|
+
* key, retries, timers, the sweep chain); the Session Store owns the record (the
|
|
8
|
+
* checkpoint, the effect journal, the events) and the wake outbox that bridges the
|
|
9
|
+
* two. Restate is the supported implementation
|
|
7
10
|
* (`adapters/execution/restate.ts`); `execution/memory.ts` is the in-process
|
|
8
11
|
* implementation used by unit tests, `startEphemeralRuntime` and a Host
|
|
9
12
|
* started without Restate endpoints.
|
|
@@ -15,11 +18,14 @@
|
|
|
15
18
|
* Different keys may run concurrently.
|
|
16
19
|
* - **At least once after a wake.** When `wake` resolves, an `advance` for the
|
|
17
20
|
* key will start after that point (possibly merged with other wakes). Wakes
|
|
18
|
-
* are sent after the Session Store transaction that caused them commits
|
|
19
|
-
* (`Tx.
|
|
21
|
+
* are sent after the Session Store transaction that caused them commits, never
|
|
22
|
+
* inside it: the transaction writes the wake to its outbox (`Tx.wake`), and the
|
|
23
|
+
* wake is sent after commit, or by the Tenant sweep when that send failed or a
|
|
24
|
+
* crash cut it off. A wake may therefore be sent more than once, always with the
|
|
25
|
+
* same `dedupeKey`.
|
|
20
26
|
* - **Dedupe.** Wakes carrying the same `dedupeKey` for the same key cause at
|
|
21
27
|
* most one advance for that cause (within the implementation's retention
|
|
22
|
-
* window, at least 24 hours for Restate).
|
|
28
|
+
* window, at least 24 hours for Restate). Every wake from the outbox has one.
|
|
23
29
|
* - **Busy re-wake.** An advance that returns `busy` is re-run for the same key
|
|
24
30
|
* after `retryAfterMs`.
|
|
25
31
|
* - **Retries on infrastructure errors.** A handler that throws is retried with
|
|
@@ -27,7 +33,9 @@
|
|
|
27
33
|
* becomes `uncertain` is settled in the Session Store and `advance` returns
|
|
28
34
|
* `done`.
|
|
29
35
|
* - **Sweeps.** `armSweep` arms one self-re-arming sweep per Tenant; arming an
|
|
30
|
-
* armed Tenant is a no-op. The sweep
|
|
36
|
+
* armed Tenant is a no-op. The sweep sends the outbox's undelivered wakes and,
|
|
37
|
+
* as a backstop for what an implementation lost, re-wakes orphaned sessions
|
|
38
|
+
* (`tenant/sweep.ts`).
|
|
31
39
|
* - **One reconcile per sandbox at a time** (F7.2). `sandbox` runs
|
|
32
40
|
* `WorkerHandlers.sandbox` for the key `<tenantId>:<sandboxId>` at least once
|
|
33
41
|
* after a `reconcile` resolves, never overlapping another run for the key. A
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Duplex } from "node:stream";
|
|
2
|
+
import type { Logger } from "../tenant/types.js";
|
|
3
|
+
/** What the vault answers for one request to a host a credential may be bound to. */
|
|
4
|
+
export type EgressCredentialRelease = {
|
|
5
|
+
readonly status: "none";
|
|
6
|
+
} | {
|
|
7
|
+
readonly status: "released";
|
|
8
|
+
readonly header: string;
|
|
9
|
+
readonly value: string;
|
|
10
|
+
readonly credentialId: string;
|
|
11
|
+
} | {
|
|
12
|
+
readonly status: "refused";
|
|
13
|
+
readonly reason: string;
|
|
14
|
+
};
|
|
15
|
+
export interface EgressCredentials {
|
|
16
|
+
/** Whether an `environment_secret` of a live session on the sandbox is bound to `host`. */
|
|
17
|
+
bound(sandboxId: string, host: string): Promise<boolean>;
|
|
18
|
+
/** The header to set on one request, read from the vault each time. */
|
|
19
|
+
release(sandboxId: string, host: string): Promise<EgressCredentialRelease>;
|
|
20
|
+
/** A fresh leaf for `host`: its key, made here, and its certificate, signed by keys. */
|
|
21
|
+
leaf(host: string): Promise<{
|
|
22
|
+
readonly key: string;
|
|
23
|
+
readonly cert: string;
|
|
24
|
+
}>;
|
|
25
|
+
/** Tests: the CA upstream servers are verified against, instead of the public roots. */
|
|
26
|
+
readonly upstreamCa?: string;
|
|
27
|
+
/** Tests: the port requests are sent to, instead of 443. */
|
|
28
|
+
readonly upstreamPort?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface CredentialTerminator {
|
|
31
|
+
/**
|
|
32
|
+
* Answers the CONNECT, terminates TLS for `host` and serves its requests, sending each to one
|
|
33
|
+
* of `addresses` (already checked) and nowhere else.
|
|
34
|
+
*/
|
|
35
|
+
terminate(client: Duplex, target: {
|
|
36
|
+
readonly sandboxId: string;
|
|
37
|
+
readonly host: string;
|
|
38
|
+
readonly addresses: readonly {
|
|
39
|
+
address: string;
|
|
40
|
+
family: number;
|
|
41
|
+
}[];
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
export declare function credentialTerminator(options: {
|
|
45
|
+
readonly credentials: EgressCredentials;
|
|
46
|
+
readonly logger: Logger;
|
|
47
|
+
readonly idleMs: number;
|
|
48
|
+
}): CredentialTerminator;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* egress-gate's credential path (R2c, D50): how a skill's CLI authenticates while the secret
|
|
3
|
+
* stays out of the sandbox. A CONNECT to port 443 of a host that an `environment_secret` of a
|
|
4
|
+
* live session on the sandbox is bound to (`allowedHosts`) is not tunnelled: the gate answers
|
|
5
|
+
* 200, terminates TLS with a fresh leaf for that host signed by the installation's egress CA
|
|
6
|
+
* (keys holds its key; the pod trusts its certificate, `harness/pod.ts`), and reads HTTP/1.1
|
|
7
|
+
* requests. For each request it reads the vault (one Postgres query, no cache, so a rotation
|
|
8
|
+
* applies to the next request), sets the credential's header (replacing any of that name, such
|
|
9
|
+
* as the `nylorun-managed` sentinel the CLI sent), and forwards it over TLS to the address the
|
|
10
|
+
* gate already checked, verifying the host's certificate by name against the public roots.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here knows a tool: the header name and its format are the credential's. A release the
|
|
13
|
+
* vault refuses (two secrets for one host, an unreadable value) answers 502 and is logged, never
|
|
14
|
+
* as an event; a value never is. Upgrades (WebSocket) and HTTP/2 are not served on this path.
|
|
15
|
+
*/
|
|
16
|
+
import { createServer } from "node:http";
|
|
17
|
+
import { Agent, request } from "node:https";
|
|
18
|
+
import { createSecureContext, TLSSocket } from "node:tls";
|
|
19
|
+
/** Headers that describe one hop and are never forwarded. */
|
|
20
|
+
const HOP_BY_HOP = new Set([
|
|
21
|
+
"connection",
|
|
22
|
+
"keep-alive",
|
|
23
|
+
"proxy-authenticate",
|
|
24
|
+
"proxy-authorization",
|
|
25
|
+
"proxy-connection",
|
|
26
|
+
"te",
|
|
27
|
+
"trailer",
|
|
28
|
+
"transfer-encoding",
|
|
29
|
+
"upgrade",
|
|
30
|
+
]);
|
|
31
|
+
/** `rawHeaders` without hop-by-hop headers and `drop`, as lowercase names to values. */
|
|
32
|
+
function forwardable(raw, drop) {
|
|
33
|
+
const headers = {};
|
|
34
|
+
for (let i = 0; i + 1 < raw.length; i += 2) {
|
|
35
|
+
const name = raw[i].toLowerCase();
|
|
36
|
+
if (HOP_BY_HOP.has(name) || name === drop)
|
|
37
|
+
continue;
|
|
38
|
+
(headers[name] ??= []).push(raw[i + 1]);
|
|
39
|
+
}
|
|
40
|
+
return headers;
|
|
41
|
+
}
|
|
42
|
+
function reply(res, status, message) {
|
|
43
|
+
if (res.headersSent) {
|
|
44
|
+
res.destroy();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const body = `egress-gate: ${message}\n`;
|
|
48
|
+
res.writeHead(status, { "content-type": "text/plain; charset=utf-8", "content-length": Buffer.byteLength(body) });
|
|
49
|
+
res.end(body);
|
|
50
|
+
}
|
|
51
|
+
export function credentialTerminator(options) {
|
|
52
|
+
const { credentials, logger } = options;
|
|
53
|
+
const terminated = new WeakMap();
|
|
54
|
+
const http = createServer((req, res) => {
|
|
55
|
+
forward(req, res).catch((error) => {
|
|
56
|
+
logger.error("egress_forward_failed", { error: error instanceof Error ? error.message : String(error) });
|
|
57
|
+
reply(res, 502, "the request failed");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
http.keepAliveTimeout = options.idleMs;
|
|
61
|
+
http.headersTimeout = 60_000;
|
|
62
|
+
// Uploads may be long; the client socket's idle timeout still applies.
|
|
63
|
+
http.requestTimeout = 0;
|
|
64
|
+
http.on("upgrade", (req, socket) => {
|
|
65
|
+
const at = terminated.get(req.socket);
|
|
66
|
+
logger.warn("egress_refused", { status: 501, reason: "upgrade_refused", sandboxId: at?.sandboxId, host: at?.host });
|
|
67
|
+
socket.destroy();
|
|
68
|
+
});
|
|
69
|
+
http.on("clientError", (_error, socket) => socket.destroy());
|
|
70
|
+
async function forward(req, res) {
|
|
71
|
+
const at = terminated.get(req.socket);
|
|
72
|
+
if (!at) {
|
|
73
|
+
res.destroy();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const { sandboxId, host } = at;
|
|
77
|
+
const release = await credentials.release(sandboxId, host);
|
|
78
|
+
if (release.status === "refused") {
|
|
79
|
+
logger.warn("egress_credential_refused", { status: 502, reason: release.reason, sandboxId, host });
|
|
80
|
+
req.resume();
|
|
81
|
+
return reply(res, 502, `the credential for ${host} could not be used`);
|
|
82
|
+
}
|
|
83
|
+
const header = release.status === "released" ? release.header.toLowerCase() : undefined;
|
|
84
|
+
const headers = forwardable(req.rawHeaders, header);
|
|
85
|
+
if (release.status === "released")
|
|
86
|
+
headers[header] = [release.value];
|
|
87
|
+
const upstream = request({
|
|
88
|
+
host,
|
|
89
|
+
port: credentials.upstreamPort ?? 443,
|
|
90
|
+
servername: host,
|
|
91
|
+
method: req.method,
|
|
92
|
+
path: req.url,
|
|
93
|
+
headers,
|
|
94
|
+
agent: at.agent,
|
|
95
|
+
});
|
|
96
|
+
upstream.on("response", (response) => {
|
|
97
|
+
res.writeHead(response.statusCode ?? 502, response.statusMessage, forwardable(response.rawHeaders));
|
|
98
|
+
response.pipe(res);
|
|
99
|
+
response.on("error", () => res.destroy());
|
|
100
|
+
});
|
|
101
|
+
upstream.on("error", (error) => {
|
|
102
|
+
logger.warn("egress_upstream_failed", { status: 502, sandboxId, host, code: error.code });
|
|
103
|
+
reply(res, 502, `could not reach ${host}`);
|
|
104
|
+
});
|
|
105
|
+
req.on("error", () => upstream.destroy());
|
|
106
|
+
res.on("close", () => {
|
|
107
|
+
if (!res.writableFinished)
|
|
108
|
+
upstream.destroy();
|
|
109
|
+
});
|
|
110
|
+
req.pipe(upstream);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
async terminate(client, target) {
|
|
114
|
+
const { sandboxId, host, addresses } = target;
|
|
115
|
+
const leaf = await credentials.leaf(host);
|
|
116
|
+
if (client.destroyed)
|
|
117
|
+
return;
|
|
118
|
+
// Requests go to the addresses the gate checked, never to the name resolved again.
|
|
119
|
+
const lookup = (_name, lookupOptions, callback) => {
|
|
120
|
+
if (lookupOptions.all)
|
|
121
|
+
callback(null, addresses);
|
|
122
|
+
else
|
|
123
|
+
callback(null, addresses[0].address, addresses[0].family);
|
|
124
|
+
};
|
|
125
|
+
const agent = new Agent({
|
|
126
|
+
keepAlive: true,
|
|
127
|
+
maxSockets: 8,
|
|
128
|
+
lookup,
|
|
129
|
+
...(credentials.upstreamCa ? { ca: credentials.upstreamCa } : {}),
|
|
130
|
+
});
|
|
131
|
+
client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
132
|
+
const tls = new TLSSocket(client, {
|
|
133
|
+
isServer: true,
|
|
134
|
+
secureContext: createSecureContext({ key: leaf.key, cert: leaf.cert }),
|
|
135
|
+
ALPNProtocols: ["http/1.1"],
|
|
136
|
+
});
|
|
137
|
+
terminated.set(tls, { sandboxId, host, agent });
|
|
138
|
+
tls.on("error", () => tls.destroy());
|
|
139
|
+
tls.on("close", () => {
|
|
140
|
+
agent.destroy();
|
|
141
|
+
client.destroy();
|
|
142
|
+
});
|
|
143
|
+
logger.info("egress_terminated", { sandboxId, host });
|
|
144
|
+
http.emit("connection", tls);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
package/dist/gates/egress.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type Socket } from "node:net";
|
|
|
3
3
|
import type { EgressTokenVerdict } from "../sandbox/egress-token.js";
|
|
4
4
|
import type { SessionStore } from "../store/types.js";
|
|
5
5
|
import type { Logger } from "../tenant/types.js";
|
|
6
|
+
import { type EgressCredentials } from "./egress-credentials.js";
|
|
6
7
|
/**
|
|
7
8
|
* What egress-gate reads of a sandbox: its current host epoch, the pod that joined at it, and
|
|
8
9
|
* its spec's `network.allow`; `undefined` when it is gone, deleted, lost, expired or not a pod.
|
|
@@ -46,6 +47,8 @@ export interface StartEgressGateOptions {
|
|
|
46
47
|
readonly blocked?: (address: string) => boolean;
|
|
47
48
|
/** Tests replace how the checked address is dialled. */
|
|
48
49
|
readonly dial?: (address: string, port: number) => Socket;
|
|
50
|
+
/** The vault and keys, for hosts an `environment_secret` is bound to (R2c). None: tunnels only. */
|
|
51
|
+
readonly credentials?: EgressCredentials;
|
|
49
52
|
}
|
|
50
53
|
export interface EgressGate {
|
|
51
54
|
/** Where the gate listens, e.g. `http://0.0.0.0:4200`. */
|
package/dist/gates/egress.js
CHANGED
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
* (cloud metadata included), multicast or reserved, else 502 or 403;
|
|
18
18
|
* 6. the gate connects to that checked address (never to the name again) and pipes.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
20
|
+
* A tunnel is opaque bytes, with one exception (R2c, D50): port 443 of a host that an
|
|
21
|
+
* `environment_secret` of a live session on the sandbox is bound to. There the gate terminates
|
|
22
|
+
* TLS and sets the credential's header on every request (`egress-credentials.ts`), still sending
|
|
23
|
+
* only to the checked address. Every other host gets the tunnel. Any other request
|
|
21
24
|
* (an absolute-form plain HTTP request included) is answered 405. Refusals are logged, never as
|
|
22
25
|
* events; the token never is. Tunnels close after `idleMs` without traffic in either direction.
|
|
23
26
|
*/
|
|
@@ -27,6 +30,7 @@ import { BlockList, connect, isIP } from "node:net";
|
|
|
27
30
|
import { isSandboxHostPattern } from "@nylorun/core/define";
|
|
28
31
|
import { isPrivateAddress } from "../tenant/outbound.js";
|
|
29
32
|
import { bindListener } from "../host/http.js";
|
|
33
|
+
import { credentialTerminator } from "./egress-credentials.js";
|
|
30
34
|
/** Ports a tunnel may open. */
|
|
31
35
|
export const EGRESS_PORTS = [443, 80];
|
|
32
36
|
/** Tunnels one sandbox may hold open at once. */
|
|
@@ -151,6 +155,9 @@ export async function startEgressGate(options) {
|
|
|
151
155
|
cache.delete(cache.keys().next().value);
|
|
152
156
|
return value;
|
|
153
157
|
}
|
|
158
|
+
const terminator = options.credentials
|
|
159
|
+
? credentialTerminator({ credentials: options.credentials, logger, idleMs })
|
|
160
|
+
: undefined;
|
|
154
161
|
const open = new Map();
|
|
155
162
|
const tunnels = new Set();
|
|
156
163
|
async function onConnect(req, client, head) {
|
|
@@ -209,6 +216,12 @@ export async function startEgressGate(options) {
|
|
|
209
216
|
return refuse(403, "address_refused", { sandboxId }, `${host} resolves only to addresses tunnels may not reach`);
|
|
210
217
|
if (client.destroyed)
|
|
211
218
|
return;
|
|
219
|
+
if (terminator && port === 443 && (await options.credentials.bound(sandboxId, host))) {
|
|
220
|
+
// A client that spoke before the 200 is not one this path serves.
|
|
221
|
+
if (head.length > 0)
|
|
222
|
+
return refuse(403, "early_data", { sandboxId }, "wait for the CONNECT answer before sending");
|
|
223
|
+
return terminator.terminate(client, { sandboxId, host, addresses: allowed });
|
|
224
|
+
}
|
|
212
225
|
const upstream = await dialFirst(allowed.map((entry) => entry.address), port);
|
|
213
226
|
if (!upstream)
|
|
214
227
|
return refuse(502, "connect_failed", { sandboxId }, `could not connect to ${host}:${port}`);
|
|
@@ -34,6 +34,10 @@ export interface TenantVault {
|
|
|
34
34
|
authorizeMcp(sessionId: string, request: McpCredentialRequest): Promise<AuthorizeResult>;
|
|
35
35
|
/** Vault writes and token signing with the Tenant's vault key (the keys service, F4.2). */
|
|
36
36
|
keys(): Keys;
|
|
37
|
+
/** Whether a live session on the sandbox has an `environment_secret` bound to `host` (R2c). */
|
|
38
|
+
environmentSecretBound(sandboxId: string, host: string): Promise<boolean>;
|
|
39
|
+
/** The header an `environment_secret` sets on one request to `host` (R2c). */
|
|
40
|
+
releaseEnvironmentSecret(sandboxId: string, host: string): ReturnType<VaultService["releaseEnvironmentSecret"]>;
|
|
37
41
|
}
|
|
38
42
|
export interface TenantVaults {
|
|
39
43
|
/**
|
|
@@ -52,6 +52,8 @@ export function createTenantVaults(options) {
|
|
|
52
52
|
session,
|
|
53
53
|
authorizeMcp: (sessionId, request) => authorizeSessionMcp(credentials, session, sessionId, request),
|
|
54
54
|
keys: () => keys,
|
|
55
|
+
environmentSecretBound: (sandboxId, host) => credentials.environmentSecretBound(sandboxId, host),
|
|
56
|
+
releaseEnvironmentSecret: (sandboxId, host) => credentials.releaseEnvironmentSecret({ sandboxId, host }),
|
|
55
57
|
};
|
|
56
58
|
}
|
|
57
59
|
/** The database's Tenant, once its schema is at this build's version. */
|