@nylorun/runtime 0.33.0-beta → 0.34.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 +16 -0
- package/dist/api/ag-ui/routes.js +4 -2
- package/dist/api/ag-ui/translate.js +3 -1
- package/dist/gates/credential-provider.d.ts +42 -0
- package/dist/gates/credential-provider.js +140 -0
- package/dist/gates/egress-credentials.d.ts +12 -0
- package/dist/gates/egress-credentials.js +60 -6
- package/dist/gates/egress.d.ts +14 -1
- package/dist/gates/egress.js +67 -19
- package/dist/harness-api/start.js +29 -2
- package/dist/host/gates.d.ts +8 -1
- package/dist/host/gates.js +6 -1
- package/dist/host/main.js +15 -1
- package/dist/host/stack-config.d.ts +12 -1
- package/dist/host/stack-config.js +32 -1
- package/dist/management-openapi.json +3 -2
- package/dist/openapi.json +3 -2
- package/dist/tenant/context.d.ts +6 -0
- package/dist/tenant/runtime.d.ts +6 -0
- package/dist/tenant/runtime.js +1 -0
- package/dist/tuning.d.ts +6 -0
- package/dist/tuning.js +14 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.34.0-beta
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2ef610b: An outside credential provider for the egress gate (Host feature `credential-provider`; no protocol change). A Host's operator names a service that holds people's keys with `NYLORUN_CREDENTIAL_PROVIDER_URL` and `NYLORUN_CREDENTIAL_PROVIDER_TOKEN`, which `nylorun start` passes from its shell to the `gateway` and `runtime` containers. For a pod Agent with an owner, egress-gate asks the provider (`POST /v1/hosts`, with the Agent and its owner) which hosts the owner connected, lets them through on 443 inside the Tenant's egress ceiling, terminates TLS for them and hands each request to `<url>/v1/forward/<path>` with the sandbox's own headers plus `nylorun-provider-host`, `nylorun-provider-agent`, `nylorun-provider-owner` and `nylorun-provider-token`; the provider adds the key and sends it on, so the Runtime never sees a value. Each segment's sandbox sees the variables the provider names (`POST /v1/environment`) as `nylorun_managed`, under the vault's own. A secret in the Agent's vault bound to the same host still wins. Answers are cached per Agent for 15 s (Host tuning `credentialProvider.*`); an Agent with no owner is never asked about, and a provider that fails answers nothing, so its hosts are refused as before.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 145dc06: The AG-UI endpoint follows AG-UI 1.0 more closely, on `@ag-ui/core` 1.0.3. `RUN_STARTED` now declares `protocolVersion: "1.0"`, which 1.0 asks every producer to set. A run from a client older than 1.0, which sends `null` for optional fields such as `forwardedProps`, `parentRunId`, `tools[].parameters`, `resume[].payload` or a media part's `metadata`, is now accepted, with those fields treated as absent; it used to be refused with `400 Invalid RunAgentInput`.
|
|
12
|
+
- Pin core to the tested release.
|
|
13
|
+
- Pin harness to the tested release.
|
|
14
|
+
- Updated dependencies [2ef610b]
|
|
15
|
+
- Updated dependencies
|
|
16
|
+
- @nylorun/core@0.30.0-beta
|
|
17
|
+
- @nylorun/harness@0.29.6-beta
|
|
18
|
+
|
|
3
19
|
## 0.33.0-beta
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/dist/api/ag-ui/routes.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* `CUSTOM nylorun.stream_closed`, and the client reattaches with a new token.
|
|
22
22
|
*/
|
|
23
23
|
import { randomUUID } from "node:crypto";
|
|
24
|
-
import { EventType, } from "@ag-ui/core";
|
|
24
|
+
import { EventType, omitOptionalNulls, } from "@ag-ui/core";
|
|
25
25
|
import { RunAgentInputSchema } from "@ag-ui/core/schemas";
|
|
26
26
|
import { PutSessionRequestSchema, } from "@nylorun/core/contracts";
|
|
27
27
|
import { messagesFromEvents, NYLORUN_FILE_PROVIDER } from "./history.js";
|
|
@@ -269,7 +269,9 @@ export function agUiCaller(scope, agentId) {
|
|
|
269
269
|
export async function startRun(ctx, scope, agentId, caller, body, response,
|
|
270
270
|
/** The request's trace context (R7.4): the turn the run starts joins the caller's trace. */
|
|
271
271
|
trace) {
|
|
272
|
-
|
|
272
|
+
// Clients before AG-UI 1.0 send `null` for optional fields (`forwardedProps`,
|
|
273
|
+
// `tools[].parameters`, `resume[].payload`, a media part's `metadata`), which 1.0 refuses.
|
|
274
|
+
const parsed = RunAgentInputSchema.safeParse(omitOptionalNulls(body, "RunAgentInput"));
|
|
273
275
|
if (!parsed.success)
|
|
274
276
|
return fail(400, "Invalid RunAgentInput", { code: "invalid_request" });
|
|
275
277
|
const input = parsed.data;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* model call's `invocationId`, a tool call is the model's `callId`, a tool result is
|
|
4
4
|
* `<callId>:result`, so live runs and `messagesFromEvents` agree.
|
|
5
5
|
*/
|
|
6
|
-
import { EventType } from "@ag-ui/core";
|
|
6
|
+
import { EventType, PROTOCOL_VERSION } from "@ag-ui/core";
|
|
7
7
|
import { isEmptyAssistantMessage, parseTranscriptEvent, } from "@nylorun/core/contracts";
|
|
8
8
|
/** The text a tool result shows; `undefined` while there is no result yet. */
|
|
9
9
|
export function toolResultText(result) {
|
|
@@ -170,6 +170,8 @@ export class RunTranslator {
|
|
|
170
170
|
type: EventType.RUN_STARTED,
|
|
171
171
|
threadId: this.threadId,
|
|
172
172
|
runId: this.runId,
|
|
173
|
+
// AG-UI 1.0 producers declare the version; the SDKs do not fill it in.
|
|
174
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
173
175
|
};
|
|
174
176
|
}
|
|
175
177
|
finished(outcome, result) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Logger } from "../tenant/types.js";
|
|
2
|
+
/** The headers the engine adds to a request it hands to the provider's forward route. */
|
|
3
|
+
export declare const PROVIDER_HEADERS: {
|
|
4
|
+
/** The host the sandbox addressed, e.g. `api.vercel.com`. */
|
|
5
|
+
readonly host: "nylorun-provider-host";
|
|
6
|
+
/** The Agent (the egress sandbox) the request comes from. */
|
|
7
|
+
readonly agent: "nylorun-provider-agent";
|
|
8
|
+
/** The Agent's owner, e.g. `u:<org>:<user>`. */
|
|
9
|
+
readonly owner: "nylorun-provider-owner";
|
|
10
|
+
/** The provider's token: the sandbox's own `Authorization` header is left as it sent it. */
|
|
11
|
+
readonly token: "nylorun-provider-token";
|
|
12
|
+
};
|
|
13
|
+
/** Every header of this prefix a sandbox sends is dropped before the engine sets its own. */
|
|
14
|
+
export declare const PROVIDER_HEADER_PREFIX = "nylorun-provider-";
|
|
15
|
+
/** Where the provider is and its token (`StackConfig.credentialProvider`). */
|
|
16
|
+
export interface CredentialProviderSettings {
|
|
17
|
+
/** `NYLORUN_CREDENTIAL_PROVIDER_URL`, without a trailing slash. */
|
|
18
|
+
readonly url: string;
|
|
19
|
+
/** `NYLORUN_CREDENTIAL_PROVIDER_TOKEN`. */
|
|
20
|
+
readonly token: string;
|
|
21
|
+
}
|
|
22
|
+
export interface CredentialProvider {
|
|
23
|
+
/** The provider's base URL, without a trailing slash. */
|
|
24
|
+
readonly url: string;
|
|
25
|
+
/** The variables to set to the stand-in for `agentId`'s sessions; none without an owner. */
|
|
26
|
+
environment(agentId: string, owner: string | undefined): Promise<readonly string[]>;
|
|
27
|
+
/** The exact host names `owner`'s keys are bound to (lowercase); none without an owner. */
|
|
28
|
+
hosts(agentId: string, owner: string | undefined): Promise<readonly string[]>;
|
|
29
|
+
/** Where one request to `path` (origin form, `/v2/projects?x=1`) is handed. */
|
|
30
|
+
forwardUrl(path: string): URL;
|
|
31
|
+
/** The headers that go with it, the provider's token among them. */
|
|
32
|
+
forwardHeaders(at: {
|
|
33
|
+
readonly host: string;
|
|
34
|
+
readonly agentId: string;
|
|
35
|
+
readonly owner: string;
|
|
36
|
+
}): Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
export declare function createCredentialProvider(settings: CredentialProviderSettings, options: {
|
|
39
|
+
readonly logger: Logger;
|
|
40
|
+
/** Tests replace the HTTP client. */
|
|
41
|
+
readonly fetch?: typeof fetch;
|
|
42
|
+
}): CredentialProvider;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The credential provider (v0.25 CP-E): a service outside the Runtime that holds a person's keys
|
|
3
|
+
* and adds them to the requests their Agents' sandboxes send, so the Runtime never sees a value.
|
|
4
|
+
* The Host's operator names it with `NYLORUN_CREDENTIAL_PROVIDER_URL` and
|
|
5
|
+
* `NYLORUN_CREDENTIAL_PROVIDER_TOKEN` (Host feature `credential-provider`); without them nothing
|
|
6
|
+
* here runs.
|
|
7
|
+
*
|
|
8
|
+
* Three calls, each with `Authorization: Bearer <token>`:
|
|
9
|
+
* - `POST <url>/v1/hosts` `{ agentId, owner }` → `{ hosts: string[] }`: the exact host names the
|
|
10
|
+
* owner's keys are bound to. egress-gate lets a pod Agent's sandbox reach them (inside the
|
|
11
|
+
* Tenant's egress ceiling), terminates TLS for them on 443 and hands each request on;
|
|
12
|
+
* - `POST <url>/v1/environment` `{ agentId, owner }` → `{ variables: string[] }`: the variables
|
|
13
|
+
* each of a pod Agent's segments sets to the stand-in `nylorun_managed`, under the vault's own;
|
|
14
|
+
* - `ANY <url>/v1/forward/<path and query>`: one request of the sandbox's, with its headers as it
|
|
15
|
+
* sent them (its own `Authorization` or inject header included, which the provider replaces),
|
|
16
|
+
* plus `nylorun-provider-host`, `-agent`, `-owner` and, in place of a bearer the sandbox's own
|
|
17
|
+
* `Authorization` would clash with, `nylorun-provider-token`. The provider sends it on to
|
|
18
|
+
* `https://<host>/<path>` and streams the answer back (`egress-credentials.ts`).
|
|
19
|
+
*
|
|
20
|
+
* Only an Agent with an owner (its owner guard) is asked about; one without gets nothing and no
|
|
21
|
+
* call. An answer is kept per Agent and owner for `credentialProvider.cacheMs` (15 s). A call
|
|
22
|
+
* that fails (unreachable, slow past `timeoutMs`, a non-2xx answer, a body of another shape)
|
|
23
|
+
* answers nothing, kept for `retryMs`, so the gate refuses what it refused before; it is logged
|
|
24
|
+
* once until a call succeeds again. The token is never logged.
|
|
25
|
+
*/
|
|
26
|
+
import { isSandboxHostPattern } from "@nylorun/core/define";
|
|
27
|
+
import { hostTuning } from "../tuning.js";
|
|
28
|
+
/** The headers the engine adds to a request it hands to the provider's forward route. */
|
|
29
|
+
export const PROVIDER_HEADERS = {
|
|
30
|
+
/** The host the sandbox addressed, e.g. `api.vercel.com`. */
|
|
31
|
+
host: "nylorun-provider-host",
|
|
32
|
+
/** The Agent (the egress sandbox) the request comes from. */
|
|
33
|
+
agent: "nylorun-provider-agent",
|
|
34
|
+
/** The Agent's owner, e.g. `u:<org>:<user>`. */
|
|
35
|
+
owner: "nylorun-provider-owner",
|
|
36
|
+
/** The provider's token: the sandbox's own `Authorization` header is left as it sent it. */
|
|
37
|
+
token: "nylorun-provider-token",
|
|
38
|
+
};
|
|
39
|
+
/** Every header of this prefix a sandbox sends is dropped before the engine sets its own. */
|
|
40
|
+
export const PROVIDER_HEADER_PREFIX = "nylorun-provider-";
|
|
41
|
+
/** The most host names, or variables, one answer may hold; the rest are ignored. */
|
|
42
|
+
const ANSWER_MAX = 256;
|
|
43
|
+
/** A variable name a shell can set. */
|
|
44
|
+
const VARIABLE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
45
|
+
export function createCredentialProvider(settings, options) {
|
|
46
|
+
const url = settings.url.replace(/\/+$/, "");
|
|
47
|
+
const { logger } = options;
|
|
48
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
49
|
+
const cache = new Map();
|
|
50
|
+
const pending = new Map();
|
|
51
|
+
/** The calls failing now: each is logged once, until one succeeds. */
|
|
52
|
+
const failing = new Set();
|
|
53
|
+
function fail(call, agentId, reason, fields = {}) {
|
|
54
|
+
if (!failing.has(call)) {
|
|
55
|
+
failing.add(call);
|
|
56
|
+
logger.warn("credential_provider_failed", { call, agentId, reason, ...fields });
|
|
57
|
+
}
|
|
58
|
+
return { ok: false, value: [] };
|
|
59
|
+
}
|
|
60
|
+
async function ask(call, agentId, owner) {
|
|
61
|
+
let response;
|
|
62
|
+
try {
|
|
63
|
+
response = await fetchImpl(`${url}/v1/${call}`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { authorization: `Bearer ${settings.token}`, "content-type": "application/json", accept: "application/json" },
|
|
66
|
+
body: JSON.stringify({ agentId, owner }),
|
|
67
|
+
signal: AbortSignal.timeout(hostTuning().credentialProvider.timeoutMs),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
const name = error instanceof Error ? error.name : "Error";
|
|
72
|
+
return fail(call, agentId, name === "TimeoutError" ? "timeout" : "unreachable", {
|
|
73
|
+
code: error?.cause?.code ?? name,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
await response.body?.cancel().catch(() => undefined);
|
|
78
|
+
return fail(call, agentId, "status", { status: response.status });
|
|
79
|
+
}
|
|
80
|
+
let body;
|
|
81
|
+
try {
|
|
82
|
+
body = await response.json();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return fail(call, agentId, "not_json");
|
|
86
|
+
}
|
|
87
|
+
const list = body?.[call === "hosts" ? "hosts" : "variables"];
|
|
88
|
+
if (!Array.isArray(list))
|
|
89
|
+
return fail(call, agentId, "shape");
|
|
90
|
+
if (failing.delete(call))
|
|
91
|
+
logger.info("credential_provider_recovered", { call });
|
|
92
|
+
const items = list.filter((item) => typeof item === "string").slice(0, ANSWER_MAX);
|
|
93
|
+
const value = call === "hosts"
|
|
94
|
+
? [...new Set(items.map((host) => host.toLowerCase().replace(/\.$/, "")).filter(isExactHost))]
|
|
95
|
+
: [...new Set(items.filter((name) => VARIABLE.test(name)))];
|
|
96
|
+
return { ok: true, value };
|
|
97
|
+
}
|
|
98
|
+
function answer(call, agentId, owner) {
|
|
99
|
+
if (owner === undefined || owner === "")
|
|
100
|
+
return Promise.resolve([]);
|
|
101
|
+
const key = `${call}\u0000${agentId}\u0000${owner}`;
|
|
102
|
+
const cached = cache.get(key);
|
|
103
|
+
if (cached && Date.now() < cached.until)
|
|
104
|
+
return Promise.resolve(cached.value);
|
|
105
|
+
const inFlight = pending.get(key);
|
|
106
|
+
if (inFlight)
|
|
107
|
+
return inFlight;
|
|
108
|
+
const tuning = hostTuning().credentialProvider;
|
|
109
|
+
const asked = ask(call, agentId, owner)
|
|
110
|
+
.catch(() => fail(call, agentId, "failed"))
|
|
111
|
+
.then(({ ok, value }) => {
|
|
112
|
+
// A failure is kept for `retryMs`, an answer for `cacheMs`.
|
|
113
|
+
const ttl = ok ? tuning.cacheMs : tuning.retryMs;
|
|
114
|
+
cache.delete(key);
|
|
115
|
+
cache.set(key, { until: Date.now() + ttl, value });
|
|
116
|
+
if (cache.size > tuning.cacheEntries)
|
|
117
|
+
cache.delete(cache.keys().next().value);
|
|
118
|
+
return value;
|
|
119
|
+
})
|
|
120
|
+
.finally(() => pending.delete(key));
|
|
121
|
+
pending.set(key, asked);
|
|
122
|
+
return asked;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
url,
|
|
126
|
+
environment: (agentId, owner) => answer("environment", agentId, owner),
|
|
127
|
+
hosts: (agentId, owner) => answer("hosts", agentId, owner),
|
|
128
|
+
forwardUrl: (path) => new URL(`${url}/v1/forward${path.startsWith("/") ? path : `/${path}`}`),
|
|
129
|
+
forwardHeaders: ({ host, agentId, owner }) => ({
|
|
130
|
+
[PROVIDER_HEADERS.host]: host,
|
|
131
|
+
[PROVIDER_HEADERS.agent]: agentId,
|
|
132
|
+
[PROVIDER_HEADERS.owner]: owner,
|
|
133
|
+
[PROVIDER_HEADERS.token]: settings.token,
|
|
134
|
+
}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** An exact host name (no `*.` pattern, no IP literal), as egress-gate serves. */
|
|
138
|
+
function isExactHost(host) {
|
|
139
|
+
return !host.startsWith("*.") && isSandboxHostPattern(host);
|
|
140
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Duplex } from "node:stream";
|
|
2
2
|
import type { Logger } from "../tenant/types.js";
|
|
3
|
+
import { type CredentialProvider } from "./credential-provider.js";
|
|
3
4
|
/** What the vault answers for one request to a host a credential may be bound to. */
|
|
4
5
|
export type EgressCredentialRelease = {
|
|
5
6
|
readonly status: "none";
|
|
@@ -34,6 +35,11 @@ export interface EgressTally {
|
|
|
34
35
|
requests: number;
|
|
35
36
|
readonly credentialIds: Set<string>;
|
|
36
37
|
}
|
|
38
|
+
/** The Agent a provider host's requests are handed on for, and its owner. */
|
|
39
|
+
export interface ProviderRoute {
|
|
40
|
+
readonly agentId: string;
|
|
41
|
+
readonly owner: string;
|
|
42
|
+
}
|
|
37
43
|
export interface CredentialTerminator {
|
|
38
44
|
/**
|
|
39
45
|
* Answers the CONNECT, terminates TLS for `host` and serves its requests, sending each to one
|
|
@@ -48,10 +54,16 @@ export interface CredentialTerminator {
|
|
|
48
54
|
}[];
|
|
49
55
|
/** Counted as requests are served, read when the connection closes. */
|
|
50
56
|
readonly tally: EgressTally;
|
|
57
|
+
/** A host the credential provider lists: its requests go to the provider's forward route. */
|
|
58
|
+
readonly provider?: ProviderRoute;
|
|
51
59
|
}): Promise<void>;
|
|
60
|
+
/** Closes the kept-alive connections to the credential provider. */
|
|
61
|
+
close(): void;
|
|
52
62
|
}
|
|
53
63
|
export declare function credentialTerminator(options: {
|
|
54
64
|
readonly credentials: EgressCredentials;
|
|
55
65
|
readonly logger: Logger;
|
|
56
66
|
readonly idleMs: number;
|
|
67
|
+
/** The outside credential provider, for hosts it lists. */
|
|
68
|
+
readonly provider?: CredentialProvider;
|
|
57
69
|
}): CredentialTerminator;
|
|
@@ -14,20 +14,29 @@
|
|
|
14
14
|
* as an event; a value never is. Upgrades (WebSocket) and HTTP/2 are not served on this path.
|
|
15
15
|
* Each connection keeps a tally for its `agent.egress` event (R7.3, D67): how many requests it
|
|
16
16
|
* read and which credentials it set, by id.
|
|
17
|
+
*
|
|
18
|
+
* A host the outside credential provider lists (`credential-provider.ts`) and the vault does not
|
|
19
|
+
* bind is terminated the same way, but its requests go to the provider's forward route instead:
|
|
20
|
+
* `<provider>/v1/forward<path and query>`, with every header the sandbox sent (its own
|
|
21
|
+
* `Authorization` or inject header included, for the provider to replace) but `host` and any
|
|
22
|
+
* `nylorun-provider-*`, plus the engine's `nylorun-provider-host`, `-agent`, `-owner` and
|
|
23
|
+
* `-token`. Bodies stream both ways; the tally counts the requests, with no credential id.
|
|
17
24
|
*/
|
|
18
|
-
import { createServer } from "node:http";
|
|
25
|
+
import { Agent as HttpAgent, createServer, request as httpRequest } from "node:http";
|
|
19
26
|
import { Agent, request } from "node:https";
|
|
20
27
|
import { createSecureContext, TLSSocket } from "node:tls";
|
|
21
28
|
import { hostTuning } from "../tuning.js";
|
|
22
29
|
import { HOP_BY_HOP_HEADERS } from "../vault/headers.js";
|
|
30
|
+
import { PROVIDER_HEADER_PREFIX } from "./credential-provider.js";
|
|
23
31
|
/** The answer to a CONNECT the gate serves; a tunnel's byte counts leave it out. */
|
|
24
32
|
export const CONNECT_ESTABLISHED = "HTTP/1.1 200 Connection Established\r\n\r\n";
|
|
25
|
-
/** `rawHeaders` without hop-by-hop headers and `drop
|
|
33
|
+
/** `rawHeaders` without hop-by-hop headers and those `drop` matches, as lowercase names to values. */
|
|
26
34
|
function forwardable(raw, drop) {
|
|
27
35
|
const headers = {};
|
|
36
|
+
const dropped = typeof drop === "function" ? drop : (name) => name === drop;
|
|
28
37
|
for (let i = 0; i + 1 < raw.length; i += 2) {
|
|
29
38
|
const name = raw[i].toLowerCase();
|
|
30
|
-
if (HOP_BY_HOP_HEADERS.has(name) || name
|
|
39
|
+
if (HOP_BY_HOP_HEADERS.has(name) || dropped(name))
|
|
31
40
|
continue;
|
|
32
41
|
(headers[name] ??= []).push(raw[i + 1]);
|
|
33
42
|
}
|
|
@@ -43,8 +52,14 @@ function reply(res, status, message) {
|
|
|
43
52
|
res.end(body);
|
|
44
53
|
}
|
|
45
54
|
export function credentialTerminator(options) {
|
|
46
|
-
const { credentials, logger } = options;
|
|
55
|
+
const { credentials, logger, provider } = options;
|
|
47
56
|
const terminated = new WeakMap();
|
|
57
|
+
// One kept-alive pool to the provider, shared by every terminated connection.
|
|
58
|
+
const providerAgent = provider
|
|
59
|
+
? new URL(provider.url).protocol === "https:"
|
|
60
|
+
? new Agent({ keepAlive: true })
|
|
61
|
+
: new HttpAgent({ keepAlive: true })
|
|
62
|
+
: undefined;
|
|
48
63
|
const http = createServer((req, res) => {
|
|
49
64
|
forward(req, res).catch((error) => {
|
|
50
65
|
logger.error("egress_forward_failed", { error: error instanceof Error ? error.message : String(error) });
|
|
@@ -69,6 +84,8 @@ export function credentialTerminator(options) {
|
|
|
69
84
|
}
|
|
70
85
|
const { sandboxId, host } = at;
|
|
71
86
|
at.tally.requests += 1;
|
|
87
|
+
if (at.provider && provider)
|
|
88
|
+
return handOn(req, res, at, at.provider, provider);
|
|
72
89
|
const release = await credentials.release(sandboxId, host);
|
|
73
90
|
if (release.status === "refused") {
|
|
74
91
|
logger.warn("egress_credential_refused", { status: 502, reason: release.reason, sandboxId, host });
|
|
@@ -106,7 +123,44 @@ export function credentialTerminator(options) {
|
|
|
106
123
|
});
|
|
107
124
|
req.pipe(upstream);
|
|
108
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* One request of a provider host, handed to the provider's forward route as the sandbox sent
|
|
128
|
+
* it: its own headers stay (the provider replaces the bound one), `host` and any
|
|
129
|
+
* `nylorun-provider-*` it sent go, and the engine's provider headers are set.
|
|
130
|
+
*/
|
|
131
|
+
function handOn(req, res, at, route, to) {
|
|
132
|
+
const { sandboxId, host } = at;
|
|
133
|
+
if (!req.url?.startsWith("/")) {
|
|
134
|
+
req.resume();
|
|
135
|
+
return reply(res, 400, "requests on this connection name a path");
|
|
136
|
+
}
|
|
137
|
+
const target = to.forwardUrl(req.url);
|
|
138
|
+
const headers = forwardable(req.rawHeaders, (name) => name === "host" || name.startsWith(PROVIDER_HEADER_PREFIX));
|
|
139
|
+
for (const [name, value] of Object.entries(to.forwardHeaders({ host, agentId: route.agentId, owner: route.owner })))
|
|
140
|
+
headers[name] = [value];
|
|
141
|
+
const upstream = target.protocol === "https:"
|
|
142
|
+
? request(target, { method: req.method, headers, agent: providerAgent })
|
|
143
|
+
: httpRequest(target, { method: req.method, headers, agent: providerAgent });
|
|
144
|
+
upstream.on("response", (response) => {
|
|
145
|
+
res.writeHead(response.statusCode ?? 502, response.statusMessage, forwardable(response.rawHeaders));
|
|
146
|
+
response.pipe(res);
|
|
147
|
+
response.on("error", () => res.destroy());
|
|
148
|
+
});
|
|
149
|
+
upstream.on("error", (error) => {
|
|
150
|
+
logger.warn("egress_provider_failed", { status: 502, sandboxId, host, code: error.code });
|
|
151
|
+
reply(res, 502, `could not reach the credential provider for ${host}`);
|
|
152
|
+
});
|
|
153
|
+
req.on("error", () => upstream.destroy());
|
|
154
|
+
res.on("close", () => {
|
|
155
|
+
if (!res.writableFinished)
|
|
156
|
+
upstream.destroy();
|
|
157
|
+
});
|
|
158
|
+
req.pipe(upstream);
|
|
159
|
+
}
|
|
109
160
|
return {
|
|
161
|
+
close() {
|
|
162
|
+
providerAgent?.destroy();
|
|
163
|
+
},
|
|
110
164
|
async terminate(client, target) {
|
|
111
165
|
const { sandboxId, host, addresses, tally } = target;
|
|
112
166
|
const leaf = await credentials.leaf(host);
|
|
@@ -131,13 +185,13 @@ export function credentialTerminator(options) {
|
|
|
131
185
|
secureContext: createSecureContext({ key: leaf.key, cert: leaf.cert }),
|
|
132
186
|
ALPNProtocols: ["http/1.1"],
|
|
133
187
|
});
|
|
134
|
-
terminated.set(tls, { sandboxId, host, agent, tally });
|
|
188
|
+
terminated.set(tls, { sandboxId, host, agent, tally, ...(target.provider ? { provider: target.provider } : {}) });
|
|
135
189
|
tls.on("error", () => tls.destroy());
|
|
136
190
|
tls.on("close", () => {
|
|
137
191
|
agent.destroy();
|
|
138
192
|
client.destroy();
|
|
139
193
|
});
|
|
140
|
-
logger.info("egress_terminated", { sandboxId, host });
|
|
194
|
+
logger.info("egress_terminated", { sandboxId, host, ...(target.provider ? { via: "provider" } : {}) });
|
|
141
195
|
http.emit("connection", tls);
|
|
142
196
|
},
|
|
143
197
|
};
|
package/dist/gates/egress.d.ts
CHANGED
|
@@ -3,17 +3,23 @@ import { type Socket } from "node:net";
|
|
|
3
3
|
import type { EgressRunningTurn, AgentEventPayload } from "@nylorun/core/contracts";
|
|
4
4
|
import type { EgressTokenVerdict } from "../sandbox/egress-token.js";
|
|
5
5
|
import type { SessionStore } from "../store/types.js";
|
|
6
|
+
import type { CredentialProvider } from "./credential-provider.js";
|
|
6
7
|
import type { Logger } from "../tenant/types.js";
|
|
7
8
|
import { type EgressCredentials } from "./egress-credentials.js";
|
|
8
9
|
/**
|
|
9
10
|
* What egress-gate reads of a sandbox: its current host epoch, the pod that joined at it, and
|
|
10
11
|
* its spec's `network.allow`; `undefined` when it is gone, deleted, lost, expired or not a pod.
|
|
12
|
+
* For an Agent with an owner guard, also the owner and the Tenant's egress ceiling
|
|
13
|
+
* (`environments.config`'s `limits.network`) when the Tenant sets one: what the credential
|
|
14
|
+
* provider is asked about and kept inside.
|
|
11
15
|
*/
|
|
12
16
|
export interface EgressSandboxes {
|
|
13
17
|
live(sandboxId: string): Promise<{
|
|
14
18
|
readonly epoch: number;
|
|
15
19
|
readonly podUid?: string;
|
|
16
20
|
readonly allow: readonly string[];
|
|
21
|
+
readonly owner?: string;
|
|
22
|
+
readonly ceiling?: readonly string[];
|
|
17
23
|
} | undefined>;
|
|
18
24
|
}
|
|
19
25
|
/** One connection's record (R7.3, D67): `agent.egress`'s payload. */
|
|
@@ -63,6 +69,11 @@ export interface StartEgressGateOptions {
|
|
|
63
69
|
readonly credentials?: EgressCredentials;
|
|
64
70
|
/** Where connections are recorded (R7.3). None: they are only logged. */
|
|
65
71
|
readonly record?: EgressRecord;
|
|
72
|
+
/**
|
|
73
|
+
* The outside credential provider (`NYLORUN_CREDENTIAL_PROVIDER_URL`): the hosts it lists for an
|
|
74
|
+
* Agent's owner pass on 443 and are handed to it. Needs `credentials` (the leaf). None: off.
|
|
75
|
+
*/
|
|
76
|
+
readonly provider?: CredentialProvider;
|
|
66
77
|
}
|
|
67
78
|
export interface EgressGate {
|
|
68
79
|
/** Where the gate listens, e.g. `http://0.0.0.0:4200`. */
|
|
@@ -84,7 +95,9 @@ export declare function startEgressGate(options: StartEgressGateOptions): Promis
|
|
|
84
95
|
/**
|
|
85
96
|
* `EgressSandboxes` over the Tenant's `sandbox_resources` (one indexed read): a pod sandbox's
|
|
86
97
|
* host epoch, the pod that joined at it and its spec's allowlist; nothing for a virtual
|
|
87
|
-
* Environment, or one deleted or lost, as `staleHost` judges a host token.
|
|
98
|
+
* Environment, or one deleted or lost, as `staleHost` judges a host token. An Agent with an
|
|
99
|
+
* owner guard also gets its owner and, when the Tenant sets one, its egress ceiling (one more
|
|
100
|
+
* read, of the Tenant's `environments.config`).
|
|
88
101
|
*/
|
|
89
102
|
export declare function storeEgressSandboxes(store: SessionStore): EgressSandboxes;
|
|
90
103
|
/**
|
package/dist/gates/egress.js
CHANGED
|
@@ -11,17 +11,21 @@
|
|
|
11
11
|
* 2. its sandbox is live and its host epoch and pod are the token's (`EgressSandboxes.live`),
|
|
12
12
|
* else 407;
|
|
13
13
|
* 3. the port is 443 or 80, the target is a host name and no IP literal, and it matches the spec's
|
|
14
|
-
* `network.allow` exactly or by `*.suffix`,
|
|
14
|
+
* `network.allow` exactly or by `*.suffix`, or (443 only) the outside credential provider lists
|
|
15
|
+
* it for the Agent's owner and the Tenant's egress ceiling holds it, else 403;
|
|
15
16
|
* 4. the sandbox has fewer than `maxPerSandbox` tunnels open, else 429;
|
|
16
17
|
* 5. the name resolves, and to at least one address that is not private, loopback, link-local
|
|
17
18
|
* (cloud metadata included), multicast or reserved, else 502 or 403;
|
|
18
19
|
* 6. the gate connects to that checked address (never to the name again) and pipes.
|
|
19
20
|
*
|
|
20
|
-
* A tunnel is opaque bytes, with
|
|
21
|
+
* A tunnel is opaque bytes, with two exceptions. First (R2c, D50): port 443 of a host that an
|
|
21
22
|
* `environment_secret` of a live session on the sandbox is bound to. There the gate terminates
|
|
22
23
|
* TLS and sets the credential's header on every request (`egress-credentials.ts`), still sending
|
|
23
|
-
* only to the checked address.
|
|
24
|
-
* (
|
|
24
|
+
* only to the checked address. Second, port 443 of a host the credential provider lists
|
|
25
|
+
* (`credential-provider.ts`) and no secret binds: terminated the same way, its requests are handed
|
|
26
|
+
* to the provider's forward route, which adds the key; the vault's binding wins where both apply.
|
|
27
|
+
* Every other host gets the tunnel. Any other request (an absolute-form plain HTTP request
|
|
28
|
+
* included) is answered 405. Refusals are logged; the token
|
|
25
29
|
* never is. Tunnels close after `idleMs` without traffic in either direction.
|
|
26
30
|
*
|
|
27
31
|
* Every connection to a named host on a live token is recorded once (R7.3, D67), when it closes
|
|
@@ -40,6 +44,8 @@ import { createServer } from "node:http";
|
|
|
40
44
|
import { BlockList, connect, isIP } from "node:net";
|
|
41
45
|
import { isSandboxHostPattern } from "@nylorun/core/define";
|
|
42
46
|
import { isPrivateAddress } from "../tenant/outbound.js";
|
|
47
|
+
import { readSandboxConfig } from "../sandbox/tenant-config.js";
|
|
48
|
+
import { withinCeiling } from "../sandbox/resolve.js";
|
|
43
49
|
import { bindListener } from "../host/http.js";
|
|
44
50
|
import { hostTuning } from "../tuning.js";
|
|
45
51
|
import { CONNECT_ESTABLISHED, credentialTerminator, } from "./egress-credentials.js";
|
|
@@ -212,8 +218,25 @@ export async function startEgressGate(options) {
|
|
|
212
218
|
return value;
|
|
213
219
|
}
|
|
214
220
|
const terminator = options.credentials
|
|
215
|
-
? credentialTerminator({
|
|
221
|
+
? credentialTerminator({
|
|
222
|
+
credentials: options.credentials,
|
|
223
|
+
logger,
|
|
224
|
+
idleMs,
|
|
225
|
+
...(options.provider ? { provider: options.provider } : {}),
|
|
226
|
+
})
|
|
216
227
|
: undefined;
|
|
228
|
+
const provider = terminator ? options.provider : undefined;
|
|
229
|
+
/**
|
|
230
|
+
* Whether the credential provider lists `host` for the sandbox's owner, inside the Tenant's
|
|
231
|
+
* ceiling: only on 443, where the gate terminates TLS, and never for an Agent without an owner.
|
|
232
|
+
*/
|
|
233
|
+
async function providerLists(sandboxId, sandbox, host, port) {
|
|
234
|
+
if (!provider || port !== 443 || sandbox.owner === undefined)
|
|
235
|
+
return false;
|
|
236
|
+
if (sandbox.ceiling && !withinCeiling(host, sandbox.ceiling))
|
|
237
|
+
return false;
|
|
238
|
+
return (await provider.hosts(sandboxId, sandbox.owner)).includes(host);
|
|
239
|
+
}
|
|
217
240
|
const open = new Map();
|
|
218
241
|
const tunnels = new Set();
|
|
219
242
|
/** Each connection's record, once its Environment and target are known. */
|
|
@@ -257,7 +280,9 @@ export async function startEgressGate(options) {
|
|
|
257
280
|
return refuse(403, "port_refused", { sandboxId }, `port ${port} is not allowed; tunnels open to 443 or 80 only`);
|
|
258
281
|
if (isIP(host.replace(/^\[|\]$/g, "")) !== 0 || !isSandboxHostPattern(host) || host.startsWith("*."))
|
|
259
282
|
return refuse(403, "not_a_host_name", { sandboxId }, `${host} is not a host name; IP literals are refused`);
|
|
260
|
-
|
|
283
|
+
// A host the provider lists goes to it, even one the spec allows, unless the vault binds it.
|
|
284
|
+
const provided = await providerLists(sandboxId, sandbox, host, port);
|
|
285
|
+
if (!provided && !egressAllows(host, sandbox.allow))
|
|
261
286
|
return refuse(403, "host_not_allowed", { sandboxId }, `${host} is not in this sandbox's network.allow`);
|
|
262
287
|
// The turns running on the Environment as the connection opens: the read starts as the
|
|
263
288
|
// name resolves, and the 200 does not wait for it.
|
|
@@ -298,6 +323,20 @@ export async function startEgressGate(options) {
|
|
|
298
323
|
client.once("close", () => record.closed(tally));
|
|
299
324
|
return terminator.terminate(client, { sandboxId, host, addresses: allowed, tally });
|
|
300
325
|
}
|
|
326
|
+
if (terminator && provided && sandbox.owner !== undefined) {
|
|
327
|
+
record.credentialBound = true;
|
|
328
|
+
if (head.length > 0)
|
|
329
|
+
return refuse(403, "early_data", { sandboxId }, "wait for the CONNECT answer before sending");
|
|
330
|
+
const tally = { requests: 0, credentialIds: new Set() };
|
|
331
|
+
client.once("close", () => record.closed(tally));
|
|
332
|
+
return terminator.terminate(client, {
|
|
333
|
+
sandboxId,
|
|
334
|
+
host,
|
|
335
|
+
addresses: allowed,
|
|
336
|
+
tally,
|
|
337
|
+
provider: { agentId: sandboxId, owner: sandbox.owner },
|
|
338
|
+
});
|
|
339
|
+
}
|
|
301
340
|
const upstream = await dialFirst(allowed.map((entry) => entry.address), port);
|
|
302
341
|
if (!upstream)
|
|
303
342
|
return refuse(502, "connect_failed", { sandboxId }, `could not connect to ${host}:${port}`);
|
|
@@ -383,6 +422,7 @@ export async function startEgressGate(options) {
|
|
|
383
422
|
for (const tunnel of tunnels)
|
|
384
423
|
tunnel.destroy();
|
|
385
424
|
server.closeAllConnections();
|
|
425
|
+
terminator?.close();
|
|
386
426
|
});
|
|
387
427
|
return closing;
|
|
388
428
|
},
|
|
@@ -391,23 +431,31 @@ export async function startEgressGate(options) {
|
|
|
391
431
|
/**
|
|
392
432
|
* `EgressSandboxes` over the Tenant's `sandbox_resources` (one indexed read): a pod sandbox's
|
|
393
433
|
* host epoch, the pod that joined at it and its spec's allowlist; nothing for a virtual
|
|
394
|
-
* Environment, or one deleted or lost, as `staleHost` judges a host token.
|
|
434
|
+
* Environment, or one deleted or lost, as `staleHost` judges a host token. An Agent with an
|
|
435
|
+
* owner guard also gets its owner and, when the Tenant sets one, its egress ceiling (one more
|
|
436
|
+
* read, of the Tenant's `environments.config`).
|
|
395
437
|
*/
|
|
396
438
|
export function storeEgressSandboxes(store) {
|
|
397
439
|
return {
|
|
398
440
|
async live(sandboxId) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
441
|
+
return store.tx(async (t) => {
|
|
442
|
+
const sandbox = await t.environment(sandboxId);
|
|
443
|
+
const pod = sandbox?.pod;
|
|
444
|
+
if (!sandbox || sandbox.kind !== "pod" || !pod)
|
|
445
|
+
return undefined;
|
|
446
|
+
if (pod.desired === "deleted" || pod.observed === "lost")
|
|
447
|
+
return undefined;
|
|
448
|
+
const owner = sandbox.ownerUserId;
|
|
449
|
+
const ceiling = owner === undefined ? undefined : (await readSandboxConfig(t)).limits?.network;
|
|
450
|
+
return {
|
|
451
|
+
epoch: pod.hostEpoch,
|
|
452
|
+
...(pod.podUid ? { podUid: pod.podUid } : {}),
|
|
453
|
+
// The applied allowlist: a recipe change writes it at once (Q8).
|
|
454
|
+
allow: sandbox.applied.network?.allowedHosts ?? [],
|
|
455
|
+
...(owner === undefined ? {} : { owner }),
|
|
456
|
+
...(ceiling === undefined ? {} : { ceiling }),
|
|
457
|
+
};
|
|
458
|
+
});
|
|
411
459
|
},
|
|
412
460
|
};
|
|
413
461
|
}
|
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
* outcomes are read; then the transcript is folded from the record (P0.3). `buildTurnStart`
|
|
5
5
|
* turns that into the run's `turn.start`: the checkpoint goes without its transcript, which the
|
|
6
6
|
* harness holds at the fold's cursor or reads once.
|
|
7
|
+
*
|
|
8
|
+
* A pod Agent's sandbox variables are its vault's (R2c), then, with an outside credential
|
|
9
|
+
* provider (`gates/credential-provider.ts`) and an owner on the Agent, each variable the provider
|
|
10
|
+
* names for that owner, set to the stand-in `nylorun_managed` and never over a vault variable.
|
|
11
|
+
* The provider is asked after the transaction, and one that fails adds nothing.
|
|
7
12
|
*/
|
|
8
|
-
import { harnessLimitsOf } from "@nylorun/core/contracts";
|
|
13
|
+
import { ENVIRONMENT_SECRET_SENTINEL, harnessLimitsOf } from "@nylorun/core/contracts";
|
|
9
14
|
import { SANDBOX_WORKSPACE } from "@nylorun/core/define";
|
|
10
15
|
import { isWorkflowManifest } from "../core/flow-host.js";
|
|
11
16
|
import { sandboxWorkspaceOf } from "../sandbox/share.js";
|
|
@@ -33,6 +38,8 @@ options = {}) {
|
|
|
33
38
|
const id = lease.sessionId;
|
|
34
39
|
// Read before the transaction: the first read of the Tenant may create the key.
|
|
35
40
|
const key = options.harness ? await traceKey(ctx.store) : undefined;
|
|
41
|
+
// The pod Agent and its owner the credential provider is asked about, once the transaction ends.
|
|
42
|
+
let providerAsk;
|
|
36
43
|
const opened = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
|
|
37
44
|
// A turn's first segment runs the version its Agent runs now (T3, D73).
|
|
38
45
|
if (options.harness && (await repinTurn(t, current)))
|
|
@@ -52,6 +59,10 @@ options = {}) {
|
|
|
52
59
|
const workspace = sandboxWorkspaceOf(current);
|
|
53
60
|
const resource = workspace.environmentId === undefined ? undefined : await t.environment(workspace.environmentId);
|
|
54
61
|
const environment = await ctx.vault.environmentVariables(t, current.environmentId);
|
|
62
|
+
providerAsk =
|
|
63
|
+
ctx.credentialProvider && resource?.kind === "pod" && resource.ownerUserId
|
|
64
|
+
? { agentId: resource.id, owner: resource.ownerUserId }
|
|
65
|
+
: undefined;
|
|
55
66
|
const sandbox = {
|
|
56
67
|
...workspace,
|
|
57
68
|
...(Object.keys(environment).length > 0 ? { environment } : {}),
|
|
@@ -84,7 +95,8 @@ options = {}) {
|
|
|
84
95
|
const model = options.harness && !isWorkflowManifest(opened.current.manifest)
|
|
85
96
|
? await currentModelDescriptor(ctx, opened.current.model)
|
|
86
97
|
: undefined;
|
|
87
|
-
const
|
|
98
|
+
const provided = await withProviderVariables(ctx, opened, providerAsk);
|
|
99
|
+
const started = model ? { ...provided, model } : provided;
|
|
88
100
|
// One history (P0.3): the engine resumes from the transcript folded from the record.
|
|
89
101
|
const { current } = started;
|
|
90
102
|
const cp = current.checkpoint;
|
|
@@ -101,6 +113,21 @@ options = {}) {
|
|
|
101
113
|
checkParity(id, transcript, transcriptOf(cp.state));
|
|
102
114
|
return { ...started, transcript, cursor };
|
|
103
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* `opened` with the credential provider's variables for `ask` added to its sandbox's environment
|
|
118
|
+
* as the stand-in, under the vault's own; `opened` itself when there are none.
|
|
119
|
+
*/
|
|
120
|
+
async function withProviderVariables(ctx, opened, ask) {
|
|
121
|
+
if (!ask || !ctx.credentialProvider)
|
|
122
|
+
return opened;
|
|
123
|
+
const names = await ctx.credentialProvider.environment(ask.agentId, ask.owner);
|
|
124
|
+
const vault = opened.sandbox.environment ?? {};
|
|
125
|
+
const added = names.filter((name) => !Object.hasOwn(vault, name));
|
|
126
|
+
if (added.length === 0)
|
|
127
|
+
return opened;
|
|
128
|
+
const environment = { ...Object.fromEntries(added.map((name) => [name, ENVIRONMENT_SECRET_SENTINEL])), ...vault };
|
|
129
|
+
return { ...opened, sandbox: { ...opened.sandbox, environment } };
|
|
130
|
+
}
|
|
104
131
|
/** The run's `turn.start`. */
|
|
105
132
|
export function buildTurnStart(ctx, segment) {
|
|
106
133
|
const { current } = segment;
|
package/dist/host/gates.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type { PostgresClient } from "../store/postgres/connect.js";
|
|
|
20
20
|
import type { Logger } from "../tenant/types.js";
|
|
21
21
|
import type { EgressConfig, GatesConfig } from "./stack-config.js";
|
|
22
22
|
import { type EgressGate, type StartEgressGateOptions } from "../gates/egress.js";
|
|
23
|
+
import { type CredentialProviderSettings } from "../gates/credential-provider.js";
|
|
23
24
|
export interface StartGatesOptions {
|
|
24
25
|
readonly gates: GatesConfig;
|
|
25
26
|
readonly logger: Logger;
|
|
@@ -71,10 +72,16 @@ export interface StartEgressOptions {
|
|
|
71
72
|
readonly vaults?: TenantVaults;
|
|
72
73
|
/** Replaces how sandboxes are read, and the gate's limits and hooks (tests). */
|
|
73
74
|
readonly gate?: Partial<Omit<StartEgressGateOptions, "listen" | "logger" | "verify">>;
|
|
75
|
+
/**
|
|
76
|
+
* The outside credential provider (`NYLORUN_CREDENTIAL_PROVIDER_*`, Host feature
|
|
77
|
+
* `credential-provider`): the hosts it lists for an Agent's owner are handed to it.
|
|
78
|
+
*/
|
|
79
|
+
readonly credentialProvider?: CredentialProviderSettings;
|
|
74
80
|
}
|
|
75
81
|
/**
|
|
76
82
|
* egress-gate (`--service egress`, F7.2): pods' CONNECT proxy, verifying egress tokens against the
|
|
77
83
|
* Tenant's public keys and reading its sandboxes from the same database as the gates, where it
|
|
78
|
-
* also records each connection (R7.3).
|
|
84
|
+
* also records each connection (R7.3). With a credential provider, the hosts it lists for an
|
|
85
|
+
* Agent's owner are terminated and handed to it.
|
|
79
86
|
*/
|
|
80
87
|
export declare function startEgress(options: StartEgressOptions): Promise<EgressGate>;
|
package/dist/host/gates.js
CHANGED
|
@@ -30,6 +30,7 @@ import { bindListener, headerValue, isAllowedRequestHost, sendRejected } from ".
|
|
|
30
30
|
import { startEgressGate, storeEgressRecord, storeEgressSandboxes, } from "../gates/egress.js";
|
|
31
31
|
import { verifyEgressToken } from "../sandbox/egress-token.js";
|
|
32
32
|
import { leafKeyPair } from "../keys/x509.js";
|
|
33
|
+
import { createCredentialProvider } from "../gates/credential-provider.js";
|
|
33
34
|
import { hostTuning } from "../tuning.js";
|
|
34
35
|
export async function startGates(options) {
|
|
35
36
|
const { gates, logger, database } = options;
|
|
@@ -159,7 +160,8 @@ export async function startGates(options) {
|
|
|
159
160
|
/**
|
|
160
161
|
* egress-gate (`--service egress`, F7.2): pods' CONNECT proxy, verifying egress tokens against the
|
|
161
162
|
* Tenant's public keys and reading its sandboxes from the same database as the gates, where it
|
|
162
|
-
* also records each connection (R7.3).
|
|
163
|
+
* also records each connection (R7.3). With a credential provider, the hosts it lists for an
|
|
164
|
+
* Agent's owner are terminated and handed to it.
|
|
163
165
|
*/
|
|
164
166
|
export async function startEgress(options) {
|
|
165
167
|
const { database } = options;
|
|
@@ -194,6 +196,9 @@ export async function startEgress(options) {
|
|
|
194
196
|
return { key: pair.privateKey, cert };
|
|
195
197
|
},
|
|
196
198
|
},
|
|
199
|
+
...(options.credentialProvider
|
|
200
|
+
? { provider: createCredentialProvider(options.credentialProvider, { logger: options.logger }) }
|
|
201
|
+
: {}),
|
|
197
202
|
...options.gate,
|
|
198
203
|
});
|
|
199
204
|
}
|
package/dist/host/main.js
CHANGED
|
@@ -63,6 +63,7 @@ import { createRunGrants } from "../tenant/run-grants.js";
|
|
|
63
63
|
import { runHarness } from "../harness/main.js";
|
|
64
64
|
import { startHarnessListener } from "../harness-api/ws-server.js";
|
|
65
65
|
import { httpSandboxesClient } from "../sandbox/pods/client.js";
|
|
66
|
+
import { createCredentialProvider } from "../gates/credential-provider.js";
|
|
66
67
|
import { retry } from "../retry.js";
|
|
67
68
|
import { hostTuning, useHostTuning } from "../tuning.js";
|
|
68
69
|
import { useHostIdentity } from "../vault/host-identity.js";
|
|
@@ -169,6 +170,7 @@ async function runGates(stack) {
|
|
|
169
170
|
...(stack.packing ? { packing: stack.packing } : {}),
|
|
170
171
|
endpoints: describeEndpoints(stack.endpoints),
|
|
171
172
|
objectStore: stack.objectStore ? "s3" : "none",
|
|
173
|
+
...(stack.credentialProvider ? { credentialProvider: stack.credentialProvider.url } : {}),
|
|
172
174
|
});
|
|
173
175
|
warnRemovedSettings(stack, logger);
|
|
174
176
|
const database = createDatabase(stack);
|
|
@@ -189,7 +191,13 @@ async function runGates(stack) {
|
|
|
189
191
|
logger.info("gates_ready", { url: server.url });
|
|
190
192
|
}
|
|
191
193
|
if (stack.egress) {
|
|
192
|
-
const egress = await startEgress({
|
|
194
|
+
const egress = await startEgress({
|
|
195
|
+
egress: stack.egress,
|
|
196
|
+
database,
|
|
197
|
+
hostRoot: resolveHostRoot(),
|
|
198
|
+
logger,
|
|
199
|
+
...(stack.credentialProvider ? { credentialProvider: stack.credentialProvider } : {}),
|
|
200
|
+
});
|
|
193
201
|
servers.push(egress);
|
|
194
202
|
logger.info("egress_ready", { url: egress.url });
|
|
195
203
|
}
|
|
@@ -271,6 +279,7 @@ export async function main() {
|
|
|
271
279
|
endpoints: describeEndpoints(stack.endpoints),
|
|
272
280
|
objectStore: stack.objectStore ? "s3" : "fs",
|
|
273
281
|
traces: stack.traces?.export ? "otlp" : "off",
|
|
282
|
+
...(stack.credentialProvider ? { credentialProvider: stack.credentialProvider.url } : {}),
|
|
274
283
|
});
|
|
275
284
|
const infra = createInfra(stack, { logger });
|
|
276
285
|
const database = infra.database;
|
|
@@ -377,6 +386,10 @@ export async function main() {
|
|
|
377
386
|
harnessImage: stack.sandboxes.harnessImage,
|
|
378
387
|
}
|
|
379
388
|
: undefined;
|
|
389
|
+
// The outside credential provider: each pod Agent segment's variables it names (CP-E).
|
|
390
|
+
const credentialProvider = stack.credentialProvider
|
|
391
|
+
? createCredentialProvider(stack.credentialProvider, { logger })
|
|
392
|
+
: undefined;
|
|
380
393
|
const tenantSettings = stack.tenant ?? { name: "default" };
|
|
381
394
|
const module = createTenantModule({
|
|
382
395
|
open: createPostgresTenantOpener({
|
|
@@ -404,6 +417,7 @@ export async function main() {
|
|
|
404
417
|
...(blobs ? { blobs } : {}),
|
|
405
418
|
...(stack.harnessMode === "remote" ? { harness: "remote" } : {}),
|
|
406
419
|
...(pods ? { pods } : {}),
|
|
420
|
+
...(credentialProvider ? { credentialProvider } : {}),
|
|
407
421
|
...(stack.traces
|
|
408
422
|
? { traces: { resource: stack.traces.resource, content: new Set(stack.traces.content) } }
|
|
409
423
|
: {}),
|
|
@@ -42,7 +42,9 @@
|
|
|
42
42
|
* their source (`OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`) and which content they carry
|
|
43
43
|
* (`NYLORUN_TRACE_CONTENT`, or all of it with `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true`).
|
|
44
44
|
* So is the Host tuning (`NYLORUN_TUNING`, `tuning.ts`), for every service: its timings,
|
|
45
|
-
* retries and internal sizes.
|
|
45
|
+
* retries and internal sizes. So is the outside credential provider
|
|
46
|
+
* (`NYLORUN_CREDENTIAL_PROVIDER_URL`, `NYLORUN_CREDENTIAL_PROVIDER_TOKEN`), for egress-gate and
|
|
47
|
+
* a process running core or loop (`gates/credential-provider.ts`).
|
|
46
48
|
*/
|
|
47
49
|
import { type TraceResource } from "../traces/otlp.js";
|
|
48
50
|
import { type TraceContentKind } from "../traces/map.js";
|
|
@@ -193,6 +195,15 @@ export interface StackConfig {
|
|
|
193
195
|
* own identity (ADR-71, `vault/host-identity.ts`). Absent: none.
|
|
194
196
|
*/
|
|
195
197
|
modelHostIdentity?: string[];
|
|
198
|
+
/**
|
|
199
|
+
* `NYLORUN_CREDENTIAL_PROVIDER_URL` and `NYLORUN_CREDENTIAL_PROVIDER_TOKEN`, for a process running
|
|
200
|
+
* egress, core or loop (Host feature `credential-provider`): the outside service egress-gate
|
|
201
|
+
* hands an Agent owner's connected hosts to, and core asks for their variables. Absent: none.
|
|
202
|
+
*/
|
|
203
|
+
credentialProvider?: {
|
|
204
|
+
url: string;
|
|
205
|
+
token: string;
|
|
206
|
+
};
|
|
196
207
|
/** Present when the process runs gates. */
|
|
197
208
|
gates?: GatesConfig;
|
|
198
209
|
/** Present when the process runs egress. */
|
|
@@ -42,7 +42,9 @@
|
|
|
42
42
|
* their source (`OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`) and which content they carry
|
|
43
43
|
* (`NYLORUN_TRACE_CONTENT`, or all of it with `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true`).
|
|
44
44
|
* So is the Host tuning (`NYLORUN_TUNING`, `tuning.ts`), for every service: its timings,
|
|
45
|
-
* retries and internal sizes.
|
|
45
|
+
* retries and internal sizes. So is the outside credential provider
|
|
46
|
+
* (`NYLORUN_CREDENTIAL_PROVIDER_URL`, `NYLORUN_CREDENTIAL_PROVIDER_TOKEN`), for egress-gate and
|
|
47
|
+
* a process running core or loop (`gates/credential-provider.ts`).
|
|
46
48
|
*/
|
|
47
49
|
import { DEFAULT_SERVICE_NAME } from "../traces/otlp.js";
|
|
48
50
|
import { TRACE_CONTENT_KINDS } from "../traces/map.js";
|
|
@@ -329,6 +331,7 @@ export function parseStackConfig(env, argv) {
|
|
|
329
331
|
const tenant = servesApi ? parseTenant(env) : undefined;
|
|
330
332
|
const workloadToken = parseWorkloadToken(env);
|
|
331
333
|
const modelHostIdentity = parseModelHostIdentity(env);
|
|
334
|
+
const credentialProvider = servesApi || services.has("egress") ? parseCredentialProvider(env) : undefined;
|
|
332
335
|
const objectStore = parseObjectStore(env);
|
|
333
336
|
const traces = services.has("core") ? parseTraces(env) : undefined;
|
|
334
337
|
const removedResolverVariables = Object.keys(env)
|
|
@@ -353,6 +356,7 @@ export function parseStackConfig(env, argv) {
|
|
|
353
356
|
...(tenant ? { tenant } : {}),
|
|
354
357
|
...(workloadToken ? { workloadToken } : {}),
|
|
355
358
|
...(modelHostIdentity ? { modelHostIdentity } : {}),
|
|
359
|
+
...(credentialProvider ? { credentialProvider } : {}),
|
|
356
360
|
...(objectStore ? { objectStore } : {}),
|
|
357
361
|
...(traces ? { traces } : {}),
|
|
358
362
|
...(tuning ? { tuning } : {}),
|
|
@@ -483,6 +487,33 @@ function parseModelHostIdentity(env) {
|
|
|
483
487
|
}
|
|
484
488
|
return providers.length ? providers : undefined;
|
|
485
489
|
}
|
|
490
|
+
/**
|
|
491
|
+
* `StackConfig.credentialProvider`: the provider's `http` or `https` base URL (no credentials,
|
|
492
|
+
* query or fragment; `http` is for a provider on the Docker bridge or the machine itself) and its
|
|
493
|
+
* token, each required with the other.
|
|
494
|
+
*/
|
|
495
|
+
function parseCredentialProvider(env) {
|
|
496
|
+
const raw = read(env, "NYLORUN_CREDENTIAL_PROVIDER_URL");
|
|
497
|
+
const token = read(env, "NYLORUN_CREDENTIAL_PROVIDER_TOKEN");
|
|
498
|
+
if (raw === undefined && token === undefined)
|
|
499
|
+
return undefined;
|
|
500
|
+
if (raw === undefined)
|
|
501
|
+
throw new StackConfigError("NYLORUN_CREDENTIAL_PROVIDER_TOKEN is set without NYLORUN_CREDENTIAL_PROVIDER_URL: set the provider's base URL, such as http://172.17.0.1:18793/edge/provider");
|
|
502
|
+
if (token === undefined)
|
|
503
|
+
throw new StackConfigError("NYLORUN_CREDENTIAL_PROVIDER_TOKEN is required with NYLORUN_CREDENTIAL_PROVIDER_URL: the bearer the provider accepts");
|
|
504
|
+
let url;
|
|
505
|
+
try {
|
|
506
|
+
url = new URL(raw);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
throw new StackConfigError(`NYLORUN_CREDENTIAL_PROVIDER_URL is not a URL: ${raw}`);
|
|
510
|
+
}
|
|
511
|
+
if ((url.protocol !== "http:" && url.protocol !== "https:") || url.username || url.password || url.search || url.hash)
|
|
512
|
+
throw new StackConfigError("NYLORUN_CREDENTIAL_PROVIDER_URL must be an http or https base URL with no credentials, query or fragment, such as http://172.17.0.1:18793/edge/provider");
|
|
513
|
+
if (token.length < 16 || !/^[\x21-\x7e]+$/.test(token))
|
|
514
|
+
throw new StackConfigError("NYLORUN_CREDENTIAL_PROVIDER_TOKEN must be at least 16 printable characters with no spaces");
|
|
515
|
+
return { url: `${url.origin}${url.pathname}`.replace(/\/+$/, ""), token };
|
|
516
|
+
}
|
|
486
517
|
/** `NYLORUN_WORKLOAD_TOKEN_ORIGINS`: each an `https` origin (`http` only to a loopback host). */
|
|
487
518
|
function parseOrigins(raw) {
|
|
488
519
|
if (raw === undefined)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.2.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Nylorun Management API",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.34.0-beta",
|
|
6
6
|
"x-nylorun-protocol": {
|
|
7
7
|
"min": 16,
|
|
8
8
|
"max": 18,
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"gateway-credentials",
|
|
37
37
|
"agent-folders",
|
|
38
38
|
"session-model",
|
|
39
|
-
"model-host-identity"
|
|
39
|
+
"model-host-identity",
|
|
40
|
+
"credential-provider"
|
|
40
41
|
]
|
|
41
42
|
},
|
|
42
43
|
"description": "For operators: the Tenant's models, vaults, signing keys, settings and application keys, at `/v1/tenant/*` on the Tenant's URL, through `@nylorun/admin`. Every operation takes a management key, acting as itself: never for a subject and never from a browser. Management keys are issued only on the Tenant's machine (`nylorun tenant keys put <id> --management`) or from `NYLORUN_MANAGEMENT_KEY_FILE`."
|
package/dist/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.2.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Nylorun Runtime API",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.34.0-beta",
|
|
6
6
|
"x-nylorun-protocol": {
|
|
7
7
|
"min": 16,
|
|
8
8
|
"max": 18,
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"gateway-credentials",
|
|
37
37
|
"agent-folders",
|
|
38
38
|
"session-model",
|
|
39
|
-
"model-host-identity"
|
|
39
|
+
"model-host-identity",
|
|
40
|
+
"credential-provider"
|
|
40
41
|
]
|
|
41
42
|
},
|
|
42
43
|
"description": "For developers: agent manifests, Agents, sessions (with AG-UI and A2A) and artifacts, through `@nylorun/agents`. Every Tenant serves it on its one URL, beside the Management API (`/openapi/management.json`). Every request sends `Nylorun-Protocol`. Who may call each operation is its `security` and its `x-nylorun-credentials` and `x-nylorun-scopes` (the subject scopes that reach it) fields: an application key (servers) or a trusted issuer's token (browsers and apps)."
|
package/dist/tenant/context.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type { McpDiagnostic, McpSnapshot } from "../mcp/snapshot.js";
|
|
|
28
28
|
import type { HarnessApiServer } from "../harness-api/server.js";
|
|
29
29
|
import type { WorkspacePort } from "../harness-api/workspace.js";
|
|
30
30
|
import type { SandboxesClient } from "../sandbox/pods/client.js";
|
|
31
|
+
import type { CredentialProvider } from "../gates/credential-provider.js";
|
|
31
32
|
import type { SandboxSignal } from "../execution/types.js";
|
|
32
33
|
import type { TenantConfig } from "./types.js";
|
|
33
34
|
import type { SessionStreams } from "./session-streams.js";
|
|
@@ -290,6 +291,11 @@ export interface TenantContext {
|
|
|
290
291
|
* Absent without a cluster (`nylorun local sandbox enable`): kind `pod` is `agent_unavailable`.
|
|
291
292
|
*/
|
|
292
293
|
readonly pods?: TenantPods;
|
|
294
|
+
/**
|
|
295
|
+
* The outside credential provider (`gates/credential-provider.ts`): the variables a pod Agent's
|
|
296
|
+
* segments set to the stand-in for its owner. Absent: none.
|
|
297
|
+
*/
|
|
298
|
+
readonly credentialProvider?: CredentialProvider;
|
|
293
299
|
/**
|
|
294
300
|
* Seam: reconcile a pod sandbox, or arm one of its timers (`DurableExecution.sandbox`),
|
|
295
301
|
* outside any tx. What a transaction causes goes through `t.signalSandbox` instead, which
|
package/dist/tenant/runtime.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { type HarnessPeer } from "../harness-api/server.js";
|
|
|
15
15
|
import { type HostAuthority } from "../sandbox/join.js";
|
|
16
16
|
import type { TenantPods } from "./context.js";
|
|
17
17
|
import type { TraceOptions } from "../traces/options.js";
|
|
18
|
+
import type { CredentialProvider } from "../gates/credential-provider.js";
|
|
18
19
|
/** TENANTS-CCR: test/injection hooks until TenantConfig gains them. */
|
|
19
20
|
export type TenantOpenHooks = {
|
|
20
21
|
modelProvider?: ModelProvider;
|
|
@@ -84,6 +85,11 @@ export type TenantOpenHooks = {
|
|
|
84
85
|
* pods copy the engine from. Without it, kind `pod` is `agent_unavailable`.
|
|
85
86
|
*/
|
|
86
87
|
pods?: TenantPods;
|
|
88
|
+
/**
|
|
89
|
+
* The outside credential provider (`NYLORUN_CREDENTIAL_PROVIDER_*`): a pod Agent's segments set
|
|
90
|
+
* the variables it names for the Agent's owner to the stand-in, under the vault's own.
|
|
91
|
+
*/
|
|
92
|
+
credentialProvider?: CredentialProvider;
|
|
87
93
|
/** How the trace route makes spans (R7.5): the Host's `OTEL_*` resource and content opt-ins. */
|
|
88
94
|
traces?: TraceOptions;
|
|
89
95
|
} & OpenedTenant;
|
package/dist/tenant/runtime.js
CHANGED
|
@@ -261,6 +261,7 @@ export class TenantRuntime {
|
|
|
261
261
|
},
|
|
262
262
|
abortLocal: (sessionId, turnId) => abortLocal(ctx, sessionId, "cancel", turnId),
|
|
263
263
|
...(hooks.pods ? { pods: hooks.pods } : {}),
|
|
264
|
+
...(hooks.credentialProvider ? { credentialProvider: hooks.credentialProvider } : {}),
|
|
264
265
|
sandboxSignal: async (sandboxId, signal) => {
|
|
265
266
|
if (ctx.closing || ctx.closed)
|
|
266
267
|
return false;
|
package/dist/tuning.d.ts
CHANGED
|
@@ -240,6 +240,12 @@ export declare const HostTuningSchema: z.ZodObject<{
|
|
|
240
240
|
credentialsHeadersTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
241
241
|
credentialsMaxSockets: z.ZodDefault<z.ZodNumber>;
|
|
242
242
|
}, z.core.$strict>>;
|
|
243
|
+
credentialProvider: z.ZodPrefault<z.ZodObject<{
|
|
244
|
+
cacheMs: z.ZodDefault<z.ZodNumber>;
|
|
245
|
+
retryMs: z.ZodDefault<z.ZodNumber>;
|
|
246
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
247
|
+
cacheEntries: z.ZodDefault<z.ZodNumber>;
|
|
248
|
+
}, z.core.$strict>>;
|
|
243
249
|
pods: z.ZodPrefault<z.ZodObject<{
|
|
244
250
|
readyDeadlineMs: z.ZodDefault<z.ZodNumber>;
|
|
245
251
|
pollMs: z.ZodDefault<z.ZodNumber>;
|
package/dist/tuning.js
CHANGED
|
@@ -386,6 +386,20 @@ export const HostTuningSchema = z
|
|
|
386
386
|
/** The upstream sockets one such tunnel opens at once. */
|
|
387
387
|
credentialsMaxSockets: count(8),
|
|
388
388
|
}),
|
|
389
|
+
/**
|
|
390
|
+
* The outside credential provider (`NYLORUN_CREDENTIAL_PROVIDER_URL`), which egress-gate asks
|
|
391
|
+
* for an Agent's connected hosts and core for its variables (`gates/credential-provider.ts`).
|
|
392
|
+
*/
|
|
393
|
+
credentialProvider: group({
|
|
394
|
+
/** How long an Agent's answer (hosts, or variables) is reused. */
|
|
395
|
+
cacheMs: ms(15_000, 0),
|
|
396
|
+
/** How long a failed answer (nothing) is reused before the provider is asked again. */
|
|
397
|
+
retryMs: ms(5_000, 0),
|
|
398
|
+
/** How long one `hosts` or `environment` call may take. */
|
|
399
|
+
timeoutMs: ms(3_000),
|
|
400
|
+
/** How many Agents' answers are kept. */
|
|
401
|
+
cacheEntries: count(4_096),
|
|
402
|
+
}),
|
|
389
403
|
/** Sandbox pods through the sandboxes service (F7.2). */
|
|
390
404
|
pods: group({
|
|
391
405
|
/** How long a pod may take to be ready before its start fails. */
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const RUNTIME_VERSION = "0.
|
|
1
|
+
export declare const RUNTIME_VERSION = "0.34.0-beta";
|
package/dist/version.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Kept in step with package.json by runtime/server/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.
|
|
3
|
+
export const RUNTIME_VERSION = "0.34.0-beta";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.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",
|
|
@@ -59,14 +59,14 @@
|
|
|
59
59
|
"db:studio": "drizzle-kit studio"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@ag-ui/core": "~1.0.
|
|
62
|
+
"@ag-ui/core": "~1.0.3",
|
|
63
63
|
"@asteasolutions/zod-to-openapi": "^9.1.0",
|
|
64
64
|
"@earendil-works/pi-ai": "1.3.0",
|
|
65
65
|
"@hono/node-server": "^2.1.5",
|
|
66
66
|
"@hono/zod-openapi": "^1.6.3",
|
|
67
67
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
68
|
-
"@nylorun/core": "0.
|
|
69
|
-
"@nylorun/harness": "0.29.
|
|
68
|
+
"@nylorun/core": "0.30.0-beta",
|
|
69
|
+
"@nylorun/harness": "0.29.6-beta",
|
|
70
70
|
"@restatedev/restate-sdk": "1.17.3",
|
|
71
71
|
"@restatedev/restate-sdk-clients": "1.17.3",
|
|
72
72
|
"@s2-dev/streamstore": "0.28.0",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
87
|
"@a2a-js/sdk": "~1.3.0",
|
|
88
|
-
"@ag-ui/client": "1.0.
|
|
88
|
+
"@ag-ui/client": "1.0.3",
|
|
89
89
|
"@scalar/openapi-parser": "^0.29.13",
|
|
90
90
|
"@types/node": "^26.6.5",
|
|
91
91
|
"@types/pg": "^8.23.1",
|