1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import * as Output from "../../Output.ts";
4import { AWSEnvironment } from "../Environment.ts";
5import { isBindingHost } from "../Lambda/Function.ts";
6import type { AgentAlias } from "./AgentAlias.ts";
7import type { DataSource } from "./DataSource.ts";
8import type { KnowledgeBase } from "./KnowledgeBase.ts";
9import { 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
22const 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 */
34export 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 */
94export 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 */
145export 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 */
204export 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 */
258export 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 });