| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Binding from "../../Binding.ts"; |
| 3 | import * as Output from "../../Output.ts"; |
| 4 | import { AWSEnvironment } from "../Environment.ts"; |
| 5 | import { isBindingHost } from "../Lambda/Function.ts"; |
| 6 | import type { AgentAlias } from "./AgentAlias.ts"; |
| 7 | import type { DataSource } from "./DataSource.ts"; |
| 8 | import type { KnowledgeBase } from "./KnowledgeBase.ts"; |
| 9 | import { bedrockModelArns } from "./ModelArns.ts"; |
| 10 | |
| 11 | /** |
| 12 | * Shared scaffolding for Bedrock HTTP bindings. |
| 13 | * |
| 14 | * NOT exported from `index.ts` — every thin `{Op}Http.ts` in this service is |
| 15 | * a `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the builders |
| 16 | * below. Everything except the operation, the IAM action list, and the |
| 17 | * injected identifier(s) is boilerplate. Genuinely-different bindings |
| 18 | * (e.g. `Rerank`, which grants a wildcard `bedrock:Rerank` alongside |
| 19 | * model-scoped `bedrock:InvokeModel`) stay bespoke. |
| 20 | */ |
| 21 | |
| 22 | const currentEnv = AWSEnvironment.current as unknown as Effect.Effect<{ |
| 23 | accountId: string; |
| 24 | region: string; |
| 25 | }>; |
| 26 | |
| 27 | /** |
| 28 | * Build the impl Effect for a model-scoped `bedrock-runtime` operation whose |
| 29 | * request carries a top-level `modelId`. The binding accepts one or more |
| 30 | * model references (foundation-model id, cross-region inference profile id, |
| 31 | * or full ARN), grants `actions` on exactly the resolved model ARNs, and the |
| 32 | * runtime callable defaults `modelId` to the first bound model. |
| 33 | */ |
| 34 | export const makeModelScopedHttpBinding = < |
| 35 | I extends { modelId: string }, |
| 36 | A, |
| 37 | E, |
| 38 | R, |
| 39 | >(options: { |
| 40 | /** Fully-qualified binding tag, e.g. `AWS.Bedrock.Converse`. */ |
| 41 | tag: string; |
| 42 | /** The distilled operation; `modelId` defaults to the first bound model. */ |
| 43 | operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>; |
| 44 | /** IAM actions granted on the bound model ARNs. */ |
| 45 | actions: readonly string[]; |
| 46 | }) => |
| 47 | Effect.gen(function* () { |
| 48 | const op = yield* options.operation; |
| 49 | |
| 50 | return Effect.fn(function* (model: string, ...additionalModels: string[]) { |
| 51 | if (!globalThis.__ALCHEMY_RUNTIME__) { |
| 52 | const host = yield* Binding.Host; |
| 53 | if (isBindingHost(host)) { |
| 54 | const { accountId, region } = yield* currentEnv; |
| 55 | // Sort so the binding identity (SID + ARN list) is deterministic |
| 56 | // regardless of argument order. |
| 57 | const sorted = [...new Set([model, ...additionalModels])].sort(); |
| 58 | yield* host.bind`Allow(${host}, ${options.tag}(${sorted.join(",")}))`( |
| 59 | { |
| 60 | policyStatements: [ |
| 61 | { |
| 62 | Effect: "Allow", |
| 63 | Action: [...options.actions], |
| 64 | Resource: [ |
| 65 | ...new Set( |
| 66 | sorted.flatMap((id) => |
| 67 | bedrockModelArns(region, accountId, id), |
| 68 | ), |
| 69 | ), |
| 70 | ], |
| 71 | }, |
| 72 | ], |
| 73 | }, |
| 74 | ); |
| 75 | } |
| 76 | } |
| 77 | return Effect.fn(`${options.tag}(${model})`)(function* ( |
| 78 | request: Omit<I, "modelId"> & { modelId?: string }, |
| 79 | ) { |
| 80 | return yield* op({ |
| 81 | ...request, |
| 82 | modelId: request.modelId ?? model, |
| 83 | } as unknown as I); |
| 84 | }); |
| 85 | }); |
| 86 | }); |
| 87 | |
| 88 | /** |
| 89 | * Build the impl Effect for a knowledge-base-scoped `bedrock-agent-runtime` |
| 90 | * operation whose request carries a top-level `knowledgeBaseId`. The binding |
| 91 | * grants `actions` on the bound {@link KnowledgeBase}'s ARN and the runtime |
| 92 | * callable injects its `knowledgeBaseId`. |
| 93 | */ |
| 94 | export const makeKnowledgeBaseScopedHttpBinding = < |
| 95 | I extends { knowledgeBaseId: string }, |
| 96 | A, |
| 97 | E, |
| 98 | R, |
| 99 | >(options: { |
| 100 | /** Fully-qualified binding tag, e.g. `AWS.Bedrock.Retrieve`. */ |
| 101 | tag: string; |
| 102 | /** The distilled operation; `knowledgeBaseId` is injected. */ |
| 103 | operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>; |
| 104 | /** IAM actions granted on the knowledge-base ARN. */ |
| 105 | actions: readonly string[]; |
| 106 | }) => |
| 107 | Effect.gen(function* () { |
| 108 | const op = yield* options.operation; |
| 109 | |
| 110 | return Effect.fn(function* <K extends KnowledgeBase>(knowledgeBase: K) { |
| 111 | const KnowledgeBaseId = yield* knowledgeBase.knowledgeBaseId; |
| 112 | if (!globalThis.__ALCHEMY_RUNTIME__) { |
| 113 | const host = yield* Binding.Host; |
| 114 | if (isBindingHost(host)) { |
| 115 | yield* host.bind`Allow(${host}, ${options.tag}(${knowledgeBase}))`({ |
| 116 | policyStatements: [ |
| 117 | { |
| 118 | Effect: "Allow", |
| 119 | Action: [...options.actions], |
| 120 | Resource: [knowledgeBase.knowledgeBaseArn], |
| 121 | }, |
| 122 | ], |
| 123 | }); |
| 124 | } |
| 125 | } |
| 126 | return Effect.fn(`${options.tag}(${knowledgeBase.LogicalId})`)(function* ( |
| 127 | request: Omit<I, "knowledgeBaseId">, |
| 128 | ) { |
| 129 | return yield* op({ |
| 130 | ...request, |
| 131 | knowledgeBaseId: yield* KnowledgeBaseId, |
| 132 | } as unknown as I); |
| 133 | }); |
| 134 | }); |
| 135 | }); |
| 136 | |
| 137 | /** |
| 138 | * Build the impl Effect for a data-source-scoped `bedrock-agent` operation |
| 139 | * whose request carries top-level `knowledgeBaseId` + `dataSourceId` |
| 140 | * (ingestion jobs, direct document ingestion). Bedrock authorizes these |
| 141 | * actions against the parent knowledge-base ARN, so the deploy-time half |
| 142 | * grants `actions` on the bound {@link DataSource}'s knowledge base; the |
| 143 | * runtime callable injects both identifiers. |
| 144 | */ |
| 145 | export const makeDataSourceScopedHttpBinding = < |
| 146 | I extends { knowledgeBaseId: string; dataSourceId: string }, |
| 147 | A, |
| 148 | E, |
| 149 | R, |
| 150 | >(options: { |
| 151 | /** Fully-qualified binding tag, e.g. `AWS.Bedrock.StartIngestionJob`. */ |
| 152 | tag: string; |
| 153 | /** The distilled operation; both identifiers are injected. */ |
| 154 | operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>; |
| 155 | /** IAM actions granted on the parent knowledge-base ARN. */ |
| 156 | actions: readonly string[]; |
| 157 | }) => |
| 158 | Effect.gen(function* () { |
| 159 | const op = yield* options.operation; |
| 160 | |
| 161 | return Effect.fn(function* <D extends DataSource>(dataSource: D) { |
| 162 | const KnowledgeBaseId = yield* dataSource.knowledgeBaseId; |
| 163 | const DataSourceId = yield* dataSource.dataSourceId; |
| 164 | if (!globalThis.__ALCHEMY_RUNTIME__) { |
| 165 | const host = yield* Binding.Host; |
| 166 | if (isBindingHost(host)) { |
| 167 | const { accountId, region } = yield* currentEnv; |
| 168 | yield* host.bind`Allow(${host}, ${options.tag}(${dataSource}))`({ |
| 169 | policyStatements: [ |
| 170 | { |
| 171 | Effect: "Allow", |
| 172 | Action: [...options.actions], |
| 173 | Resource: [ |
| 174 | dataSource.knowledgeBaseId.pipe( |
| 175 | Output.map( |
| 176 | (id) => |
| 177 | `arn:aws:bedrock:${region}:${accountId}:knowledge-base/${id}`, |
| 178 | ), |
| 179 | ), |
| 180 | ], |
| 181 | }, |
| 182 | ], |
| 183 | }); |
| 184 | } |
| 185 | } |
| 186 | return Effect.fn(`${options.tag}(${dataSource.LogicalId})`)(function* ( |
| 187 | request: Omit<I, "knowledgeBaseId" | "dataSourceId">, |
| 188 | ) { |
| 189 | return yield* op({ |
| 190 | ...request, |
| 191 | knowledgeBaseId: yield* KnowledgeBaseId, |
| 192 | dataSourceId: yield* DataSourceId, |
| 193 | } as unknown as I); |
| 194 | }); |
| 195 | }); |
| 196 | }); |
| 197 | |
| 198 | /** |
| 199 | * Build the impl Effect for an agent-alias-scoped `bedrock-agent-runtime` |
| 200 | * operation whose request carries top-level `agentId` + `agentAliasId`. The |
| 201 | * binding grants `actions` on the bound {@link AgentAlias}'s ARN and the |
| 202 | * runtime callable injects both identifiers. |
| 203 | */ |
| 204 | export const makeAgentAliasScopedHttpBinding = < |
| 205 | I extends { agentId: string; agentAliasId: string }, |
| 206 | A, |
| 207 | E, |
| 208 | R, |
| 209 | >(options: { |
| 210 | /** Fully-qualified binding tag, e.g. `AWS.Bedrock.InvokeAgent`. */ |
| 211 | tag: string; |
| 212 | /** The distilled operation; both identifiers are injected. */ |
| 213 | operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>; |
| 214 | /** IAM actions granted on the agent-alias ARN. */ |
| 215 | actions: readonly string[]; |
| 216 | }) => |
| 217 | Effect.gen(function* () { |
| 218 | const op = yield* options.operation; |
| 219 | |
| 220 | return Effect.fn(function* <AA extends AgentAlias>(alias: AA) { |
| 221 | const AgentId = yield* alias.agentId; |
| 222 | const AgentAliasId = yield* alias.agentAliasId; |
| 223 | if (!globalThis.__ALCHEMY_RUNTIME__) { |
| 224 | const host = yield* Binding.Host; |
| 225 | if (isBindingHost(host)) { |
| 226 | yield* host.bind`Allow(${host}, ${options.tag}(${alias}))`({ |
| 227 | policyStatements: [ |
| 228 | { |
| 229 | Effect: "Allow", |
| 230 | Action: [...options.actions], |
| 231 | Resource: [alias.agentAliasArn], |
| 232 | }, |
| 233 | ], |
| 234 | }); |
| 235 | } |
| 236 | } |
| 237 | return Effect.fn(`${options.tag}(${alias.LogicalId})`)(function* ( |
| 238 | request: Omit<I, "agentId" | "agentAliasId">, |
| 239 | ) { |
| 240 | return yield* op({ |
| 241 | ...request, |
| 242 | agentId: yield* AgentId, |
| 243 | agentAliasId: yield* AgentAliasId, |
| 244 | } as unknown as I); |
| 245 | }); |
| 246 | }); |
| 247 | }); |
| 248 | |
| 249 | /** |
| 250 | * Build the impl Effect for a managed-RAG operation (`RetrieveAndGenerate` / |
| 251 | * `RetrieveAndGenerateStream`). The binding grants `bedrock:Retrieve` + |
| 252 | * `bedrock:RetrieveAndGenerate` on the bound {@link KnowledgeBase} plus |
| 253 | * `bedrock:InvokeModel` scoped to the named generation models (or all |
| 254 | * foundation models + cross-region inference profiles when none are named). |
| 255 | * The request passes through unchanged — the knowledge base is referenced |
| 256 | * inside `retrieveAndGenerateConfiguration`. |
| 257 | */ |
| 258 | export const makeRagHttpBinding = <I, A, E, R>(options: { |
| 259 | /** Fully-qualified binding tag, e.g. `AWS.Bedrock.RetrieveAndGenerate`. */ |
| 260 | tag: string; |
| 261 | /** The distilled operation; the request passes through unchanged. */ |
| 262 | operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>; |
| 263 | }) => |
| 264 | Effect.gen(function* () { |
| 265 | const op = yield* options.operation; |
| 266 | |
| 267 | return Effect.fn(function* <K extends KnowledgeBase>( |
| 268 | knowledgeBase: K, |
| 269 | ...models: string[] |
| 270 | ) { |
| 271 | if (!globalThis.__ALCHEMY_RUNTIME__) { |
| 272 | const host = yield* Binding.Host; |
| 273 | if (isBindingHost(host)) { |
| 274 | const { accountId, region } = yield* currentEnv; |
| 275 | // Scope InvokeModel to the named models, or all foundation models + |
| 276 | // cross-region inference profiles when the caller names none. |
| 277 | const modelResources = |
| 278 | models.length > 0 |
| 279 | ? [ |
| 280 | ...new Set( |
| 281 | models.flatMap((id) => |
| 282 | bedrockModelArns(region, accountId, id), |
| 283 | ), |
| 284 | ), |
| 285 | ] |
| 286 | : [ |
| 287 | `arn:aws:bedrock:${region}::foundation-model/*`, |
| 288 | `arn:aws:bedrock:${region}:${accountId}:inference-profile/*`, |
| 289 | ]; |
| 290 | yield* host.bind`Allow(${host}, ${options.tag}(${knowledgeBase}))`({ |
| 291 | policyStatements: [ |
| 292 | { |
| 293 | Effect: "Allow", |
| 294 | Action: ["bedrock:Retrieve", "bedrock:RetrieveAndGenerate"], |
| 295 | Resource: [knowledgeBase.knowledgeBaseArn], |
| 296 | }, |
| 297 | { |
| 298 | Effect: "Allow", |
| 299 | Action: ["bedrock:InvokeModel"], |
| 300 | Resource: modelResources, |
| 301 | }, |
| 302 | ], |
| 303 | }); |
| 304 | } |
| 305 | } |
| 306 | return Effect.fn(`${options.tag}(${knowledgeBase.LogicalId})`)(function* ( |
| 307 | request: I, |
| 308 | ) { |
| 309 | return yield* op(request); |
| 310 | }); |
| 311 | }); |
| 312 | }); |