@nylorun/runtime 0.15.2-beta → 0.17.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 +134 -0
- package/README.md +68 -29
- package/dist/adapters/execution/restate.d.ts +9 -1
- package/dist/adapters/execution/restate.js +64 -5
- package/dist/adapters/sandbox/local.d.ts +10 -0
- package/dist/adapters/sandbox/local.js +195 -0
- package/dist/adapters/sandbox/virtual.js +36 -1
- package/dist/admin-openapi.json +341 -7
- package/dist/api/a2a/tasks.js +6 -1
- package/dist/api/ag-ui/endpoint.js +1 -2
- package/dist/api/ag-ui/history.js +6 -1
- package/dist/api/ag-ui/routes.d.ts +1 -1
- package/dist/api/ag-ui/routes.js +15 -13
- package/dist/api/components.d.ts +2667 -711
- package/dist/api/components.js +24 -11
- package/dist/api/gate/keys.d.ts +4 -3
- package/dist/api/gate/routes.d.ts +19 -3
- package/dist/api/gate/routes.js +162 -53
- package/dist/api/http/app.js +6 -0
- package/dist/api/http/define.d.ts +19 -12
- package/dist/api/http/define.js +13 -27
- package/dist/api/http/routes/access.d.ts +3 -19
- package/dist/api/http/routes/access.js +12 -172
- package/dist/api/http/routes/actions.js +0 -1
- package/dist/api/http/routes/artifacts.d.ts +11 -0
- package/dist/api/http/routes/artifacts.js +516 -0
- package/dist/api/http/routes/me.d.ts +13 -0
- package/dist/api/http/routes/me.js +45 -0
- package/dist/api/http/routes/sandboxes.d.ts +9 -0
- package/dist/api/http/routes/sandboxes.js +150 -0
- package/dist/api/http/routes/sessions.js +9 -13
- package/dist/api/http/routes/tenant.js +36 -2
- package/dist/api/http/routes/vaults.d.ts +12 -3
- package/dist/api/http/routes/vaults.js +122 -54
- package/dist/api/openapi.js +4 -10
- package/dist/artifacts/config.d.ts +23 -0
- package/dist/artifacts/config.js +27 -0
- package/dist/artifacts/export.d.ts +8 -0
- package/dist/artifacts/export.js +205 -0
- package/dist/artifacts/files.d.ts +36 -0
- package/dist/artifacts/files.js +45 -0
- package/dist/artifacts/folders.d.ts +60 -0
- package/dist/artifacts/folders.js +209 -0
- package/dist/artifacts/links.d.ts +28 -0
- package/dist/artifacts/links.js +92 -0
- package/dist/artifacts/media-types.d.ts +13 -0
- package/dist/artifacts/media-types.js +80 -0
- package/dist/artifacts/parts.d.ts +34 -0
- package/dist/artifacts/parts.js +38 -0
- package/dist/artifacts/service.d.ts +74 -0
- package/dist/artifacts/service.js +301 -0
- package/dist/artifacts/workspace.d.ts +60 -0
- package/dist/artifacts/workspace.js +57 -0
- package/dist/artifacts/zip.d.ts +10 -0
- package/dist/artifacts/zip.js +126 -0
- package/dist/blob/fs.d.ts +13 -0
- package/dist/blob/fs.js +194 -0
- package/dist/blob/index.d.ts +9 -0
- package/dist/blob/index.js +9 -0
- package/dist/blob/s3.d.ts +42 -0
- package/dist/blob/s3.js +366 -0
- package/dist/blob/types.d.ts +108 -0
- package/dist/blob/types.js +85 -0
- package/dist/core/sandbox-routes.d.ts +2 -2
- package/dist/core/sandbox-routes.js +4 -3
- package/dist/execution/memory.d.ts +11 -1
- package/dist/execution/memory.js +77 -3
- package/dist/execution/types.d.ts +54 -1
- package/dist/execution/types.js +31 -0
- package/dist/gates/contract.d.ts +14 -13
- package/dist/gates/contract.js +13 -12
- package/dist/gates/egress.d.ts +72 -0
- package/dist/gates/egress.js +319 -0
- package/dist/gates/handler.d.ts +3 -0
- package/dist/gates/handler.js +3 -2
- package/dist/gates/http-client.d.ts +3 -2
- package/dist/gates/http-client.js +17 -6
- package/dist/gates/in-process.d.ts +13 -1
- package/dist/gates/in-process.js +11 -0
- package/dist/gates/inflight.d.ts +23 -4
- package/dist/gates/inflight.js +18 -4
- package/dist/gates/mcp-handler.js +5 -1
- package/dist/gates/model-gate.d.ts +6 -2
- package/dist/gates/tenant-vaults.d.ts +18 -9
- package/dist/gates/tenant-vaults.js +17 -12
- package/dist/gates/tool-calls.d.ts +9 -5
- package/dist/gates/tool-calls.js +6 -6
- package/dist/gates/tool-client.d.ts +9 -2
- package/dist/gates/tool-client.js +56 -22
- package/dist/gates/tool-contract.d.ts +12 -7
- package/dist/gates/tool-contract.js +10 -4
- package/dist/gates/tool-gate.d.ts +1 -0
- package/dist/harness/calls.d.ts +52 -0
- package/dist/harness/calls.js +113 -0
- package/dist/harness/executors.d.ts +22 -0
- package/dist/harness/executors.js +96 -0
- package/dist/harness/main.d.ts +6 -0
- package/dist/harness/main.js +103 -0
- package/dist/harness/pod.d.ts +21 -0
- package/dist/harness/pod.js +119 -0
- package/dist/harness/run-tokens.d.ts +18 -0
- package/dist/harness/run-tokens.js +15 -0
- package/dist/harness/service.d.ts +46 -0
- package/dist/harness/service.js +179 -0
- package/dist/harness/ws-client.d.ts +43 -0
- package/dist/harness/ws-client.js +122 -0
- package/dist/harness/ws-port.d.ts +22 -0
- package/dist/harness/ws-port.js +78 -0
- package/dist/harness-api/in-process.d.ts +24 -0
- package/dist/harness-api/in-process.js +119 -0
- package/dist/harness-api/record.d.ts +44 -0
- package/dist/harness-api/record.js +250 -0
- package/dist/harness-api/renew.d.ts +37 -0
- package/dist/harness-api/renew.js +77 -0
- package/dist/harness-api/server.d.ts +104 -0
- package/dist/harness-api/server.js +408 -0
- package/dist/harness-api/start.d.ts +36 -0
- package/dist/harness-api/start.js +118 -0
- package/dist/harness-api/workspace.d.ts +53 -0
- package/dist/harness-api/workspace.js +201 -0
- package/dist/harness-api/ws-server.d.ts +34 -0
- package/dist/harness-api/ws-server.js +227 -0
- package/dist/host/admin-api.d.ts +12 -5
- package/dist/host/admin-api.js +72 -6
- package/dist/host/app.d.ts +0 -1
- package/dist/host/app.js +35 -24
- package/dist/host/config-for.d.ts +4 -0
- package/dist/host/config-for.js +2 -0
- package/dist/host/config.d.ts +0 -6
- package/dist/host/create-host.d.ts +3 -10
- package/dist/host/create-host.js +4 -1
- package/dist/host/gates.d.ts +27 -2
- package/dist/host/gates.js +52 -2
- package/dist/host/http.d.ts +4 -1
- package/dist/host/http.js +6 -1
- package/dist/host/main.js +167 -37
- package/dist/host/stack-config.d.ts +112 -9
- package/dist/host/stack-config.js +222 -20
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/keys/client.js +9 -1
- package/dist/keys/keys.d.ts +12 -2
- package/dist/keys/keys.js +8 -0
- package/dist/mcp/connect.js +2 -1
- package/dist/mcp/pool.d.ts +1 -0
- package/dist/mcp/pool.js +1 -0
- package/dist/mcp/snapshot.d.ts +8 -1
- package/dist/mcp/snapshot.js +28 -0
- package/dist/media.d.ts +5 -24
- package/dist/media.js +5 -0
- package/dist/model/pi-model.d.ts +6 -2
- package/dist/model/pi-model.js +39 -22
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/openapi.json +14084 -9426
- package/dist/record/index.d.ts +1 -0
- package/dist/record/index.js +1 -0
- package/dist/record/sandbox.d.ts +25 -0
- package/dist/record/sandbox.js +43 -0
- package/dist/sandbox/capability.d.ts +3 -0
- package/dist/sandbox/capability.js +10 -0
- package/dist/sandbox/egress-token.d.ts +26 -0
- package/dist/sandbox/egress-token.js +8 -0
- package/dist/sandbox/join.d.ts +37 -0
- package/dist/sandbox/join.js +165 -0
- package/dist/sandbox/manager.d.ts +99 -34
- package/dist/sandbox/manager.js +216 -84
- package/dist/sandbox/placement.d.ts +27 -0
- package/dist/sandbox/placement.js +44 -0
- package/dist/sandbox/pods/client.d.ts +90 -0
- package/dist/sandbox/pods/client.js +79 -0
- package/dist/sandbox/pods/lifecycle.d.ts +91 -0
- package/dist/sandbox/pods/lifecycle.js +178 -0
- package/dist/sandbox/pods/name.d.ts +1 -0
- package/dist/sandbox/pods/name.js +28 -0
- package/dist/sandbox/pods/network-gate.d.ts +25 -0
- package/dist/sandbox/pods/network-gate.js +71 -0
- package/dist/sandbox/pods/reconcile.d.ts +11 -0
- package/dist/sandbox/pods/reconcile.js +152 -0
- package/dist/sandbox/pods/spec.d.ts +45 -0
- package/dist/sandbox/pods/spec.js +62 -0
- package/dist/sandbox/records.d.ts +29 -0
- package/dist/sandbox/records.js +79 -0
- package/dist/sandbox/resolve.d.ts +5 -0
- package/dist/sandbox/resolve.js +3 -2
- package/dist/sandbox/session-sandbox.js +7 -5
- package/dist/sandbox/share.d.ts +11 -0
- package/dist/sandbox/share.js +10 -0
- package/dist/sandbox/tenant-config.d.ts +16 -1
- package/dist/sandbox/tenant-config.js +9 -0
- package/dist/sandbox/types.d.ts +29 -1
- package/dist/sandbox/types.js +13 -1
- package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
- package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
- package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
- package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
- package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
- package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
- package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +49 -0
- package/dist/store/postgres/record-writer.d.ts +6 -1
- package/dist/store/postgres/record-writer.js +20 -2
- package/dist/store/postgres/schema.d.ts +1168 -250
- package/dist/store/postgres/schema.js +174 -28
- package/dist/store/postgres/store.js +339 -103
- package/dist/store/postgres/tenant.d.ts +1 -1
- package/dist/store/postgres/tenant.js +2 -2
- package/dist/store/types.d.ts +163 -32
- package/dist/streams/control.d.ts +2 -5
- package/dist/streams/control.js +5 -8
- package/dist/streams/types.d.ts +8 -8
- package/dist/streams/types.js +1 -1
- package/dist/tenant/advance.js +218 -160
- package/dist/tenant/artifact-tool.d.ts +5 -0
- package/dist/tenant/artifact-tool.js +119 -0
- package/dist/tenant/auth.d.ts +13 -9
- package/dist/tenant/auth.js +25 -32
- package/dist/tenant/commands.js +31 -10
- package/dist/tenant/context.d.ts +83 -26
- package/dist/tenant/delivery-token.js +4 -4
- package/dist/tenant/effects.d.ts +24 -14
- package/dist/tenant/effects.js +10 -398
- package/dist/tenant/ephemeral.d.ts +14 -2
- package/dist/tenant/ephemeral.js +8 -1
- package/dist/tenant/history.d.ts +13 -23
- package/dist/tenant/history.js +14 -62
- package/dist/tenant/host-token.d.ts +92 -0
- package/dist/tenant/host-token.js +179 -0
- package/dist/tenant/identity-file.d.ts +75 -0
- package/dist/tenant/identity-file.js +202 -0
- package/dist/tenant/issuers.d.ts +48 -0
- package/dist/tenant/issuers.js +311 -0
- package/dist/tenant/jwt.js +3 -3
- package/dist/tenant/module.js +1 -0
- package/dist/tenant/operator-keys.d.ts +18 -0
- package/dist/tenant/operator-keys.js +55 -0
- package/dist/tenant/outbound.d.ts +30 -0
- package/dist/tenant/outbound.js +142 -10
- package/dist/tenant/paths.d.ts +2 -1
- package/dist/tenant/paths.js +3 -1
- package/dist/tenant/principals.d.ts +2 -11
- package/dist/tenant/principals.js +10 -30
- package/dist/tenant/reset.d.ts +5 -2
- package/dist/tenant/reset.js +22 -2
- package/dist/tenant/run-grants.d.ts +49 -0
- package/dist/tenant/run-grants.js +62 -0
- package/dist/tenant/run-token.d.ts +70 -0
- package/dist/tenant/run-token.js +138 -0
- package/dist/tenant/runtime.d.ts +42 -0
- package/dist/tenant/runtime.js +134 -33
- package/dist/tenant/sandboxes.d.ts +119 -0
- package/dist/tenant/sandboxes.js +551 -0
- package/dist/tenant/session-streams.d.ts +6 -10
- package/dist/tenant/session-streams.js +9 -27
- package/dist/tenant/session.d.ts +2 -13
- package/dist/tenant/session.js +4 -28
- package/dist/tenant/sessions.d.ts +6 -1
- package/dist/tenant/sessions.js +29 -2
- package/dist/tenant/signing-keys.d.ts +2 -1
- package/dist/tenant/signing-keys.js +2 -2
- package/dist/tenant/status.d.ts +8 -3
- package/dist/tenant/status.js +5 -0
- package/dist/tenant/streams.d.ts +8 -2
- package/dist/tenant/streams.js +25 -6
- package/dist/tenant/sweep.js +37 -4
- package/dist/tenant/types.d.ts +39 -1
- package/dist/tenant/worker.d.ts +11 -5
- package/dist/tenant/worker.js +12 -6
- package/dist/vault/crypto.d.ts +7 -0
- package/dist/vault/crypto.js +16 -0
- package/dist/vault/oauth.d.ts +55 -0
- package/dist/vault/oauth.js +186 -0
- package/dist/vault/service.d.ts +54 -7
- package/dist/vault/service.js +261 -17
- package/dist/vault/sources.d.ts +54 -0
- package/dist/vault/sources.js +185 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -10
- package/dist/adapters/media.d.ts +0 -24
- package/dist/adapters/media.js +0 -111
- package/dist/host/cors.d.ts +0 -27
- package/dist/host/cors.js +0 -64
- package/dist/tenant/access-policy.d.ts +0 -30
- package/dist/tenant/access-policy.js +0 -52
- package/dist/tenant/browser.d.ts +0 -15
- package/dist/tenant/browser.js +0 -39
- package/dist/tenant/subject-limits.d.ts +0 -16
- package/dist/tenant/subject-limits.js +0 -45
- package/dist/tenant/tokens.d.ts +0 -14
- package/dist/tenant/tokens.js +0 -180
package/dist/vault/service.js
CHANGED
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
* // Administration: each opens its own transaction and writes its audit rows
|
|
13
13
|
* // in that transaction. Creates and rotations replay by idempotency key.
|
|
14
14
|
* createVault(body: CreateVaultRequest): Promise<VaultInfo>
|
|
15
|
-
* listVaults(ownerUserId: string): Promise<VaultInfo[]>
|
|
15
|
+
* listVaults(ownerUserId: string | undefined, options?: { installation?: boolean }): Promise<VaultInfo[]>
|
|
16
16
|
* getVault(id: string): Promise<VaultInfo>
|
|
17
|
-
* assertOwner(id: string, ownerUserId: string): Promise<void>
|
|
18
17
|
* deleteVault(id: string): Promise<{ id: string }>
|
|
19
18
|
* createCredential(vaultId: string, body: CreateCredentialRequest): Promise<CredentialInfo>
|
|
20
19
|
* listCredentials(vaultId: string): Promise<CredentialInfo[]>
|
|
@@ -26,9 +25,14 @@
|
|
|
26
25
|
* assertAttachment(t: Tx, ownerUserId: string, vaultIds: readonly string[], selections: readonly CredentialSelection[], options?: { opaque?: boolean }): Promise<void>
|
|
27
26
|
* recordAttachment(t: Tx, sessionId: string, vaultIds: readonly string[]): Promise<void>
|
|
28
27
|
*
|
|
29
|
-
* // Use: opens its own transactions; never call it inside one.
|
|
28
|
+
* // Use: opens its own transactions; never call it inside one. `CredentialSources`
|
|
29
|
+
* // (`vault/sources.ts`) wraps it with the operator's credential resolver.
|
|
30
30
|
* authorize(input: { sessionId; vaultIds; credentialSelections; url; serverName? }): Promise<AuthorizeResult>
|
|
31
31
|
*
|
|
32
|
+
* // MCP OAuth connect (F9 C2, installation vaults only): network calls outside any transaction.
|
|
33
|
+
* startOAuth(input: { vaultId; server; url; clientId?; redirectUri }): Promise<{ authorizeUrl; expiresAt }>
|
|
34
|
+
* finishOAuth(input: { state; code?; error? }): Promise<{ vaultId; credentialId }>
|
|
35
|
+
*
|
|
32
36
|
* // Host model credential: each opens its own transaction.
|
|
33
37
|
* getHostModel(): Promise<HostModelView>
|
|
34
38
|
* listHostProviders(): Promise<{ providers: HostModelProviderInfo[] }>
|
|
@@ -40,6 +44,13 @@
|
|
|
40
44
|
* (`vault/host-model.ts`), which only the Model Gate uses: this service seals the host model's
|
|
41
45
|
* credential but never reads it back.
|
|
42
46
|
*
|
|
47
|
+
* ## Scopes
|
|
48
|
+
*
|
|
49
|
+
* - `user`: one person's vault (owner `ownerUserId`), attachable only to that person's sessions.
|
|
50
|
+
* - `installation` (`INSTALLATION_OWNER`): the installation's own, created by application keys
|
|
51
|
+
* only, attachable to any session. Subject and token callers never see one.
|
|
52
|
+
* - `host`: the model vault (`HOST_VAULT_ID`), never listed or attached.
|
|
53
|
+
*
|
|
43
54
|
* ## Transactions and I/O
|
|
44
55
|
*
|
|
45
56
|
* - No network I/O runs inside a transaction. The KEK getter may touch the
|
|
@@ -59,14 +70,19 @@
|
|
|
59
70
|
* has an audit row. Unreadable ciphertext and refresh failures are audited
|
|
60
71
|
* in their own transaction before the refusal is returned.
|
|
61
72
|
*/
|
|
62
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
73
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
74
|
+
import { INSTALLATION_OWNER } from "@nylorun/core/contracts";
|
|
63
75
|
import { canonical } from "../store/canonical.js";
|
|
64
|
-
import { decryptSecret, encryptSecret, VaultCryptoError, } from "./crypto.js";
|
|
76
|
+
import { decryptSecret, encryptSecret, kekId as kekIdOf, openBytes, sealBytes, VaultCryptoError, } from "./crypto.js";
|
|
77
|
+
import { HttpError } from "../tenant/http.js";
|
|
78
|
+
import { authorizationUrl, discoverOAuthServer, exchangeCode, oauthClient, } from "./oauth.js";
|
|
65
79
|
import { VaultError } from "./error.js";
|
|
66
80
|
import { hostModelCatalog } from "../model/catalog.js";
|
|
67
81
|
import { normalizeVaultUrl } from "./url.js";
|
|
68
82
|
const REFRESH_SKEW_MS = 60_000;
|
|
69
83
|
const REFRESH_TIMEOUT_MS = 30_000;
|
|
84
|
+
/** How long an MCP OAuth connect may take between its start and its callback (F9 C2). */
|
|
85
|
+
export const OAUTH_PENDING_MS = 10 * 60_000;
|
|
70
86
|
export const HOST_VAULT_ID = "host";
|
|
71
87
|
const HOST_MODEL_ID = "host-model";
|
|
72
88
|
export class VaultService {
|
|
@@ -85,13 +101,17 @@ export class VaultService {
|
|
|
85
101
|
// --- administration --------------------------------------------------------
|
|
86
102
|
async createVault(body) {
|
|
87
103
|
return this.store.tx((t) => this.replay(t, `create-vault:${body.idempotencyKey}`, body, async () => {
|
|
104
|
+
const installation = body.scope === "installation";
|
|
105
|
+
const ownerUserId = installation ? INSTALLATION_OWNER : body.ownerUserId;
|
|
106
|
+
if (!ownerUserId)
|
|
107
|
+
throw new VaultError(400, "ownerUserId is required for a user vault");
|
|
88
108
|
const row = {
|
|
89
109
|
id: randomUUID(),
|
|
90
110
|
name: body.name,
|
|
91
|
-
ownerUserId
|
|
111
|
+
ownerUserId,
|
|
92
112
|
metadataJson: body.metadata ? JSON.stringify(body.metadata) : null,
|
|
93
113
|
createdAt: new Date().toISOString(),
|
|
94
|
-
scope: "user",
|
|
114
|
+
scope: installation ? "installation" : "user",
|
|
95
115
|
};
|
|
96
116
|
await t.insertVault(row);
|
|
97
117
|
await this.audit(t, {
|
|
@@ -103,20 +123,20 @@ export class VaultService {
|
|
|
103
123
|
return vaultInfoOf(row);
|
|
104
124
|
}));
|
|
105
125
|
}
|
|
106
|
-
|
|
107
|
-
|
|
126
|
+
/**
|
|
127
|
+
* One person's vaults, then (with `installation`) the installation vaults. No owner lists
|
|
128
|
+
* the installation vaults only.
|
|
129
|
+
*/
|
|
130
|
+
async listVaults(ownerUserId, options = {}) {
|
|
131
|
+
return this.store.tx(async (t) => {
|
|
132
|
+
const own = ownerUserId === undefined ? [] : await t.vaultsByOwner(ownerUserId);
|
|
133
|
+
const shared = options.installation ? await t.installationVaults() : [];
|
|
134
|
+
return [...own, ...shared].map(vaultInfoOf);
|
|
135
|
+
});
|
|
108
136
|
}
|
|
109
137
|
async getVault(id) {
|
|
110
138
|
return this.store.tx((t) => this.vaultInfo(t, id));
|
|
111
139
|
}
|
|
112
|
-
/** Another owner's vault, or the host vault, is the same 404 as a missing one. */
|
|
113
|
-
async assertOwner(id, ownerUserId) {
|
|
114
|
-
await this.store.tx(async (t) => {
|
|
115
|
-
const row = await t.getVault(id);
|
|
116
|
-
if (!row || row.scope !== "user" || row.ownerUserId !== ownerUserId)
|
|
117
|
-
throw new VaultError(404, "Vault not found");
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
140
|
async deleteVault(id) {
|
|
121
141
|
return this.store.tx(async (t) => {
|
|
122
142
|
await this.vaultInfo(t, id);
|
|
@@ -238,6 +258,9 @@ export class VaultService {
|
|
|
238
258
|
throw new VaultError(404, "Vault not found");
|
|
239
259
|
if (vault.scope === "host")
|
|
240
260
|
throw new VaultError(400, "Host vault cannot be attached to a session");
|
|
261
|
+
// Any session may use the installation's own vaults (F9-D13).
|
|
262
|
+
if (vault.scope === "installation")
|
|
263
|
+
continue;
|
|
241
264
|
if (vault.ownerUserId !== ownerUserId)
|
|
242
265
|
throw options.opaque
|
|
243
266
|
? new VaultError(404, "Vault not found")
|
|
@@ -371,6 +394,193 @@ export class VaultService {
|
|
|
371
394
|
headers: { authorization: `Bearer ${token}` },
|
|
372
395
|
};
|
|
373
396
|
}
|
|
397
|
+
// --- MCP OAuth connect (F9 C2) ---------------------------------------------
|
|
398
|
+
/**
|
|
399
|
+
* Starts signing the installation in to the MCP server at `url`: discovery, a client (the
|
|
400
|
+
* given id, or one registered now), an S256 PKCE verifier and a `state`, kept for ten minutes
|
|
401
|
+
* in a pending row (verifier and secret sealed, state hashed). Returns where to send the
|
|
402
|
+
* browser. Installation vaults only.
|
|
403
|
+
*/
|
|
404
|
+
async startOAuth(input) {
|
|
405
|
+
const url = normalizeVaultUrl(input.url);
|
|
406
|
+
const redirectUri = callbackUrl(input.redirectUri);
|
|
407
|
+
await this.store.tx(async (t) => {
|
|
408
|
+
await this.installationVault(t, input.vaultId);
|
|
409
|
+
await t.deleteExpiredOAuthPending(new Date().toISOString());
|
|
410
|
+
});
|
|
411
|
+
const kek = this.kek();
|
|
412
|
+
const server = await discoverOAuthServer(url, this.fetchImpl);
|
|
413
|
+
const client = await oauthClient(server, {
|
|
414
|
+
...(input.clientId === undefined ? {} : { clientId: input.clientId }),
|
|
415
|
+
redirectUri,
|
|
416
|
+
fetchFn: this.fetchImpl,
|
|
417
|
+
});
|
|
418
|
+
const state = randomBytes(32).toString("base64url");
|
|
419
|
+
const { authorizeUrl, codeVerifier } = await authorizationUrl(server, client, { redirectUri, state });
|
|
420
|
+
const stateHash = hashState(state);
|
|
421
|
+
const now = Date.now();
|
|
422
|
+
const expiresAt = new Date(now + OAUTH_PENDING_MS).toISOString();
|
|
423
|
+
const row = {
|
|
424
|
+
stateHash,
|
|
425
|
+
vaultId: input.vaultId,
|
|
426
|
+
server: input.server,
|
|
427
|
+
url,
|
|
428
|
+
tokenEndpoint: server.metadata.token_endpoint,
|
|
429
|
+
clientId: client.clientId,
|
|
430
|
+
tokenEndpointAuth: client.tokenEndpointAuth,
|
|
431
|
+
resource: server.resource ?? null,
|
|
432
|
+
kekId: "",
|
|
433
|
+
clientSecret: null,
|
|
434
|
+
codeVerifier: new Uint8Array(),
|
|
435
|
+
redirectUri,
|
|
436
|
+
expiresAt,
|
|
437
|
+
createdAt: new Date(now).toISOString(),
|
|
438
|
+
};
|
|
439
|
+
const verifier = sealBytes(kek, pendingAad(row, "codeVerifier"), Buffer.from(codeVerifier, "utf8"));
|
|
440
|
+
const secret = client.clientSecret
|
|
441
|
+
? sealBytes(kek, pendingAad(row, "clientSecret"), Buffer.from(client.clientSecret, "utf8"))
|
|
442
|
+
: null;
|
|
443
|
+
await this.store.tx(async (t) => {
|
|
444
|
+
await this.installationVault(t, input.vaultId);
|
|
445
|
+
await t.insertOAuthPending({
|
|
446
|
+
...row,
|
|
447
|
+
kekId: kekIdOf(kek),
|
|
448
|
+
codeVerifier: verifier,
|
|
449
|
+
clientSecret: secret,
|
|
450
|
+
});
|
|
451
|
+
await this.audit(t, {
|
|
452
|
+
actor: "application",
|
|
453
|
+
action: "oauth_start",
|
|
454
|
+
vaultId: input.vaultId,
|
|
455
|
+
target: url,
|
|
456
|
+
outcome: "started",
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
return { authorizeUrl, expiresAt };
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Finishes a connect from its callback: takes the pending row of `state` (deleted whatever
|
|
463
|
+
* happens next), exchanges the code, and seals an `oauth` credential bound to the server's
|
|
464
|
+
* URL in the installation vault, rotating the vault's OAuth credential for that URL when it
|
|
465
|
+
* has one.
|
|
466
|
+
*/
|
|
467
|
+
async finishOAuth(input) {
|
|
468
|
+
const row = await this.store.tx((t) => t.takeOAuthPending(hashState(input.state)));
|
|
469
|
+
if (!row || Date.parse(row.expiresAt) <= Date.now())
|
|
470
|
+
throw new HttpError(400, "This sign-in is unknown, already used or expired: start the connect again", { code: "oauth_state_invalid" });
|
|
471
|
+
const failed = async (error) => {
|
|
472
|
+
await this.store.tx((t) => this.audit(t, {
|
|
473
|
+
actor: "application",
|
|
474
|
+
action: "oauth_connect",
|
|
475
|
+
vaultId: row.vaultId,
|
|
476
|
+
target: row.url,
|
|
477
|
+
outcome: "failed",
|
|
478
|
+
}));
|
|
479
|
+
throw error;
|
|
480
|
+
};
|
|
481
|
+
if (input.error !== undefined || !input.code)
|
|
482
|
+
return failed(new HttpError(400, `The authorization server did not grant access (${oauthErrorName(input.error)})`, { code: "oauth_failed" }));
|
|
483
|
+
const kek = this.kek();
|
|
484
|
+
let client;
|
|
485
|
+
let codeVerifier;
|
|
486
|
+
try {
|
|
487
|
+
codeVerifier = openSealed(kek, row, "codeVerifier", row.codeVerifier);
|
|
488
|
+
client = {
|
|
489
|
+
clientId: row.clientId,
|
|
490
|
+
tokenEndpointAuth: row.tokenEndpointAuth,
|
|
491
|
+
...(row.clientSecret ? { clientSecret: openSealed(kek, row, "clientSecret", row.clientSecret) } : {}),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
if (!(error instanceof VaultCryptoError))
|
|
496
|
+
throw error;
|
|
497
|
+
return failed(new HttpError(500, "The sign-in could not be read with this vault key", { code: "oauth_failed" }));
|
|
498
|
+
}
|
|
499
|
+
let tokens;
|
|
500
|
+
try {
|
|
501
|
+
tokens = await exchangeCode({
|
|
502
|
+
tokenEndpoint: row.tokenEndpoint,
|
|
503
|
+
client,
|
|
504
|
+
code: input.code,
|
|
505
|
+
codeVerifier,
|
|
506
|
+
redirectUri: row.redirectUri,
|
|
507
|
+
...(row.resource === null ? {} : { resource: row.resource }),
|
|
508
|
+
fetchFn: this.fetchImpl,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
return failed(error instanceof HttpError ? error : new HttpError(502, "The OAuth code exchange failed", { code: "oauth_failed" }));
|
|
513
|
+
}
|
|
514
|
+
if (tokens.token_type.toLowerCase() !== "bearer")
|
|
515
|
+
return failed(new HttpError(502, `The authorization server issued a ${tokens.token_type} token; MCP needs a bearer token`, {
|
|
516
|
+
code: "oauth_failed",
|
|
517
|
+
}));
|
|
518
|
+
const payload = {
|
|
519
|
+
accessToken: tokens.access_token,
|
|
520
|
+
...(tokens.refresh_token
|
|
521
|
+
? {
|
|
522
|
+
refreshToken: tokens.refresh_token,
|
|
523
|
+
tokenEndpoint: row.tokenEndpoint,
|
|
524
|
+
clientId: client.clientId,
|
|
525
|
+
tokenEndpointAuth: client.tokenEndpointAuth,
|
|
526
|
+
...(client.clientSecret ? { clientSecret: client.clientSecret } : {}),
|
|
527
|
+
}
|
|
528
|
+
: {}),
|
|
529
|
+
};
|
|
530
|
+
const now = new Date();
|
|
531
|
+
const expiresAt = typeof tokens.expires_in === "number" && Number.isFinite(tokens.expires_in)
|
|
532
|
+
? new Date(now.getTime() + tokens.expires_in * 1000).toISOString()
|
|
533
|
+
: null;
|
|
534
|
+
return this.store.tx(async (t) => {
|
|
535
|
+
await this.installationVault(t, row.vaultId);
|
|
536
|
+
const existing = (await t.credentialsForVault(row.vaultId, { type: "oauth" })).find((item) => bindingUrl(item) === row.url);
|
|
537
|
+
if (existing) {
|
|
538
|
+
await this.writePayload(t, kek, existing, payload, expiresAt, now.toISOString());
|
|
539
|
+
await this.audit(t, {
|
|
540
|
+
actor: "application",
|
|
541
|
+
action: "oauth_connect",
|
|
542
|
+
vaultId: row.vaultId,
|
|
543
|
+
credentialId: existing.id,
|
|
544
|
+
target: row.url,
|
|
545
|
+
outcome: "rotated",
|
|
546
|
+
});
|
|
547
|
+
return { vaultId: row.vaultId, credentialId: existing.id };
|
|
548
|
+
}
|
|
549
|
+
const id = randomUUID();
|
|
550
|
+
const sealed = encryptSecret(kek, credentialAad(row.vaultId, id, "oauth", row.url), Buffer.from(JSON.stringify(payload), "utf8"));
|
|
551
|
+
await t.insertCredential({
|
|
552
|
+
id,
|
|
553
|
+
vaultId: row.vaultId,
|
|
554
|
+
name: row.server,
|
|
555
|
+
type: "oauth",
|
|
556
|
+
bindingJson: JSON.stringify({ url: row.url }),
|
|
557
|
+
expiresAt,
|
|
558
|
+
createdAt: now.toISOString(),
|
|
559
|
+
rotatedAt: null,
|
|
560
|
+
...sealed,
|
|
561
|
+
});
|
|
562
|
+
await this.audit(t, {
|
|
563
|
+
actor: "application",
|
|
564
|
+
action: "oauth_connect",
|
|
565
|
+
vaultId: row.vaultId,
|
|
566
|
+
credentialId: id,
|
|
567
|
+
target: row.url,
|
|
568
|
+
outcome: "created",
|
|
569
|
+
});
|
|
570
|
+
return { vaultId: row.vaultId, credentialId: id };
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
/** An installation vault, else the 404 of a missing one (a user vault is a 400). */
|
|
574
|
+
async installationVault(t, id) {
|
|
575
|
+
const row = await t.getVault(id);
|
|
576
|
+
if (!row || row.scope === "host")
|
|
577
|
+
throw new VaultError(404, "Vault not found");
|
|
578
|
+
if (row.scope !== "installation")
|
|
579
|
+
throw new HttpError(400, "MCP OAuth connect stores its credential in an installation vault only", {
|
|
580
|
+
code: "request_rejected",
|
|
581
|
+
});
|
|
582
|
+
return row;
|
|
583
|
+
}
|
|
374
584
|
// --- host model ----------------------------------------------------------------
|
|
375
585
|
async getHostModel() {
|
|
376
586
|
return this.store.tx((t) => this.hostModelView(t));
|
|
@@ -727,6 +937,40 @@ export class VaultService {
|
|
|
727
937
|
});
|
|
728
938
|
}
|
|
729
939
|
}
|
|
940
|
+
function hashState(state) {
|
|
941
|
+
return createHash("sha256").update(state, "utf8").digest("hex");
|
|
942
|
+
}
|
|
943
|
+
function pendingAad(row, field) {
|
|
944
|
+
return Buffer.from(canonical({ table: "oauth_pending", stateHash: row.stateHash, vaultId: row.vaultId, field }), "utf8");
|
|
945
|
+
}
|
|
946
|
+
function openSealed(kek, row, field, sealed) {
|
|
947
|
+
const plaintext = openBytes(kek, pendingAad(row, field), Buffer.from(sealed), row.kekId);
|
|
948
|
+
try {
|
|
949
|
+
return plaintext.toString("utf8");
|
|
950
|
+
}
|
|
951
|
+
finally {
|
|
952
|
+
plaintext.fill(0);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
/** The callback URL the authorization server sends the browser back to. */
|
|
956
|
+
function callbackUrl(value) {
|
|
957
|
+
let url;
|
|
958
|
+
try {
|
|
959
|
+
url = new URL(value);
|
|
960
|
+
}
|
|
961
|
+
catch {
|
|
962
|
+
throw new VaultError(400, "The OAuth redirect URI is invalid");
|
|
963
|
+
}
|
|
964
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
965
|
+
throw new VaultError(400, "The OAuth redirect URI must be http or https");
|
|
966
|
+
return url.href;
|
|
967
|
+
}
|
|
968
|
+
/** An authorization server's `error`, safe to show: an OAuth error code, or a stand-in. */
|
|
969
|
+
function oauthErrorName(error) {
|
|
970
|
+
if (error === undefined)
|
|
971
|
+
return "no code";
|
|
972
|
+
return /^[A-Za-z0-9_.-]{1,64}$/.test(error) ? error : "error";
|
|
973
|
+
}
|
|
730
974
|
function isUserCredential(row) {
|
|
731
975
|
return row.type !== "model";
|
|
732
976
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CredentialSelection } from "@nylorun/core/contracts";
|
|
2
|
+
import type { AuthorizeResult, VaultService } from "./service.js";
|
|
3
|
+
/** The operator's credential resolver (`NYLORUN_RESOLVER_URL`, `NYLORUN_RESOLVER_TOKEN`). */
|
|
4
|
+
export interface ResolverConfig {
|
|
5
|
+
readonly url: string;
|
|
6
|
+
readonly token: string;
|
|
7
|
+
}
|
|
8
|
+
/** What the resolver is asked about: the session's row, as stored. */
|
|
9
|
+
export interface CredentialSession {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly ownerUserId: string;
|
|
12
|
+
readonly activeTurnId: string | null;
|
|
13
|
+
readonly vaultIds?: readonly string[];
|
|
14
|
+
readonly credentialSelections?: readonly CredentialSelection[];
|
|
15
|
+
}
|
|
16
|
+
/** One MCP request: the declared server's URL, its name and the agent that declares it. */
|
|
17
|
+
export interface McpCredentialRequest {
|
|
18
|
+
readonly url: string;
|
|
19
|
+
readonly serverName?: string;
|
|
20
|
+
/** The agent used as a tool that declares the server; absent for the session's root agent. */
|
|
21
|
+
readonly agentId?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface CredentialSourcesOptions {
|
|
24
|
+
readonly vault: Pick<VaultService, "authorize">;
|
|
25
|
+
/** Absent: the vaults are the only source, as before F9. */
|
|
26
|
+
readonly resolver?: ResolverConfig;
|
|
27
|
+
/** Default `globalThis.fetch`. */
|
|
28
|
+
readonly fetch?: typeof fetch;
|
|
29
|
+
/** How long the resolver may take. Default 5 s. */
|
|
30
|
+
readonly timeoutMs?: number;
|
|
31
|
+
readonly now?: () => number;
|
|
32
|
+
}
|
|
33
|
+
export declare const RESOLVER_TIMEOUT_MS = 5000;
|
|
34
|
+
/** The longest an answer is kept, whatever its `expiresAt`. */
|
|
35
|
+
export declare const RESOLVER_MAX_CACHE_MS: number;
|
|
36
|
+
/** How long an answer without `expiresAt` (and a 404) is kept. */
|
|
37
|
+
export declare const RESOLVER_DEFAULT_CACHE_MS = 60000;
|
|
38
|
+
/** The refusal reason when the resolver could not answer. */
|
|
39
|
+
export declare const CREDENTIAL_UNAVAILABLE = "credential_unavailable";
|
|
40
|
+
export declare class CredentialSources {
|
|
41
|
+
private readonly vault;
|
|
42
|
+
private readonly resolver;
|
|
43
|
+
private readonly fetchImpl;
|
|
44
|
+
private readonly timeoutMs;
|
|
45
|
+
private readonly now;
|
|
46
|
+
private readonly cache;
|
|
47
|
+
private readonly pending;
|
|
48
|
+
constructor(options: CredentialSourcesOptions);
|
|
49
|
+
/** The credential of one MCP request made for `session`. */
|
|
50
|
+
authorize(session: CredentialSession, request: McpCredentialRequest): Promise<AuthorizeResult>;
|
|
51
|
+
private answer;
|
|
52
|
+
private until;
|
|
53
|
+
private ask;
|
|
54
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a session's MCP credential comes from (F9 C1): the session's attached vaults first,
|
|
3
|
+
* then the operator's credential resolver, an HTTP service on the operator's own network that
|
|
4
|
+
* holds people's own credentials (OSS never stores them, F9-D7).
|
|
5
|
+
*
|
|
6
|
+
* Both authorize sites call this: the gateway (`gates/tenant-vaults.ts`, remote MCP servers in
|
|
7
|
+
* the gates service) and the Tenant (`tenant/effects.ts`, the in-process MCP pool).
|
|
8
|
+
*
|
|
9
|
+
* ## The resolver contract
|
|
10
|
+
*
|
|
11
|
+
* Asked only when the attached vaults hold no credential for the URL (the vault's
|
|
12
|
+
* `unauthenticated`), and only for a person's session (never a reserved owner):
|
|
13
|
+
*
|
|
14
|
+
* ```text
|
|
15
|
+
* POST <resolver url>
|
|
16
|
+
* Authorization: Bearer <resolver token>
|
|
17
|
+
* { "owner": "u:priya", "session": "s_…", "turn": "t_…" | null,
|
|
18
|
+
* "target": { "kind": "mcp", "server": "github", "agent": "support", "url": "https://…/mcp" } }
|
|
19
|
+
*
|
|
20
|
+
* 200 { "headers": { "authorization": "Bearer …" }, "expiresAt"?: "<ISO time>" } → authorized
|
|
21
|
+
* 404 → unauthenticated (the call goes without one)
|
|
22
|
+
* anything else, a bad body, or no answer in 5 s → refused: credential_unavailable
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* `owner` and `turn` come from the session row, never from the harness. Because authorize runs
|
|
26
|
+
* on every MCP HTTP request, answers (200 and 404) are cached per (owner, url): until
|
|
27
|
+
* `expiresAt`, at most 5 minutes, and 60 s without one. Concurrent misses for one key share one
|
|
28
|
+
* request. Failures are not cached. The resolver's URL is the operator's, so private addresses
|
|
29
|
+
* are allowed; redirects are refused.
|
|
30
|
+
*/
|
|
31
|
+
import { isSubject } from "@nylorun/core/contracts";
|
|
32
|
+
export const RESOLVER_TIMEOUT_MS = 5_000;
|
|
33
|
+
/** The longest an answer is kept, whatever its `expiresAt`. */
|
|
34
|
+
export const RESOLVER_MAX_CACHE_MS = 5 * 60_000;
|
|
35
|
+
/** How long an answer without `expiresAt` (and a 404) is kept. */
|
|
36
|
+
export const RESOLVER_DEFAULT_CACHE_MS = 60_000;
|
|
37
|
+
/** The refusal reason when the resolver could not answer. */
|
|
38
|
+
export const CREDENTIAL_UNAVAILABLE = "credential_unavailable";
|
|
39
|
+
/** RFC 9110 field names. */
|
|
40
|
+
const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
41
|
+
/** Headers the MCP transport owns; a resolver may not set them. */
|
|
42
|
+
const RESERVED_HEADERS = new Set([
|
|
43
|
+
"host",
|
|
44
|
+
"content-length",
|
|
45
|
+
"content-type",
|
|
46
|
+
"transfer-encoding",
|
|
47
|
+
"connection",
|
|
48
|
+
"accept",
|
|
49
|
+
"mcp-session-id",
|
|
50
|
+
"mcp-protocol-version",
|
|
51
|
+
]);
|
|
52
|
+
export class CredentialSources {
|
|
53
|
+
vault;
|
|
54
|
+
resolver;
|
|
55
|
+
fetchImpl;
|
|
56
|
+
timeoutMs;
|
|
57
|
+
now;
|
|
58
|
+
cache = new Map();
|
|
59
|
+
pending = new Map();
|
|
60
|
+
constructor(options) {
|
|
61
|
+
this.vault = options.vault;
|
|
62
|
+
this.resolver = options.resolver;
|
|
63
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
64
|
+
this.timeoutMs = options.timeoutMs ?? RESOLVER_TIMEOUT_MS;
|
|
65
|
+
this.now = options.now ?? Date.now;
|
|
66
|
+
}
|
|
67
|
+
/** The credential of one MCP request made for `session`. */
|
|
68
|
+
async authorize(session, request) {
|
|
69
|
+
const result = await this.vault.authorize({
|
|
70
|
+
sessionId: session.id,
|
|
71
|
+
vaultIds: session.vaultIds ?? [],
|
|
72
|
+
credentialSelections: session.credentialSelections ?? [],
|
|
73
|
+
url: request.url,
|
|
74
|
+
...(request.serverName === undefined ? {} : { serverName: request.serverName }),
|
|
75
|
+
});
|
|
76
|
+
if (result.status !== "unauthenticated" || !this.resolver)
|
|
77
|
+
return result;
|
|
78
|
+
// `host` and `installation` are no one: there is no person to resolve for.
|
|
79
|
+
if (!isSubject(session.ownerUserId))
|
|
80
|
+
return result;
|
|
81
|
+
const answer = await this.answer(session, request, result.url);
|
|
82
|
+
if (answer.kind === "failed")
|
|
83
|
+
return { status: "refused", url: result.url, credentialIds: [], reason: CREDENTIAL_UNAVAILABLE };
|
|
84
|
+
if (answer.kind === "none")
|
|
85
|
+
return result;
|
|
86
|
+
return { status: "authorized", url: result.url, headers: { ...answer.headers } };
|
|
87
|
+
}
|
|
88
|
+
async answer(session, request, url) {
|
|
89
|
+
const key = JSON.stringify([session.ownerUserId, url]);
|
|
90
|
+
const cached = this.cache.get(key);
|
|
91
|
+
if (cached) {
|
|
92
|
+
if (cached.until > this.now())
|
|
93
|
+
return cached.answer;
|
|
94
|
+
this.cache.delete(key);
|
|
95
|
+
}
|
|
96
|
+
let pending = this.pending.get(key);
|
|
97
|
+
if (!pending) {
|
|
98
|
+
pending = this.ask(session, request, url)
|
|
99
|
+
.then((answer) => {
|
|
100
|
+
if (answer.kind !== "failed") {
|
|
101
|
+
const until = this.until(answer);
|
|
102
|
+
if (until > this.now())
|
|
103
|
+
this.cache.set(key, { answer, until });
|
|
104
|
+
}
|
|
105
|
+
return answer;
|
|
106
|
+
})
|
|
107
|
+
.finally(() => this.pending.delete(key));
|
|
108
|
+
this.pending.set(key, pending);
|
|
109
|
+
}
|
|
110
|
+
return pending;
|
|
111
|
+
}
|
|
112
|
+
until(answer) {
|
|
113
|
+
const now = this.now();
|
|
114
|
+
const cap = now + RESOLVER_MAX_CACHE_MS;
|
|
115
|
+
if (answer.kind === "headers" && answer.expiresAt !== undefined)
|
|
116
|
+
return Math.min(answer.expiresAt, cap);
|
|
117
|
+
return now + RESOLVER_DEFAULT_CACHE_MS;
|
|
118
|
+
}
|
|
119
|
+
async ask(session, request, url) {
|
|
120
|
+
const resolver = this.resolver;
|
|
121
|
+
const body = {
|
|
122
|
+
owner: session.ownerUserId,
|
|
123
|
+
session: session.id,
|
|
124
|
+
turn: session.activeTurnId ?? null,
|
|
125
|
+
target: {
|
|
126
|
+
kind: "mcp",
|
|
127
|
+
...(request.serverName ? { server: request.serverName } : {}),
|
|
128
|
+
...(request.agentId ? { agent: request.agentId } : {}),
|
|
129
|
+
url,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
try {
|
|
133
|
+
const response = await this.fetchImpl(resolver.url, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: {
|
|
136
|
+
authorization: `Bearer ${resolver.token}`,
|
|
137
|
+
"content-type": "application/json",
|
|
138
|
+
accept: "application/json",
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify(body),
|
|
141
|
+
redirect: "error",
|
|
142
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
143
|
+
});
|
|
144
|
+
if (response.status === 404) {
|
|
145
|
+
await response.body?.cancel().catch(() => undefined);
|
|
146
|
+
return { kind: "none" };
|
|
147
|
+
}
|
|
148
|
+
if (response.status !== 200) {
|
|
149
|
+
await response.body?.cancel().catch(() => undefined);
|
|
150
|
+
return { kind: "failed" };
|
|
151
|
+
}
|
|
152
|
+
return parseAnswer(await response.json(), this.now());
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return { kind: "failed" };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function parseAnswer(value, now) {
|
|
160
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
161
|
+
return { kind: "failed" };
|
|
162
|
+
const { headers, expiresAt } = value;
|
|
163
|
+
if (!headers || typeof headers !== "object" || Array.isArray(headers))
|
|
164
|
+
return { kind: "failed" };
|
|
165
|
+
const entries = Object.entries(headers);
|
|
166
|
+
if (entries.length === 0)
|
|
167
|
+
return { kind: "failed" };
|
|
168
|
+
const out = {};
|
|
169
|
+
for (const [name, item] of entries) {
|
|
170
|
+
if (!HEADER_NAME.test(name) || RESERVED_HEADERS.has(name.toLowerCase()))
|
|
171
|
+
return { kind: "failed" };
|
|
172
|
+
if (typeof item !== "string" || item === "" || /[\r\n\0]/.test(item))
|
|
173
|
+
return { kind: "failed" };
|
|
174
|
+
out[name.toLowerCase()] = item;
|
|
175
|
+
}
|
|
176
|
+
if (expiresAt === undefined || expiresAt === null)
|
|
177
|
+
return { kind: "headers", headers: out };
|
|
178
|
+
if (typeof expiresAt !== "string")
|
|
179
|
+
return { kind: "failed" };
|
|
180
|
+
const at = Date.parse(expiresAt);
|
|
181
|
+
if (Number.isNaN(at))
|
|
182
|
+
return { kind: "failed" };
|
|
183
|
+
// An answer that has already expired is used once and not kept.
|
|
184
|
+
return { kind: "headers", headers: out, expiresAt: Math.max(at, now) };
|
|
185
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const RUNTIME_VERSION = "0.
|
|
1
|
+
export declare const RUNTIME_VERSION = "0.17.0-beta";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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,16 +59,17 @@
|
|
|
59
59
|
"db:studio": "drizzle-kit studio"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@ag-ui/core": "~1.0.
|
|
62
|
+
"@ag-ui/core": "~1.0.1",
|
|
63
63
|
"@asteasolutions/zod-to-openapi": "^9.1.0",
|
|
64
64
|
"@earendil-works/pi-ai": "0.99.1",
|
|
65
|
-
"@hono/node-server": "^2.1.
|
|
65
|
+
"@hono/node-server": "^2.1.3",
|
|
66
66
|
"@hono/zod-openapi": "^1.6.3",
|
|
67
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
68
|
-
"@nylorun/core": "0.
|
|
69
|
-
"@nylorun/harness": "0.
|
|
70
|
-
"@restatedev/restate-sdk": "1.
|
|
71
|
-
"@s2-dev/streamstore": "0.
|
|
67
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
68
|
+
"@nylorun/core": "0.13.0-beta",
|
|
69
|
+
"@nylorun/harness": "0.22.1-beta",
|
|
70
|
+
"@restatedev/restate-sdk": "1.17.2",
|
|
71
|
+
"@s2-dev/streamstore": "0.28.0",
|
|
72
|
+
"aws4fetch": "1.0.20",
|
|
72
73
|
"drizzle-orm": "^0.45.3",
|
|
73
74
|
"hono": "^4.13.12",
|
|
74
75
|
"jose": "^6.2.12",
|
|
@@ -76,17 +77,20 @@
|
|
|
76
77
|
"pg": "8.23.1",
|
|
77
78
|
"pg-logical-replication": "2.5.0",
|
|
78
79
|
"postgres": "^3.4.9",
|
|
80
|
+
"ws": "^8.22.0",
|
|
81
|
+
"yaml": "^2.9.1",
|
|
79
82
|
"zod": "^4.6.5"
|
|
80
83
|
},
|
|
81
84
|
"devDependencies": {
|
|
82
85
|
"@a2a-js/sdk": "~1.3.0",
|
|
83
|
-
"@ag-ui/client": "1.0.
|
|
86
|
+
"@ag-ui/client": "1.0.1",
|
|
84
87
|
"@scalar/openapi-parser": "^0.29.9",
|
|
85
88
|
"@types/node": "^26.5.0",
|
|
86
89
|
"@types/pg": "^8.23.1",
|
|
90
|
+
"@types/ws": "^8.18.2",
|
|
87
91
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
88
92
|
"drizzle-kit": "^0.31.11",
|
|
89
93
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
90
|
-
"vitest": "^5.0.
|
|
94
|
+
"vitest": "^5.0.2"
|
|
91
95
|
}
|
|
92
96
|
}
|