@nylorun/runtime 0.23.0-beta → 0.25.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 +252 -0
- package/README.md +87 -28
- package/dist/adapters/execution/restate.js +1 -1
- package/dist/adapters/replication/pgoutput.d.ts +2 -0
- package/dist/adapters/replication/pgoutput.js +6 -3
- package/dist/adapters/sandbox/local.d.ts +9 -1
- package/dist/adapters/sandbox/local.js +16 -4
- package/dist/adapters/sandbox/virtual.js +4 -1
- package/dist/api/a2a/card.d.ts +11 -1
- package/dist/api/a2a/card.js +26 -2
- package/dist/api/a2a/endpoint.js +9 -3
- package/dist/api/a2a/protocol.d.ts +25 -2
- package/dist/api/a2a/protocol.js +20 -3
- package/dist/api/a2a/routes.d.ts +6 -1
- package/dist/api/a2a/routes.js +67 -9
- package/dist/api/a2a/tasks.js +2 -1
- package/dist/api/ag-ui/endpoint.js +8 -3
- package/dist/api/ag-ui/history.d.ts +6 -1
- package/dist/api/ag-ui/history.js +39 -3
- package/dist/api/ag-ui/routes.d.ts +4 -1
- package/dist/api/ag-ui/routes.js +63 -20
- package/dist/api/ag-ui/translate.js +3 -2
- package/dist/api/components.d.ts +5327 -1091
- package/dist/api/components.js +40 -16
- package/dist/api/gate/keys.d.ts +2 -1
- package/dist/api/gate/keys.js +14 -1
- package/dist/api/gate/routes.d.ts +6 -0
- package/dist/api/gate/routes.js +52 -3
- package/dist/api/http/app.js +17 -6
- package/dist/api/http/body.d.ts +3 -3
- package/dist/api/http/body.js +7 -7
- package/dist/api/http/define.d.ts +6 -0
- package/dist/api/http/define.js +13 -1
- package/dist/api/http/respond.js +1 -1
- package/dist/api/http/routes/artifacts.js +35 -8
- package/dist/api/http/routes/coverage.d.ts +9 -0
- package/dist/api/http/routes/coverage.js +34 -0
- package/dist/api/http/routes/environment-manifests.d.ts +9 -0
- package/dist/api/http/routes/environment-manifests.js +118 -0
- package/dist/api/http/routes/environments.d.ts +11 -0
- package/dist/api/http/routes/environments.js +332 -0
- package/dist/api/http/routes/mcp.d.ts +1 -1
- package/dist/api/http/routes/mcp.js +2 -2
- package/dist/api/http/routes/me.js +1 -1
- package/dist/api/http/routes/reads.d.ts +94 -9
- package/dist/api/http/routes/reads.js +54 -5
- package/dist/api/http/routes/sessions.js +24 -9
- package/dist/api/http/routes/tenant.js +65 -15
- package/dist/api/inbound-files.d.ts +65 -0
- package/dist/api/inbound-files.js +115 -0
- package/dist/api/openapi.js +48 -28
- package/dist/artifacts/disk.d.ts +59 -0
- package/dist/artifacts/disk.js +569 -0
- package/dist/artifacts/export.d.ts +1 -1
- package/dist/artifacts/export.js +2 -3
- package/dist/artifacts/folders.d.ts +6 -3
- package/dist/artifacts/folders.js +17 -12
- package/dist/artifacts/media-types.d.ts +15 -0
- package/dist/artifacts/media-types.js +56 -4
- package/dist/artifacts/parts.d.ts +12 -3
- package/dist/artifacts/parts.js +15 -4
- package/dist/artifacts/service.d.ts +2 -1
- package/dist/artifacts/service.js +6 -1
- package/dist/artifacts/workspace.d.ts +5 -5
- package/dist/artifacts/workspace.js +7 -19
- package/dist/core/flow-host.d.ts +3 -4
- package/dist/core/flow-host.js +5 -2
- package/dist/core/provider.d.ts +3 -2
- package/dist/core/provider.js +5 -37
- package/dist/core/sandbox-routes.d.ts +2 -21
- package/dist/core/sandbox-routes.js +11 -44
- package/dist/core/turn-manifest.d.ts +3 -2
- package/dist/core/turn-manifest.js +29 -5
- package/dist/execution/types.d.ts +1 -1
- package/dist/gates/contract.d.ts +3 -24
- package/dist/gates/contract.js +1 -36
- package/dist/gates/egress-credentials.d.ts +9 -0
- package/dist/gates/egress-credentials.js +11 -4
- package/dist/gates/egress.d.ts +22 -1
- package/dist/gates/egress.js +116 -10
- package/dist/gates/fixture-model.d.ts +28 -0
- package/dist/gates/fixture-model.js +92 -0
- package/dist/gates/handler.js +4 -4
- package/dist/gates/http-tool.d.ts +9 -6
- package/dist/gates/http-tool.js +11 -17
- package/dist/gates/in-process.d.ts +5 -0
- package/dist/gates/in-process.js +12 -1
- package/dist/gates/journal.d.ts +30 -0
- package/dist/gates/journal.js +51 -0
- package/dist/gates/mcp-handler.d.ts +4 -0
- package/dist/gates/mcp-handler.js +7 -4
- package/dist/gates/meter.d.ts +10 -2
- package/dist/gates/meter.js +20 -5
- package/dist/gates/policy.d.ts +49 -0
- package/dist/gates/policy.js +132 -0
- package/dist/gates/tenant-vaults.d.ts +7 -7
- package/dist/gates/tenant-vaults.js +5 -5
- package/dist/gates/tool-calls.d.ts +10 -5
- package/dist/gates/tool-calls.js +28 -10
- package/dist/gates/tool-client.d.ts +8 -0
- package/dist/gates/tool-client.js +27 -11
- package/dist/gates/tool-contract.d.ts +11 -69
- package/dist/gates/tool-contract.js +1 -85
- package/dist/gates/tool-gate.d.ts +1 -1
- package/dist/harness/calls.d.ts +14 -7
- package/dist/harness/calls.js +30 -24
- package/dist/harness/executors.js +12 -9
- package/dist/harness/main.js +24 -5
- package/dist/harness/pod.d.ts +39 -0
- package/dist/harness/pod.js +46 -0
- package/dist/harness/service.d.ts +0 -1
- package/dist/harness/service.js +17 -9
- package/dist/harness/setup.d.ts +29 -0
- package/dist/harness/setup.js +174 -0
- package/dist/harness/ws-client.js +3 -2
- package/dist/harness/ws-port.d.ts +1 -5
- package/dist/harness/ws-port.js +1 -5
- package/dist/harness-api/in-process.js +3 -3
- package/dist/harness-api/record.d.ts +15 -1
- package/dist/harness-api/record.js +180 -50
- package/dist/harness-api/redact.d.ts +3 -0
- package/dist/harness-api/redact.js +45 -0
- package/dist/harness-api/server.d.ts +35 -4
- package/dist/harness-api/server.js +130 -17
- package/dist/harness-api/start.d.ts +10 -3
- package/dist/harness-api/start.js +40 -10
- package/dist/harness-api/tool-journal.d.ts +105 -0
- package/dist/harness-api/tool-journal.js +132 -0
- package/dist/harness-api/tool-status.d.ts +13 -0
- package/dist/harness-api/tool-status.js +26 -0
- package/dist/harness-api/workspace.d.ts +12 -7
- package/dist/harness-api/workspace.js +47 -25
- package/dist/harness-api/ws-server.d.ts +11 -3
- package/dist/harness-api/ws-server.js +94 -15
- package/dist/host/gates.d.ts +2 -1
- package/dist/host/gates.js +10 -2
- package/dist/host/main.js +54 -5
- package/dist/host/operate.js +67 -5
- package/dist/host/stack-config.d.ts +64 -0
- package/dist/host/stack-config.js +62 -2
- package/dist/keys/client.js +4 -1
- package/dist/keys/keys.d.ts +13 -6
- package/dist/keys/keys.js +9 -8
- package/dist/management-openapi.json +1264 -2171
- package/dist/mcp/connect.d.ts +26 -22
- package/dist/mcp/connect.js +30 -34
- package/dist/mcp/pool.d.ts +3 -6
- package/dist/mcp/pool.js +14 -28
- package/dist/mcp/preview.d.ts +3 -3
- package/dist/mcp/preview.js +10 -21
- package/dist/mcp/snapshot.d.ts +16 -24
- package/dist/mcp/snapshot.js +33 -45
- package/dist/model/capabilities.d.ts +26 -0
- package/dist/model/capabilities.js +44 -0
- package/dist/model/catalog.d.ts +34 -3
- package/dist/model/catalog.js +38 -2
- package/dist/model/models.d.ts +2 -0
- package/dist/model/models.js +1 -1
- package/dist/model/native-blocks.d.ts +35 -0
- package/dist/model/native-blocks.js +170 -0
- package/dist/model/pi-model.js +114 -94
- package/dist/openapi.json +12822 -3943
- package/dist/reads/types.d.ts +16 -3
- package/dist/record/append.d.ts +3 -1
- package/dist/record/append.js +1 -0
- package/dist/record/envelope.d.ts +5 -1
- package/dist/record/envelope.js +7 -3
- package/dist/record/environment.d.ts +25 -0
- package/dist/record/environment.js +44 -0
- package/dist/record/index.d.ts +1 -1
- package/dist/record/index.js +1 -1
- package/dist/sandbox/disk-fs.d.ts +95 -0
- package/dist/sandbox/disk-fs.js +206 -0
- package/dist/sandbox/join.d.ts +17 -18
- package/dist/sandbox/join.js +46 -15
- package/dist/sandbox/manager.d.ts +47 -21
- package/dist/sandbox/manager.js +162 -107
- package/dist/sandbox/placement.d.ts +14 -13
- package/dist/sandbox/placement.js +31 -14
- package/dist/sandbox/pods/client.d.ts +9 -2
- package/dist/sandbox/pods/lifecycle.d.ts +36 -27
- package/dist/sandbox/pods/lifecycle.js +29 -26
- package/dist/sandbox/pods/reconcile.js +18 -12
- package/dist/sandbox/pods/spec.d.ts +15 -13
- package/dist/sandbox/pods/spec.js +17 -20
- package/dist/sandbox/records.d.ts +2 -2
- package/dist/sandbox/records.js +3 -3
- package/dist/sandbox/resolve.d.ts +37 -27
- package/dist/sandbox/resolve.js +73 -41
- package/dist/sandbox/runtime-tools.d.ts +9 -0
- package/dist/sandbox/runtime-tools.js +16 -0
- package/dist/sandbox/session-sandbox.d.ts +4 -6
- package/dist/sandbox/session-sandbox.js +47 -94
- package/dist/sandbox/setup.d.ts +64 -0
- package/dist/sandbox/setup.js +160 -0
- package/dist/sandbox/share.d.ts +21 -26
- package/dist/sandbox/share.js +36 -63
- package/dist/sandbox/skills.d.ts +8 -6
- package/dist/sandbox/skills.js +24 -23
- package/dist/sandbox/tenant-config.d.ts +12 -14
- package/dist/sandbox/tenant-config.js +12 -17
- package/dist/sandbox/types.d.ts +6 -0
- package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
- package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
- package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
- package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
- package/dist/store/postgres/drizzle/0024_flat_manifest.sql +7 -0
- package/dist/store/postgres/drizzle/0025_environment_manifests.sql +14 -0
- package/dist/store/postgres/drizzle/0026_environment_forks.sql +1 -0
- package/dist/store/postgres/drizzle/0027_environment_image_digest.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +56 -0
- package/dist/store/postgres/reads.js +123 -17
- package/dist/store/postgres/record-writer.d.ts +4 -4
- package/dist/store/postgres/record-writer.js +9 -9
- package/dist/store/postgres/record.d.ts +7 -0
- package/dist/store/postgres/record.js +23 -2
- package/dist/store/postgres/schema.d.ts +1126 -736
- package/dist/store/postgres/schema.js +136 -73
- package/dist/store/postgres/store.js +195 -67
- package/dist/store/types.d.ts +165 -50
- package/dist/tenant/advance.d.ts +7 -6
- package/dist/tenant/advance.js +118 -42
- package/dist/tenant/artifact-tool.d.ts +1 -1
- package/dist/tenant/artifact-tool.js +46 -26
- package/dist/tenant/commands.d.ts +15 -2
- package/dist/tenant/commands.js +115 -14
- package/dist/tenant/context.d.ts +56 -24
- package/dist/tenant/context.js +4 -15
- package/dist/tenant/effects.d.ts +1 -1
- package/dist/tenant/effects.js +44 -29
- package/dist/tenant/environment-manifests.d.ts +57 -0
- package/dist/tenant/environment-manifests.js +200 -0
- package/dist/tenant/environments.d.ts +174 -0
- package/dist/tenant/environments.js +1044 -0
- package/dist/tenant/harness-tokens.d.ts +21 -0
- package/dist/tenant/harness-tokens.js +61 -0
- package/dist/tenant/host-token.d.ts +1 -1
- package/dist/tenant/host-token.js +2 -4
- package/dist/tenant/identity-file.d.ts +4 -4
- package/dist/tenant/identity-file.js +17 -8
- package/dist/tenant/issuers.js +3 -3
- package/dist/tenant/model-setting.d.ts +14 -0
- package/dist/tenant/model-setting.js +51 -0
- package/dist/tenant/principals.js +1 -1
- package/dist/tenant/reset.js +3 -3
- package/dist/tenant/runtime.d.ts +6 -1
- package/dist/tenant/runtime.js +7 -1
- package/dist/tenant/session.d.ts +3 -3
- package/dist/tenant/session.js +0 -14
- package/dist/tenant/sessions.d.ts +18 -14
- package/dist/tenant/sessions.js +99 -143
- package/dist/tenant/signing-keys.d.ts +5 -1
- package/dist/tenant/skill-tool.d.ts +3 -3
- package/dist/tenant/skill-tool.js +6 -11
- package/dist/tenant/status.js +3 -3
- package/dist/tenant/sweep.js +13 -7
- package/dist/tenant/tool-results.js +1 -1
- package/dist/tenant/tool-search.d.ts +7 -11
- package/dist/tenant/tool-search.js +8 -5
- package/dist/tenant/transcript.d.ts +245 -36
- package/dist/tenant/transcript.js +64 -12
- package/dist/tenant/turn-timing.d.ts +43 -0
- package/dist/tenant/turn-timing.js +39 -0
- package/dist/tenant/types.d.ts +2 -0
- package/dist/traces/ids.d.ts +41 -0
- package/dist/traces/ids.js +81 -0
- package/dist/traces/key.d.ts +6 -0
- package/dist/traces/key.js +37 -0
- package/dist/traces/map.d.ts +113 -0
- package/dist/traces/map.js +289 -0
- package/dist/traces/options.d.ts +7 -0
- package/dist/traces/options.js +1 -0
- package/dist/traces/otlp.d.ts +42 -0
- package/dist/traces/otlp.js +84 -0
- package/dist/traces/projector.d.ts +58 -0
- package/dist/traces/projector.js +200 -0
- package/dist/traces/read.d.ts +23 -0
- package/dist/traces/read.js +16 -0
- package/dist/vault/coverage.d.ts +26 -0
- package/dist/vault/coverage.js +62 -0
- package/dist/vault/error.d.ts +11 -1
- package/dist/vault/error.js +5 -1
- package/dist/vault/host-model.d.ts +4 -1
- package/dist/vault/host-model.js +10 -2
- package/dist/vault/service.d.ts +92 -48
- package/dist/vault/service.js +462 -376
- package/dist/vault/sources.d.ts +5 -16
- package/dist/vault/sources.js +6 -8
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -2
- package/package.json +13 -13
- package/dist/api/http/routes/sandboxes.d.ts +0 -9
- package/dist/api/http/routes/sandboxes.js +0 -153
- package/dist/api/http/routes/vaults.d.ts +0 -10
- package/dist/api/http/routes/vaults.js +0 -139
- package/dist/record/sandbox.d.ts +0 -25
- package/dist/record/sandbox.js +0 -43
- package/dist/sandbox/capability.d.ts +0 -3
- package/dist/sandbox/capability.js +0 -10
- package/dist/tenant/sandboxes.d.ts +0 -123
- package/dist/tenant/sandboxes.js +0 -566
package/dist/vault/service.js
CHANGED
|
@@ -1,34 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Runtime vault (docs/design/runtime/vault.md,
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* this module holds no SQL.
|
|
2
|
+
* The Runtime vault (docs/design/runtime/vault.md, D56): one vault per Environment, created and
|
|
3
|
+
* deleted with it, holding the Environment's secrets; and the host vault, which holds the model
|
|
4
|
+
* credential. Storage is the async Session Store (`store/types.ts`); this module holds no SQL.
|
|
6
5
|
*
|
|
7
6
|
* ## Public surface
|
|
8
7
|
*
|
|
9
8
|
* ```ts
|
|
10
9
|
* new VaultService({ store: SessionStore, kek: () => Buffer })
|
|
11
10
|
*
|
|
12
|
-
* //
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* listVaults(ownerUserId: string | undefined, options?: { installation?: boolean }): Promise<VaultInfo[]>
|
|
16
|
-
* getVault(id: string): Promise<VaultInfo>
|
|
17
|
-
* deleteVault(id: string): Promise<{ id: string }>
|
|
18
|
-
* createCredential(vaultId: string, body: CreateCredentialRequest): Promise<CredentialInfo>
|
|
19
|
-
* listCredentials(vaultId: string): Promise<CredentialInfo[]>
|
|
20
|
-
* getCredential(vaultId: string, id: string): Promise<CredentialInfo>
|
|
21
|
-
* rotateCredential(vaultId: string, id: string, body: RotateCredentialRequest): Promise<CredentialInfo>
|
|
22
|
-
* deleteCredential(vaultId: string, id: string): Promise<{ id: string }>
|
|
11
|
+
* // An Environment's vault, in the caller's transaction (`tenant/environments.ts`).
|
|
12
|
+
* createEnvironmentVault(t: Tx, environmentId: string, actor: string, id?: string): Promise<string>
|
|
13
|
+
* deleteEnvironmentVault(t: Tx, environmentId: string, actor: string): Promise<void>
|
|
23
14
|
*
|
|
24
|
-
* //
|
|
25
|
-
*
|
|
26
|
-
*
|
|
15
|
+
* // A fork's secrets (Q15): `Keys` seals the copies with the vault key in its own transaction,
|
|
16
|
+
* // before the fork's; the fork's transaction inserts them, so the process that forks never
|
|
17
|
+
* // holds the key.
|
|
18
|
+
* sealEnvironmentSecretCopies(from, vaultId, slots | null): Promise<SealedSecretCopies>
|
|
19
|
+
* insertEnvironmentSecretCopies(t: Tx, vaultId, from, actor, sealed): Promise<{ copied; dropped }>
|
|
20
|
+
*
|
|
21
|
+
* // Its secrets (`/v1/environments/{id}/secrets`): each opens its own transaction and writes its
|
|
22
|
+
* // audit rows in it. A put needs the vault key, so callers reach it through `Keys`.
|
|
23
|
+
* putEnvironmentSecret(environmentId, name, body: PutEnvironmentSecretRequest, actor): Promise<EnvironmentSecretView>
|
|
24
|
+
* listEnvironmentSecrets(environmentId): Promise<EnvironmentSecretView[]>
|
|
25
|
+
* getEnvironmentSecret(environmentId, name): Promise<EnvironmentSecretView>
|
|
26
|
+
* deleteEnvironmentSecret(environmentId, name, actor): Promise<{ id: string }>
|
|
27
|
+
*
|
|
28
|
+
* // The shell's variables of a session's sandbox, in the caller's transaction (R2c).
|
|
29
|
+
* environmentVariables(t: Tx, environmentId: string | undefined): Promise<Record<string, string>>
|
|
27
30
|
*
|
|
28
31
|
* // Use: opens its own transactions; never call it inside one. `sessionCredentials`
|
|
29
32
|
* // (`vault/sources.ts`) calls it for a session's MCP servers and HTTP tools; a tool preview
|
|
30
|
-
* // (R2b C12, `mcp/preview.ts`) for no session.
|
|
31
|
-
* authorize(input: { sessionId?;
|
|
33
|
+
* // (R2b C12, `mcp/preview.ts`) for no session; egress-gate releases shell secrets.
|
|
34
|
+
* authorize(input: { sessionId?; environmentId?; url }): Promise<AuthorizeResult>
|
|
35
|
+
* releaseEnvironmentSecret(input: { environmentId; host }): Promise<…>
|
|
36
|
+
*
|
|
37
|
+
* // Coverage: what `authorize` would decide for each URL a manifest names, in one transaction,
|
|
38
|
+
* // with no secret read and no audit row.
|
|
39
|
+
* coverage(manifest: AgentManifest | WorkflowManifest, request: CredentialCoverageRequest): Promise<CredentialCoverage>
|
|
32
40
|
*
|
|
33
41
|
* // Host model credential: each opens its own transaction.
|
|
34
42
|
* getHostModel(): Promise<HostModelView>
|
|
@@ -41,12 +49,12 @@
|
|
|
41
49
|
* (`vault/host-model.ts`), which only the Model Gate uses: this service seals the host model's
|
|
42
50
|
* credential but never reads it back.
|
|
43
51
|
*
|
|
44
|
-
* ##
|
|
52
|
+
* ## One credential per host and per URL
|
|
45
53
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
54
|
+
* A secret write is refused (`409 credential_conflict`) when another `environment_secret` of the
|
|
55
|
+
* vault is bound to one of its hosts, or another `bearer` or `headers` credential to its URL, so
|
|
56
|
+
* a release or a tool call never has two to choose from. Names are unique per vault: for the
|
|
57
|
+
* shell kinds the name is the variable, so no variable is set twice.
|
|
50
58
|
*
|
|
51
59
|
* ## Transactions and I/O
|
|
52
60
|
*
|
|
@@ -54,32 +62,31 @@
|
|
|
54
62
|
* filesystem (it creates the key file on first use), so methods that
|
|
55
63
|
* encrypt or decrypt resolve the KEK before opening their transaction and
|
|
56
64
|
* do only in-memory crypto inside it.
|
|
57
|
-
* - `authorize` reads the
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* that write fails, `authorize` rejects and no header leaves the vault, so
|
|
64
|
-
* every approved use has an audit row. Unreadable ciphertext is audited in
|
|
65
|
-
* its own transaction before the refusal is returned.
|
|
65
|
+
* - `authorize` reads the Environment's vault and the credential bound to the URL (ciphertext
|
|
66
|
+
* only) in one transaction. Decryption happens after it commits, so the plaintext never
|
|
67
|
+
* enters a transaction. The `use`/`approved` audit row is written in a final transaction
|
|
68
|
+
* before the header is returned: if that write fails, `authorize` rejects and no header
|
|
69
|
+
* leaves the vault, so every approved use has an audit row. Unreadable ciphertext is audited
|
|
70
|
+
* in its own transaction before the refusal is returned.
|
|
66
71
|
*
|
|
67
72
|
* ## Credentials (R2b C2)
|
|
68
73
|
*
|
|
69
|
-
* A credential is bound to the URL a manifest names and sends a `bearer` token or a
|
|
70
|
-
* map, both sealed. Its `via` (where requests go, such as a gateway) and `identity`
|
|
71
|
-
* that names the session owner) are not secret: they live unsealed in the binding,
|
|
72
|
-
* URL and the header names
|
|
73
|
-
*
|
|
74
|
-
* credential is chosen.
|
|
74
|
+
* A tool credential is bound to the URL a manifest names and sends a `bearer` token or a
|
|
75
|
+
* `headers` map, both sealed. Its `via` (where requests go, such as a gateway) and `identity`
|
|
76
|
+
* (the header that names the session owner) are not secret: they live unsealed in the binding,
|
|
77
|
+
* beside the URL and the header names. The seal's AAD binds the vault, credential id, type and
|
|
78
|
+
* URL only, and matching is by URL only, so `via` never decides which credential is chosen.
|
|
75
79
|
*/
|
|
76
80
|
import { createHash, randomUUID } from "node:crypto";
|
|
77
|
-
import { ENVIRONMENT_SECRET_DEFAULT_INJECT, ENVIRONMENT_SECRET_SENTINEL,
|
|
81
|
+
import { ENVIRONMENT_SECRET_DEFAULT_INJECT, ENVIRONMENT_SECRET_SENTINEL, environmentSecretNameError, renderInjectFormat, } from "@nylorun/core/contracts";
|
|
78
82
|
import { canonical } from "../store/canonical.js";
|
|
79
83
|
import { decryptSecret, encryptSecret, VaultCryptoError } from "./crypto.js";
|
|
80
84
|
import { VaultError } from "./error.js";
|
|
85
|
+
import { credentialNeeds } from "./coverage.js";
|
|
81
86
|
import { checkCredentialHeaders } from "./headers.js";
|
|
82
|
-
import { hostModelCatalog } from "../model/catalog.js";
|
|
87
|
+
import { hostModelCatalog, inputsOfHostModel } from "../model/catalog.js";
|
|
88
|
+
import { inputsError } from "../model/capabilities.js";
|
|
89
|
+
import { readAddedInputs } from "../tenant/model-setting.js";
|
|
83
90
|
import { normalizeVaultUrl } from "./url.js";
|
|
84
91
|
export const HOST_VAULT_ID = "host";
|
|
85
92
|
const HOST_MODEL_ID = "host-model";
|
|
@@ -90,242 +97,262 @@ export class VaultService {
|
|
|
90
97
|
this.store = options.store;
|
|
91
98
|
this.kek = options.kek;
|
|
92
99
|
}
|
|
93
|
-
// ---
|
|
94
|
-
async createVault(body) {
|
|
95
|
-
return this.store.tx((t) => this.replay(t, `create-vault:${body.idempotencyKey}`, body, async () => {
|
|
96
|
-
const installation = body.scope === "installation";
|
|
97
|
-
const ownerUserId = installation ? INSTALLATION_OWNER : body.ownerUserId;
|
|
98
|
-
if (!ownerUserId)
|
|
99
|
-
throw new VaultError(400, "ownerUserId is required for a user vault");
|
|
100
|
-
const row = {
|
|
101
|
-
id: randomUUID(),
|
|
102
|
-
name: body.name,
|
|
103
|
-
ownerUserId,
|
|
104
|
-
metadataJson: body.metadata ? JSON.stringify(body.metadata) : null,
|
|
105
|
-
createdAt: new Date().toISOString(),
|
|
106
|
-
scope: installation ? "installation" : "user",
|
|
107
|
-
};
|
|
108
|
-
await t.insertVault(row);
|
|
109
|
-
await this.audit(t, {
|
|
110
|
-
actor: "application",
|
|
111
|
-
action: "create",
|
|
112
|
-
vaultId: row.id,
|
|
113
|
-
outcome: "created",
|
|
114
|
-
});
|
|
115
|
-
return vaultInfoOf(row);
|
|
116
|
-
}));
|
|
117
|
-
}
|
|
100
|
+
// --- an Environment's vault (caller's transaction) ------------------------------
|
|
118
101
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
102
|
+
* Creates Environment `environmentId`'s vault (D56), in the transaction that creates it; answers
|
|
103
|
+
* its id, `id` when given (a fork's, whose secrets were sealed for it first).
|
|
121
104
|
*/
|
|
122
|
-
async
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
105
|
+
async createEnvironmentVault(t, environmentId, actor, id = randomUUID()) {
|
|
106
|
+
const row = {
|
|
107
|
+
id,
|
|
108
|
+
name: environmentId,
|
|
109
|
+
metadataJson: null,
|
|
110
|
+
createdAt: new Date().toISOString(),
|
|
111
|
+
environmentId,
|
|
112
|
+
};
|
|
113
|
+
await t.insertVault(row);
|
|
114
|
+
await this.audit(t, { actor, action: "create", vaultId: row.id, target: environmentId, outcome: "created" });
|
|
115
|
+
return row.id;
|
|
131
116
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
await t.deleteVault(id);
|
|
117
|
+
/**
|
|
118
|
+
* Deletes Environment `environmentId`'s vault and its credentials, with their audit rows, in
|
|
119
|
+
* the transaction that deletes the Environment: no release or tool call finds them after it
|
|
120
|
+
* commits.
|
|
121
|
+
*/
|
|
122
|
+
async deleteEnvironmentVault(t, environmentId, actor) {
|
|
123
|
+
const vault = await t.vaultOfEnvironment(environmentId);
|
|
124
|
+
if (!vault)
|
|
125
|
+
return;
|
|
126
|
+
for (const credential of await t.credentialsForVault(vault.id))
|
|
145
127
|
await this.audit(t, {
|
|
146
|
-
actor
|
|
128
|
+
actor,
|
|
147
129
|
action: "delete",
|
|
148
|
-
vaultId: id,
|
|
130
|
+
vaultId: vault.id,
|
|
131
|
+
credentialId: credential.id,
|
|
132
|
+
target: environmentId,
|
|
149
133
|
outcome: "deleted",
|
|
150
134
|
});
|
|
151
|
-
|
|
152
|
-
});
|
|
135
|
+
await t.deleteVault(vault.id);
|
|
136
|
+
await this.audit(t, { actor, action: "delete", vaultId: vault.id, target: environmentId, outcome: "deleted" });
|
|
153
137
|
}
|
|
154
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Seals Environment `from`'s secrets again for vault `vaultId`, a fork's new vault (Q15): each
|
|
140
|
+
* value is unsealed and sealed under the new vault's and credential's ids, which its AAD binds.
|
|
141
|
+
* Needs the vault key, so callers reach it through `Keys`; it reads `from` in its own
|
|
142
|
+
* transaction and writes nothing. With `slots` (what the fork runs declares secret slots, D64),
|
|
143
|
+
* only the names it declares are sealed, each with its slot's binding; a value the slot's type
|
|
144
|
+
* cannot take is left behind. The fork's transaction inserts the copies
|
|
145
|
+
* (`insertEnvironmentSecretCopies`).
|
|
146
|
+
*/
|
|
147
|
+
async sealEnvironmentSecretCopies(from, vaultId, slots) {
|
|
155
148
|
const kek = this.kek();
|
|
156
|
-
|
|
157
|
-
await
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
149
|
+
const rows = await this.store.tx(async (t) => {
|
|
150
|
+
const source = await t.vaultOfEnvironment(from);
|
|
151
|
+
return source ? (await t.credentialsForVault(source.id)).filter(isUserCredential) : [];
|
|
152
|
+
});
|
|
153
|
+
const sealed = [];
|
|
154
|
+
const dropped = [];
|
|
155
|
+
const now = new Date().toISOString();
|
|
156
|
+
for (const row of rows) {
|
|
157
|
+
const payload = readPayload(kek, row);
|
|
158
|
+
let type = row.type;
|
|
159
|
+
let binding = credentialBinding(row);
|
|
160
|
+
let value = payload;
|
|
161
|
+
if (slots) {
|
|
162
|
+
const slot = slots[row.name];
|
|
163
|
+
const rebound = slot === undefined ? undefined : rebind(row.name, slot, row, payload, binding);
|
|
164
|
+
if (rebound === undefined) {
|
|
165
|
+
dropped.push(row.name);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
({ type, binding, payload: value } = rebound);
|
|
169
|
+
try {
|
|
170
|
+
conflictOf(type, binding, sealed);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (!(error instanceof VaultError))
|
|
174
|
+
throw error;
|
|
175
|
+
dropped.push(row.name);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
169
178
|
}
|
|
170
|
-
const
|
|
171
|
-
|
|
179
|
+
const id = randomUUID();
|
|
180
|
+
sealed.push({
|
|
172
181
|
id,
|
|
173
182
|
vaultId,
|
|
174
|
-
name:
|
|
183
|
+
name: row.name,
|
|
175
184
|
type,
|
|
176
185
|
bindingJson: JSON.stringify(binding),
|
|
177
|
-
expiresAt:
|
|
178
|
-
createdAt:
|
|
186
|
+
expiresAt: row.expiresAt,
|
|
187
|
+
createdAt: now,
|
|
179
188
|
rotatedAt: null,
|
|
180
|
-
...
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
189
|
+
...encryptSecret(kek, credentialAad(vaultId, id, type, binding), Buffer.from(JSON.stringify(value), "utf8")),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
copies: sealed.map((row) => ({
|
|
194
|
+
id: row.id,
|
|
195
|
+
name: row.name,
|
|
196
|
+
type: row.type,
|
|
197
|
+
bindingJson: row.bindingJson,
|
|
198
|
+
expiresAt: row.expiresAt,
|
|
199
|
+
kekId: row.kekId,
|
|
200
|
+
nonce: Buffer.from(row.nonce).toString("base64"),
|
|
201
|
+
ciphertext: Buffer.from(row.ciphertext).toString("base64"),
|
|
202
|
+
wrappedDek: Buffer.from(row.wrappedDek).toString("base64"),
|
|
203
|
+
})),
|
|
204
|
+
dropped: dropped.sort(),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Inserts a fork's sealed copies (`sealEnvironmentSecretCopies`) into vault `vaultId`, in the
|
|
209
|
+
* transaction that creates the fork, with an audit row each naming `from`. Reads no secret.
|
|
210
|
+
*/
|
|
211
|
+
async insertEnvironmentSecretCopies(t, vaultId, from, actor, sealed) {
|
|
212
|
+
const now = new Date().toISOString();
|
|
213
|
+
for (const copy of sealed.copies) {
|
|
214
|
+
await t.insertCredential({
|
|
215
|
+
id: copy.id,
|
|
186
216
|
vaultId,
|
|
187
|
-
|
|
188
|
-
|
|
217
|
+
name: copy.name,
|
|
218
|
+
type: copy.type,
|
|
219
|
+
bindingJson: copy.bindingJson,
|
|
220
|
+
expiresAt: copy.expiresAt,
|
|
221
|
+
createdAt: now,
|
|
222
|
+
rotatedAt: null,
|
|
223
|
+
kekId: copy.kekId,
|
|
224
|
+
nonce: Buffer.from(copy.nonce, "base64"),
|
|
225
|
+
ciphertext: Buffer.from(copy.ciphertext, "base64"),
|
|
226
|
+
wrappedDek: Buffer.from(copy.wrappedDek, "base64"),
|
|
189
227
|
});
|
|
190
|
-
|
|
191
|
-
}
|
|
228
|
+
await this.audit(t, { actor, action: "create", vaultId, credentialId: copy.id, target: from, outcome: "copied" });
|
|
229
|
+
}
|
|
230
|
+
return { copied: sealed.copies.map((copy) => copy.name).sort(), dropped: [...sealed.dropped] };
|
|
192
231
|
}
|
|
193
|
-
|
|
232
|
+
// --- an Environment's secrets ----------------------------------------------------
|
|
233
|
+
/**
|
|
234
|
+
* `PUT /v1/environments/{id}/secrets/{name}`: creates or replaces one credential. The same
|
|
235
|
+
* name and type keeps the credential's id and re-seals its value and binding; another type
|
|
236
|
+
* replaces it. Refused (`409 credential_conflict`) when another credential of the vault is
|
|
237
|
+
* bound to one of its hosts or to its URL. Runs under the Environment row's lock, which also
|
|
238
|
+
* orders it with session opens and deletes, and counts as the Environment's use.
|
|
239
|
+
*/
|
|
240
|
+
async putEnvironmentSecret(environmentId, name, body, actor) {
|
|
241
|
+
const invalid = environmentSecretNameError(body.type, name);
|
|
242
|
+
if (invalid)
|
|
243
|
+
throw new VaultError(400, invalid);
|
|
244
|
+
let payload;
|
|
245
|
+
let binding;
|
|
246
|
+
if (body.type === "environment_secret" || body.type === "environment_variable")
|
|
247
|
+
({ payload, binding } = environmentCredential(name, body));
|
|
248
|
+
else {
|
|
249
|
+
const url = normalizeVaultUrl(body.url);
|
|
250
|
+
payload = payloadOf(body);
|
|
251
|
+
binding = bindingOf(url, payload, body);
|
|
252
|
+
}
|
|
253
|
+
const kek = this.kek();
|
|
194
254
|
return this.store.tx(async (t) => {
|
|
195
|
-
await this.
|
|
196
|
-
|
|
255
|
+
const vault = await this.environmentVault(t, environmentId, { lock: true });
|
|
256
|
+
const now = new Date().toISOString();
|
|
257
|
+
const others = (await t.credentialsForVault(vault.id))
|
|
197
258
|
.filter(isUserCredential)
|
|
198
|
-
.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const kek = this.kek();
|
|
206
|
-
return this.store.tx((t) => this.replay(t, `rotate:${vaultId}:${id}:${body.idempotencyKey}`, body, async () => {
|
|
207
|
-
const row = await this.credentialRow(t, vaultId, id);
|
|
208
|
-
if (row.type !== body.auth.type)
|
|
209
|
-
throw new VaultError(409, "Credential type cannot change");
|
|
210
|
-
const current = credentialBinding(row);
|
|
211
|
-
let next;
|
|
212
|
-
let binding;
|
|
213
|
-
if (body.auth.type === "environment_secret") {
|
|
214
|
-
next = { value: body.auth.secretValue };
|
|
215
|
-
binding = current;
|
|
216
|
-
}
|
|
217
|
-
else if (body.auth.type === "environment_variable") {
|
|
218
|
-
next = {};
|
|
219
|
-
binding = { ...current, variableValue: body.auth.variableValue };
|
|
259
|
+
.filter((row) => row.name !== name);
|
|
260
|
+
conflictOf(body.type, binding, others);
|
|
261
|
+
const existing = await t.credentialByName(vault.id, name);
|
|
262
|
+
let row;
|
|
263
|
+
if (existing && isUserCredential(existing) && existing.type === body.type) {
|
|
264
|
+
row = await this.writePayload(t, kek, existing, payload, binding, now);
|
|
265
|
+
await this.audit(t, { actor, action: "rotate", vaultId: vault.id, credentialId: row.id, outcome: "rotated" });
|
|
220
266
|
}
|
|
221
267
|
else {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
268
|
+
if (existing) {
|
|
269
|
+
await t.deleteCredential(vault.id, existing.id);
|
|
270
|
+
await this.audit(t, { actor, action: "delete", vaultId: vault.id, credentialId: existing.id, outcome: "deleted" });
|
|
271
|
+
}
|
|
272
|
+
const id = randomUUID();
|
|
273
|
+
const sealed = encryptSecret(kek, credentialAad(vault.id, id, body.type, binding), Buffer.from(JSON.stringify(payload), "utf8"));
|
|
274
|
+
row = {
|
|
275
|
+
id,
|
|
276
|
+
vaultId: vault.id,
|
|
277
|
+
name,
|
|
278
|
+
type: body.type,
|
|
279
|
+
bindingJson: JSON.stringify(binding),
|
|
280
|
+
expiresAt: null,
|
|
281
|
+
createdAt: now,
|
|
282
|
+
rotatedAt: null,
|
|
283
|
+
...sealed,
|
|
284
|
+
};
|
|
285
|
+
await t.insertCredential(row);
|
|
286
|
+
await this.audit(t, { actor, action: "create", vaultId: vault.id, credentialId: id, outcome: "created" });
|
|
227
287
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
action: "rotate",
|
|
232
|
-
vaultId,
|
|
233
|
-
credentialId: id,
|
|
234
|
-
outcome: "rotated",
|
|
235
|
-
});
|
|
236
|
-
return credentialInfoOf(updated);
|
|
237
|
-
}));
|
|
288
|
+
await t.updateEnvironment(environmentId, { lastUsedAt: now }, now);
|
|
289
|
+
return secretViewOf(row);
|
|
290
|
+
});
|
|
238
291
|
}
|
|
239
|
-
|
|
292
|
+
/** The Environment's secrets: names, types and bindings, never values. */
|
|
293
|
+
async listEnvironmentSecrets(environmentId) {
|
|
240
294
|
return this.store.tx(async (t) => {
|
|
241
|
-
await this.
|
|
242
|
-
await t.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
vaultId,
|
|
247
|
-
credentialId: id,
|
|
248
|
-
outcome: "deleted",
|
|
249
|
-
});
|
|
250
|
-
return { id };
|
|
295
|
+
const vault = await this.environmentVault(t, environmentId);
|
|
296
|
+
return (await t.credentialsForVault(vault.id))
|
|
297
|
+
.filter(isUserCredential)
|
|
298
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
299
|
+
.map(secretViewOf);
|
|
251
300
|
});
|
|
252
301
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (new Set(selections.map((item) => item.serverName)).size !== selections.length)
|
|
262
|
-
throw new VaultError(400, "Duplicate credential selection");
|
|
263
|
-
for (const id of vaultIds) {
|
|
264
|
-
const vault = await t.getVault(id);
|
|
265
|
-
if (!vault)
|
|
266
|
-
throw new VaultError(404, "Vault not found");
|
|
267
|
-
if (vault.scope === "host")
|
|
268
|
-
throw new VaultError(400, "Host vault cannot be attached to a session");
|
|
269
|
-
// Any session may use the installation's own vaults (F9-D13).
|
|
270
|
-
if (vault.scope === "installation")
|
|
271
|
-
continue;
|
|
272
|
-
if (vault.ownerUserId !== ownerUserId)
|
|
273
|
-
throw options.opaque
|
|
274
|
-
? new VaultError(404, "Vault not found")
|
|
275
|
-
: new VaultError(403, "Vault belongs to another user");
|
|
276
|
-
}
|
|
277
|
-
for (const selection of selections) {
|
|
278
|
-
const row = await t.getCredential(selection.credentialId);
|
|
279
|
-
if (!row || !vaultIds.includes(row.vaultId))
|
|
280
|
-
throw new VaultError(400, "Credential is not in an attached vault");
|
|
281
|
-
}
|
|
302
|
+
async getEnvironmentSecret(environmentId, name) {
|
|
303
|
+
return this.store.tx(async (t) => {
|
|
304
|
+
const vault = await this.environmentVault(t, environmentId);
|
|
305
|
+
const row = await t.credentialByName(vault.id, name);
|
|
306
|
+
if (!row || !isUserCredential(row))
|
|
307
|
+
throw new VaultError(404, "Secret not found");
|
|
308
|
+
return secretViewOf(row);
|
|
309
|
+
});
|
|
282
310
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
outcome: "
|
|
311
|
+
async deleteEnvironmentSecret(environmentId, name, actor) {
|
|
312
|
+
return this.store.tx(async (t) => {
|
|
313
|
+
const vault = await this.environmentVault(t, environmentId, { lock: true });
|
|
314
|
+
const row = await t.credentialByName(vault.id, name);
|
|
315
|
+
if (!row || !isUserCredential(row))
|
|
316
|
+
throw new VaultError(404, "Secret not found");
|
|
317
|
+
await t.deleteCredential(vault.id, row.id);
|
|
318
|
+
await this.audit(t, { actor, action: "delete", vaultId: vault.id, credentialId: row.id, outcome: "deleted" });
|
|
319
|
+
const now = new Date().toISOString();
|
|
320
|
+
await t.updateEnvironment(environmentId, { lastUsedAt: now }, now);
|
|
321
|
+
return { id: name };
|
|
291
322
|
});
|
|
292
323
|
}
|
|
293
324
|
/**
|
|
294
325
|
* The variables a session's sandbox commands get (R2c, D50): each `environment_secret`'s name
|
|
295
|
-
* set to the sentinel, each `environment_variable`'s value,
|
|
296
|
-
*
|
|
297
|
-
* secrets bound to one host. Reads rows only; no plaintext. Run in the caller's transaction.
|
|
326
|
+
* set to the sentinel, and each `environment_variable`'s value, from the Environment's vault.
|
|
327
|
+
* Reads rows only; no plaintext. Run in the caller's transaction.
|
|
298
328
|
*/
|
|
299
|
-
async
|
|
329
|
+
async environmentVariables(t, environmentId) {
|
|
330
|
+
if (environmentId === undefined)
|
|
331
|
+
return {};
|
|
332
|
+
const vault = await t.vaultOfEnvironment(environmentId);
|
|
333
|
+
if (!vault)
|
|
334
|
+
return {};
|
|
300
335
|
const environment = {};
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
environment[name] = row.type === "environment_secret" ? ENVIRONMENT_SECRET_SENTINEL : binding.variableValue;
|
|
312
|
-
for (const host of row.type === "environment_secret" ? binding.allowedHosts ?? [] : []) {
|
|
313
|
-
if (hosts.has(host) && hosts.get(host) !== row.id)
|
|
314
|
-
conflict ??= `Two attached environment_secret credentials are bound to ${host}`;
|
|
315
|
-
hosts.set(host, row.id);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return { environment, secretHosts: Object.fromEntries([...hosts].sort()), ...(conflict ? { conflict } : {}) };
|
|
336
|
+
for (const row of await t.credentialsForVault(vault.id)) {
|
|
337
|
+
if (row.type !== "environment_secret" && row.type !== "environment_variable")
|
|
338
|
+
continue;
|
|
339
|
+
const binding = JSON.parse(row.bindingJson);
|
|
340
|
+
if (row.type === "environment_secret")
|
|
341
|
+
environment[binding.secretName] = ENVIRONMENT_SECRET_SENTINEL;
|
|
342
|
+
else
|
|
343
|
+
environment[binding.variableName] = binding.variableValue;
|
|
344
|
+
}
|
|
345
|
+
return environment;
|
|
319
346
|
}
|
|
320
347
|
/**
|
|
321
|
-
* The header egress-gate sets on a request from
|
|
322
|
-
* `
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
348
|
+
* The header egress-gate sets on a request from Environment `environmentId`'s sandbox to
|
|
349
|
+
* `host` (R2c, D50): the `environment_secret` of its vault bound to `host`, its value unsealed
|
|
350
|
+
* and rendered into its format. `none` when no secret is bound to the host; `refused` when the
|
|
351
|
+
* sealed value cannot be read. One query and one unseal per call; nothing is cached (E3).
|
|
352
|
+
* Plaintext exists only in the returned header.
|
|
326
353
|
*/
|
|
327
354
|
async releaseEnvironmentSecret(input) {
|
|
328
|
-
const found = await this.store.tx((t) =>
|
|
355
|
+
const found = await this.store.tx((t) => environmentSecretsFor(t, input.environmentId, input.host));
|
|
329
356
|
if (found.length === 0)
|
|
330
357
|
return { status: "none" };
|
|
331
358
|
if (found.length > 1)
|
|
@@ -345,21 +372,23 @@ export class VaultService {
|
|
|
345
372
|
const inject = credentialBinding(row).inject ?? ENVIRONMENT_SECRET_DEFAULT_INJECT;
|
|
346
373
|
return { status: "released", header: inject.header, value: renderInjectFormat(inject.format, payload.value), credentialId: row.id };
|
|
347
374
|
}
|
|
348
|
-
/** Whether
|
|
349
|
-
async environmentSecretBound(
|
|
350
|
-
return (await this.store.tx((t) =>
|
|
375
|
+
/** Whether an `environment_secret` of Environment `environmentId` is bound to `host`. */
|
|
376
|
+
async environmentSecretBound(environmentId, host) {
|
|
377
|
+
return (await this.store.tx((t) => environmentSecretsFor(t, environmentId, host))).length > 0;
|
|
351
378
|
}
|
|
352
379
|
// --- use ---------------------------------------------------------------------
|
|
380
|
+
/**
|
|
381
|
+
* The headers a request to `url` carries: the credential of the Environment's vault bound to
|
|
382
|
+
* it, or none (`unauthenticated`) without an Environment or such a credential.
|
|
383
|
+
*/
|
|
353
384
|
async authorize(input) {
|
|
354
385
|
const url = normalizeVaultUrl(input.url);
|
|
386
|
+
const environmentId = input.environmentId;
|
|
387
|
+
if (environmentId === undefined)
|
|
388
|
+
return { status: "unauthenticated", url, headers: {} };
|
|
355
389
|
const decided = await this.store.tx(async (t) => {
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
const found = await t.getVault(id);
|
|
359
|
-
if (found) {
|
|
360
|
-
scopes.set(id, found.scope);
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
390
|
+
const vault = await t.vaultOfEnvironment(environmentId);
|
|
391
|
+
if (!vault) {
|
|
363
392
|
await this.audit(t, {
|
|
364
393
|
actor: "host",
|
|
365
394
|
action: "use",
|
|
@@ -367,48 +396,29 @@ export class VaultService {
|
|
|
367
396
|
target: url,
|
|
368
397
|
outcome: "refused",
|
|
369
398
|
});
|
|
370
|
-
return {
|
|
371
|
-
result: {
|
|
372
|
-
status: "refused",
|
|
373
|
-
url,
|
|
374
|
-
credentialIds: [],
|
|
375
|
-
reason: "vault_missing",
|
|
376
|
-
},
|
|
377
|
-
};
|
|
399
|
+
return { result: { status: "refused", url, credentialIds: [], reason: "vault_missing" } };
|
|
378
400
|
}
|
|
379
|
-
const matches =
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
matches.push(row);
|
|
384
|
-
matches.sort((a, b) => a.id.localeCompare(b.id));
|
|
401
|
+
const matches = (await t.credentialsForVault(vault.id))
|
|
402
|
+
.filter(isUserCredential)
|
|
403
|
+
.filter((row) => bindingUrl(row) === url)
|
|
404
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
385
405
|
if (matches.length === 0)
|
|
386
406
|
return { result: { status: "unauthenticated", url, headers: {} } };
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
: undefined;
|
|
390
|
-
const chosen = choose(matches, selection);
|
|
391
|
-
if (chosen.kind === "refused") {
|
|
407
|
+
if (matches.length > 1) {
|
|
408
|
+
// Refused at write (D56); kept as a guard.
|
|
392
409
|
await this.audit(t, {
|
|
393
410
|
actor: "host",
|
|
394
411
|
action: "use",
|
|
412
|
+
vaultId: vault.id,
|
|
395
413
|
sessionId: input.sessionId,
|
|
396
414
|
target: url,
|
|
397
415
|
outcome: "refused",
|
|
398
416
|
});
|
|
399
417
|
return {
|
|
400
|
-
result: {
|
|
401
|
-
status: "refused",
|
|
402
|
-
url,
|
|
403
|
-
credentialIds: chosen.credentialIds,
|
|
404
|
-
reason: chosen.reason,
|
|
405
|
-
},
|
|
418
|
+
result: { status: "refused", url, credentialIds: matches.map((row) => row.id), reason: "ambiguous" },
|
|
406
419
|
};
|
|
407
420
|
}
|
|
408
|
-
return {
|
|
409
|
-
row: chosen.row,
|
|
410
|
-
vault: scopes.get(chosen.row.vaultId) === "installation" ? "installation" : "user",
|
|
411
|
-
};
|
|
421
|
+
return { row: matches[0] };
|
|
412
422
|
});
|
|
413
423
|
if ("result" in decided)
|
|
414
424
|
return decided.result;
|
|
@@ -431,22 +441,11 @@ export class VaultService {
|
|
|
431
441
|
target: url,
|
|
432
442
|
outcome: "refused",
|
|
433
443
|
}));
|
|
434
|
-
return {
|
|
435
|
-
status: "refused",
|
|
436
|
-
url,
|
|
437
|
-
credentialIds: [row.id],
|
|
438
|
-
reason: "unreadable",
|
|
439
|
-
};
|
|
444
|
+
return { status: "refused", url, credentialIds: [row.id], reason: "unreadable" };
|
|
440
445
|
}
|
|
441
446
|
const headers = sendHeaders(row, payload);
|
|
442
|
-
if (!headers)
|
|
443
|
-
return {
|
|
444
|
-
status: "refused",
|
|
445
|
-
url,
|
|
446
|
-
credentialIds: [row.id],
|
|
447
|
-
reason: "unreadable",
|
|
448
|
-
};
|
|
449
|
-
}
|
|
447
|
+
if (!headers)
|
|
448
|
+
return { status: "refused", url, credentialIds: [row.id], reason: "unreadable" };
|
|
450
449
|
await this.store.tx((t) => this.audit(t, {
|
|
451
450
|
actor: "host",
|
|
452
451
|
action: "use",
|
|
@@ -463,9 +462,45 @@ export class VaultService {
|
|
|
463
462
|
headers,
|
|
464
463
|
...(binding.via === undefined ? {} : { via: binding.via }),
|
|
465
464
|
...(binding.identity === undefined ? {} : { identity: binding.identity }),
|
|
466
|
-
vault: decided.vault,
|
|
467
465
|
};
|
|
468
466
|
}
|
|
467
|
+
// --- coverage --------------------------------------------------------------------
|
|
468
|
+
/**
|
|
469
|
+
* What a session that names `request.environmentId` would send for each URL `manifest` names
|
|
470
|
+
* (`credentialNeeds`), decided as `authorize` decides a call: the credential of the
|
|
471
|
+
* Environment's vault bound to the URL. Reads no secret, writes no audit row.
|
|
472
|
+
*/
|
|
473
|
+
async coverage(manifest, request) {
|
|
474
|
+
const environmentId = request.environmentId;
|
|
475
|
+
return this.store.tx(async (t) => {
|
|
476
|
+
const credentials = [];
|
|
477
|
+
if (environmentId !== undefined) {
|
|
478
|
+
const vault = await this.environmentVault(t, environmentId);
|
|
479
|
+
for (const row of await t.credentialsForVault(vault.id))
|
|
480
|
+
if (isUserCredential(row))
|
|
481
|
+
credentials.push({ row, url: bindingUrl(row) });
|
|
482
|
+
}
|
|
483
|
+
const entries = credentialNeeds(manifest).map((need) => {
|
|
484
|
+
let url;
|
|
485
|
+
try {
|
|
486
|
+
url = normalizeVaultUrl(need.url);
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
if (!(error instanceof VaultError))
|
|
490
|
+
throw error;
|
|
491
|
+
return coverageEntry(need, need.url, undefined, environmentId, error.message);
|
|
492
|
+
}
|
|
493
|
+
const bound = credentials.find((item) => item.url === url);
|
|
494
|
+
return coverageEntry(need, url, bound && coverageCredential(environmentId, bound.row), environmentId);
|
|
495
|
+
});
|
|
496
|
+
return {
|
|
497
|
+
agentId: request.agentId,
|
|
498
|
+
environmentId: environmentId ?? null,
|
|
499
|
+
complete: entries.every((entry) => entry.status === "covered"),
|
|
500
|
+
entries,
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
}
|
|
469
504
|
// --- host model ----------------------------------------------------------------
|
|
470
505
|
async getHostModel() {
|
|
471
506
|
return this.store.tx((t) => this.hostModelView(t));
|
|
@@ -473,9 +508,11 @@ export class VaultService {
|
|
|
473
508
|
async listHostProviders() {
|
|
474
509
|
return this.store.tx(async (t) => {
|
|
475
510
|
const active = await activeProviderId(t);
|
|
511
|
+
const added = await readAddedInputs(t);
|
|
476
512
|
const catalog = new Map(hostModelCatalog().providers.map((provider) => [provider.id, provider.name]));
|
|
477
513
|
const providers = (await hostModelRows(t)).map((row) => {
|
|
478
514
|
const binding = modelBinding(row);
|
|
515
|
+
const inputs = inputsOfHostModel({ configured: true, ...binding }, added);
|
|
479
516
|
return {
|
|
480
517
|
id: binding.provider,
|
|
481
518
|
name: catalog.get(binding.provider) ??
|
|
@@ -486,6 +523,7 @@ export class VaultService {
|
|
|
486
523
|
authType: binding.authType,
|
|
487
524
|
...(binding.baseUrl ? { baseUrl: binding.baseUrl } : {}),
|
|
488
525
|
...(binding.settings ? { settings: binding.settings } : {}),
|
|
526
|
+
...(inputs ? { inputs } : {}),
|
|
489
527
|
lastUpdated: row.rotatedAt ?? row.createdAt,
|
|
490
528
|
active: binding.provider === active,
|
|
491
529
|
};
|
|
@@ -586,31 +624,39 @@ export class VaultService {
|
|
|
586
624
|
throw new VaultError(404, "Credential not found");
|
|
587
625
|
return { ...row, ...patch };
|
|
588
626
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
627
|
+
/**
|
|
628
|
+
* The vault of a live Environment (not being deleted), or the 404 of a missing one. With
|
|
629
|
+
* `lock`, the Environment's row is locked until the transaction ends.
|
|
630
|
+
*/
|
|
631
|
+
async environmentVault(t, environmentId, options = {}) {
|
|
632
|
+
const environment = await t.environment(environmentId, options);
|
|
633
|
+
const vault = environment && environment.pod?.desired !== "deleted"
|
|
634
|
+
? await t.vaultOfEnvironment(environmentId)
|
|
635
|
+
: undefined;
|
|
636
|
+
if (!vault)
|
|
637
|
+
throw new VaultError(404, "Environment not found");
|
|
638
|
+
return vault;
|
|
594
639
|
}
|
|
595
640
|
async ensureHostVault(t) {
|
|
596
|
-
|
|
597
|
-
if (existing?.scope === "host")
|
|
641
|
+
if (await t.getVault(HOST_VAULT_ID))
|
|
598
642
|
return;
|
|
599
|
-
if (existing)
|
|
600
|
-
throw new VaultError(409, "Host vault id is already used");
|
|
601
643
|
await t.insertVault({
|
|
602
644
|
id: HOST_VAULT_ID,
|
|
603
645
|
name: "Host",
|
|
604
|
-
ownerUserId: "host",
|
|
605
646
|
metadataJson: null,
|
|
606
647
|
createdAt: new Date().toISOString(),
|
|
607
|
-
|
|
648
|
+
environmentId: null,
|
|
608
649
|
});
|
|
609
650
|
}
|
|
610
651
|
validateHostModel(body) {
|
|
611
652
|
if (body.settings && body.provider !== "custom")
|
|
612
|
-
throw new VaultError(400, "Model settings (context window, output limit, reasoning, compat) apply only to a custom provider.");
|
|
653
|
+
throw new VaultError(400, "Model settings (context window, output limit, reasoning, inputs, compat) apply only to a custom provider.");
|
|
613
654
|
if (body.provider === "custom") {
|
|
655
|
+
// A custom endpoint is OpenAI-compatible: it reads only what that API carries (protocol 12).
|
|
656
|
+
const inputsRefused = body.settings?.inputs &&
|
|
657
|
+
inputsError({ api: "openai-completions", input: ["text", "image"] }, body.settings.inputs);
|
|
658
|
+
if (inputsRefused)
|
|
659
|
+
throw new VaultError(400, inputsRefused);
|
|
614
660
|
if (!body.baseUrl)
|
|
615
661
|
throw new VaultError(400, "A base URL is required for a custom provider.");
|
|
616
662
|
let url;
|
|
@@ -680,12 +726,6 @@ export class VaultService {
|
|
|
680
726
|
for (const id of new Set(ids))
|
|
681
727
|
await t.deleteCredential(HOST_VAULT_ID, id);
|
|
682
728
|
}
|
|
683
|
-
async credentialRow(t, vaultId, id) {
|
|
684
|
-
const row = await t.getCredential(id);
|
|
685
|
-
if (!row || row.vaultId !== vaultId || !isUserCredential(row))
|
|
686
|
-
throw new VaultError(404, "Credential not found");
|
|
687
|
-
return row;
|
|
688
|
-
}
|
|
689
729
|
async audit(t, entry) {
|
|
690
730
|
await t.insertVaultAudit({
|
|
691
731
|
id: randomUUID(),
|
|
@@ -706,35 +746,80 @@ function isUserCredential(row) {
|
|
|
706
746
|
function bindingUrl(row) {
|
|
707
747
|
return JSON.parse(row.bindingJson).url;
|
|
708
748
|
}
|
|
709
|
-
|
|
749
|
+
/** Where a credential is, for coverage: never its value. */
|
|
750
|
+
function coverageCredential(environmentId, row) {
|
|
751
|
+
const via = JSON.parse(row.bindingJson).via;
|
|
752
|
+
return { environmentId, name: row.name, ...(via === undefined ? {} : { via }) };
|
|
753
|
+
}
|
|
754
|
+
/** One coverage entry, with the sentence that says what the session would do. */
|
|
755
|
+
function coverageEntry(need, url, credential, environmentId, invalid) {
|
|
756
|
+
const required = need.kind === "http";
|
|
757
|
+
const where = environmentId === undefined
|
|
758
|
+
? "Without an Environment, no credential is sent"
|
|
759
|
+
: `Environment ${environmentId} holds no credential for this URL`;
|
|
760
|
+
const message = invalid !== undefined
|
|
761
|
+
? `No credential can be bound to this URL (${invalid}).`
|
|
762
|
+
: credential !== undefined
|
|
763
|
+
? `Sends '${credential.name}' from Environment ${credential.environmentId}${credential.via ? ` through ${credential.via}` : ""}.`
|
|
764
|
+
: required
|
|
765
|
+
? `${where}: each call fails with http.credential.`
|
|
766
|
+
: `${where}: the server is called without one, which only a public server accepts.`;
|
|
710
767
|
return {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
768
|
+
kind: need.kind,
|
|
769
|
+
...(need.agentId === undefined ? {} : { agentId: need.agentId }),
|
|
770
|
+
...(need.stage === undefined ? {} : { stage: need.stage }),
|
|
771
|
+
name: need.name,
|
|
772
|
+
serverName: need.serverName,
|
|
773
|
+
url,
|
|
774
|
+
required,
|
|
775
|
+
status: credential === undefined ? "missing" : "covered",
|
|
776
|
+
...(credential === undefined ? {} : { credential }),
|
|
777
|
+
message,
|
|
718
778
|
};
|
|
719
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* Refuses a write that would bind a second credential to a host or a URL (D56): `others` are
|
|
782
|
+
* the vault's other credentials.
|
|
783
|
+
*/
|
|
784
|
+
function conflictOf(type, binding, others) {
|
|
785
|
+
if (type === "environment_secret") {
|
|
786
|
+
for (const row of others) {
|
|
787
|
+
if (row.type !== "environment_secret")
|
|
788
|
+
continue;
|
|
789
|
+
const hosts = credentialBinding(row).allowedHosts ?? [];
|
|
790
|
+
const host = (binding.allowedHosts ?? []).find((item) => hosts.includes(item));
|
|
791
|
+
if (host !== undefined)
|
|
792
|
+
throw new VaultError(409, `${row.name} is already bound to ${host}`, {
|
|
793
|
+
code: "credential_conflict",
|
|
794
|
+
details: { host, existing: row.name },
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
else if (type === "bearer" || type === "headers") {
|
|
799
|
+
const existing = others.find((row) => (row.type === "bearer" || row.type === "headers") && credentialBinding(row).url === binding.url);
|
|
800
|
+
if (existing)
|
|
801
|
+
throw new VaultError(409, `${existing.name} is already bound to ${binding.url}`, {
|
|
802
|
+
code: "credential_conflict",
|
|
803
|
+
details: { url: binding.url, existing: existing.name },
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
720
807
|
function credentialBinding(row) {
|
|
721
808
|
return JSON.parse(row.bindingJson);
|
|
722
809
|
}
|
|
723
|
-
|
|
810
|
+
/** A secret as `GET .../secrets/{name}` shows it: never its value. */
|
|
811
|
+
function secretViewOf(row) {
|
|
724
812
|
const binding = credentialBinding(row);
|
|
725
813
|
return {
|
|
726
|
-
id: row.id,
|
|
727
|
-
vaultId: row.vaultId,
|
|
728
814
|
name: row.name,
|
|
729
815
|
type: row.type,
|
|
730
816
|
binding: row.type === "environment_secret"
|
|
731
817
|
? {
|
|
732
|
-
secretName: binding.secretName,
|
|
733
818
|
allowedHosts: binding.allowedHosts,
|
|
734
819
|
inject: binding.inject ?? { ...ENVIRONMENT_SECRET_DEFAULT_INJECT },
|
|
735
820
|
}
|
|
736
821
|
: row.type === "environment_variable"
|
|
737
|
-
? {
|
|
822
|
+
? {}
|
|
738
823
|
: { url: binding.url },
|
|
739
824
|
...(binding.headerNames === undefined ? {} : { headerNames: binding.headerNames }),
|
|
740
825
|
...(binding.via === undefined ? {} : { via: binding.via }),
|
|
@@ -819,33 +904,10 @@ export function readHostPayload(kek, row, binding) {
|
|
|
819
904
|
plaintext.fill(0);
|
|
820
905
|
}
|
|
821
906
|
}
|
|
822
|
-
|
|
823
|
-
const ids = matches.map((row) => row.id);
|
|
824
|
-
if (matches.length === 1) {
|
|
825
|
-
const only = matches[0];
|
|
826
|
-
if (selection && selection.credentialId !== only.id)
|
|
827
|
-
return {
|
|
828
|
-
kind: "refused",
|
|
829
|
-
credentialIds: ids,
|
|
830
|
-
reason: "selection_mismatch",
|
|
831
|
-
};
|
|
832
|
-
return { kind: "use", row: only };
|
|
833
|
-
}
|
|
834
|
-
if (!selection)
|
|
835
|
-
return { kind: "refused", credentialIds: ids, reason: "ambiguous" };
|
|
836
|
-
const chosen = matches.find((row) => row.id === selection.credentialId);
|
|
837
|
-
if (!chosen)
|
|
838
|
-
return {
|
|
839
|
-
kind: "refused",
|
|
840
|
-
credentialIds: ids,
|
|
841
|
-
reason: "selection_mismatch",
|
|
842
|
-
};
|
|
843
|
-
return { kind: "use", row: chosen };
|
|
844
|
-
}
|
|
845
|
-
/** The sealed part of a create or rotate: a token, or a header map with lower-cased names. */
|
|
907
|
+
/** The sealed part of a tool credential: a token, or a header map with lower-cased names. */
|
|
846
908
|
function payloadOf(auth) {
|
|
847
909
|
if (auth.type === "bearer")
|
|
848
|
-
return { token: auth.
|
|
910
|
+
return { token: auth.value };
|
|
849
911
|
return {
|
|
850
912
|
headers: Object.fromEntries(Object.entries(auth.headers).map(([name, value]) => [name.toLowerCase(), value])),
|
|
851
913
|
};
|
|
@@ -907,18 +969,51 @@ function credentialAad(vaultId, credentialId, type, binding) {
|
|
|
907
969
|
}
|
|
908
970
|
: { vaultId, credentialId, type, url: binding.url }), "utf8");
|
|
909
971
|
}
|
|
910
|
-
/**
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
972
|
+
/**
|
|
973
|
+
* A copied credential's type, binding and payload under a slot of the fork (D64): its value with
|
|
974
|
+
* the slot's binding, or undefined when the slot's type cannot take it.
|
|
975
|
+
*/
|
|
976
|
+
function rebind(name, slot, row, payload, binding) {
|
|
977
|
+
const type = slot.type;
|
|
978
|
+
const value = row.type === "environment_variable" ? binding.variableValue : row.type === "bearer" ? payload.token : payload.value;
|
|
979
|
+
try {
|
|
980
|
+
if (type === "environment_secret" || type === "environment_variable") {
|
|
981
|
+
if (value === undefined)
|
|
982
|
+
return undefined;
|
|
983
|
+
const made = environmentCredential(name, { ...slot, type, value });
|
|
984
|
+
return { type, ...made };
|
|
985
|
+
}
|
|
986
|
+
const auth = type === "headers"
|
|
987
|
+
? payload.headers === undefined
|
|
988
|
+
? undefined
|
|
989
|
+
: { type, headers: payload.headers }
|
|
990
|
+
: value === undefined
|
|
991
|
+
? undefined
|
|
992
|
+
: { type: "bearer", value };
|
|
993
|
+
if (auth === undefined)
|
|
994
|
+
return undefined;
|
|
995
|
+
const sealed = payloadOf(auth);
|
|
996
|
+
const routing = slot;
|
|
997
|
+
return { type, payload: sealed, binding: bindingOf(normalizeVaultUrl(routing.url), sealed, routing) };
|
|
998
|
+
}
|
|
999
|
+
catch (error) {
|
|
1000
|
+
if (error instanceof VaultError)
|
|
1001
|
+
return undefined;
|
|
1002
|
+
throw error;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
/** The sealed payload and unsealed binding of a shell credential named `name` (R2c). */
|
|
1006
|
+
function environmentCredential(name, body) {
|
|
1007
|
+
if (body.type === "environment_variable")
|
|
1008
|
+
return { payload: {}, binding: { variableName: name, variableValue: body.value } };
|
|
1009
|
+
if (body.inject)
|
|
1010
|
+
checkCredentialHeaders([body.inject.header.toLowerCase()], undefined);
|
|
916
1011
|
return {
|
|
917
|
-
payload: { value:
|
|
1012
|
+
payload: { value: body.value },
|
|
918
1013
|
binding: {
|
|
919
|
-
secretName:
|
|
920
|
-
allowedHosts:
|
|
921
|
-
...(
|
|
1014
|
+
secretName: name,
|
|
1015
|
+
allowedHosts: body.allowedHosts.map((host) => host.toLowerCase()),
|
|
1016
|
+
...(body.inject ? { inject: { header: body.inject.header, format: body.inject.format } } : {}),
|
|
922
1017
|
},
|
|
923
1018
|
};
|
|
924
1019
|
}
|
|
@@ -926,21 +1021,12 @@ function requestHash(body) {
|
|
|
926
1021
|
const { requestId: _requestId, ...rest } = body;
|
|
927
1022
|
return canonical(rest);
|
|
928
1023
|
}
|
|
929
|
-
/**
|
|
930
|
-
|
|
931
|
-
* sandbox (R2c). A session between turns counts: it can run again, and what it started may
|
|
932
|
-
* still run. Session open keeps them to one owner and one credential per host.
|
|
933
|
-
*/
|
|
934
|
-
async function sandboxSecretsFor(t, sandboxId, host) {
|
|
1024
|
+
/** The `environment_secret` credentials of Environment `environmentId` bound to `host` (R2c). */
|
|
1025
|
+
async function environmentSecretsFor(t, environmentId, host) {
|
|
935
1026
|
const lower = host.toLowerCase();
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
for (const vaultId of vaultIds)
|
|
942
|
-
for (const row of await t.credentialsForVault(vaultId))
|
|
943
|
-
if (row.type === "environment_secret" && (credentialBinding(row).allowedHosts ?? []).includes(lower))
|
|
944
|
-
matches.push(row);
|
|
945
|
-
return matches;
|
|
1027
|
+
const vault = await t.vaultOfEnvironment(environmentId);
|
|
1028
|
+
if (!vault)
|
|
1029
|
+
return [];
|
|
1030
|
+
return (await t.credentialsForVault(vault.id)).filter((row) => row.type === "environment_secret" &&
|
|
1031
|
+
(credentialBinding(row).allowedHosts ?? []).includes(lower));
|
|
946
1032
|
}
|