1import * as aiplatform from "@distilled.cloud/gcp/aiplatform_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Schedule from "effect/Schedule";
5import * as Stream from "effect/Stream";
6import { Unowned } from "../../AdoptPolicy.ts";
7import { isResolved } from "../../Diff.ts";
8import * as Provider from "../../Provider.ts";
9import { Resource } from "../../Resource.ts";
10import { GcpEnvironment } from "../Environment.ts";
11import { createInternalLabels } from "../Labels.ts";
12import type { Providers } from "../Providers.ts";
13import { listLocations } from "./names.ts";
14import { resourceNameFromOperation, waitForOperation } from "./operations.ts";
15import {
16 encodeOwnership,
17 hasOwnershipMarker,
18 lastSegment,
19 locationOf,
20 locationParent,
21 ownedByAlchemy,
22 parseOwnership,
23 toResourceId,
24} from "./ownership.ts";
25
26export type McpTool = {
27 /**
28 * MCP server resource name
29 * (`projects/{project}/locations/{location}/mcpServers/{mcp_server}`).
30 */
31 mcpServer?: string;
32 /** MCP tool resource names. Omit to apply to every tool on the server. */
33 tools?: string[];
34};
35
36export type SemanticGovernancePolicyProps = {
37 /**
38 * Region (`us-central1`, …). Immutable — changing it replaces the
39 * policy.
40 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
41 */
42 location?: string;
43 /**
44 * Policy id (the `{semantic_governance_policy}` segment). If omitted, a
45 * unique id is generated. Immutable.
46 */
47 semanticGovernancePolicyId?: string;
48 /**
49 * User-facing display name.
50 */
51 displayName?: string;
52 /**
53 * Human-readable description. SemanticGovernancePolicy has no labels
54 * field, so Alchemy ownership is stored in a `[alchemy …]` prefix.
55 */
56 description?: string;
57 /**
58 * Agent Registry resource name affected by this policy.
59 */
60 agent: string;
61 /**
62 * Natural-language constraint enforced by the policy.
63 */
64 naturalLanguageConstraint: string;
65 /**
66 * Optional MCP tools the policy applies to.
67 */
68 mcpTools?: McpTool[];
69};
70
71export type SemanticGovernancePolicy = Resource<
72 "GCP.AIPlatform.SemanticGovernancePolicy",
73 SemanticGovernancePolicyProps,
74 {
75 /** Full resource name. */
76 name: string;
77 /** Policy id (last path segment). */
78 semanticGovernancePolicyId: string;
79 /** Region id. */
80 location: string;
81 /** Project id. */
82 project: string;
83 /** User-facing display name. */
84 displayName: string | undefined;
85 /** User description with the Alchemy ownership prefix stripped. */
86 description: string | undefined;
87 /** Agent Registry resource name. */
88 agent: string | undefined;
89 /** Natural-language constraint. */
90 naturalLanguageConstraint: string | undefined;
91 /** Agent identity used by the PDP. */
92 agentIdentity: string | undefined;
93 /** MCP tools covered by the policy. */
94 mcpTools: McpTool[];
95 /** RFC3339 creation timestamp. */
96 createTime: string | undefined;
97 /** RFC3339 last-update timestamp. */
98 updateTime: string | undefined;
99 },
100 never,
101 Providers
102>;
103
104/**
105 * A Vertex AI SemanticGovernancePolicy constraining an Agent's tools.
106 *
107 * Policies have no labels field — Alchemy stamps ownership into the
108 * description. Location and policy id are immutable.
109 *
110 * ### Creating a Policy
111 * **Example:** Natural-language constraint on an agent
112 * ```typescript
113 * const policy = yield* GCP.AIPlatform.SemanticGovernancePolicy("Safety", {
114 * agent: "projects/my-project/locations/us-central1/agents/support",
115 * naturalLanguageConstraint: "Never share customer PII.",
116 * });
117 * ```
118 *
119 * @resource
120 * @category AIPlatform
121 */
122export const SemanticGovernancePolicy = Resource<SemanticGovernancePolicy>(
123 "GCP.AIPlatform.SemanticGovernancePolicy",
124);
125
126export class SemanticGovernancePolicyNotResolved extends Data.TaggedError(
127 "GCP.AIPlatform.SemanticGovernancePolicyNotResolved",
128)<{
129 name: string;
130}> {}
131
132const resourceName = (parent: string, policyId: string) =>
133 `${parent}/semanticGovernancePolicies/${policyId}`;
134
135const toAttrs = (
136 policy: aiplatform.GoogleCloudAiplatformV1SemanticGovernancePolicy,
137 project: string,
138) => {
139 const name = policy.name ?? "";
140 const parsed = parseOwnership(policy.description);
141 return {
142 name,
143 semanticGovernancePolicyId: lastSegment(name),
144 location: locationOf(name),
145 project,
146 displayName: policy.displayName,
147 description: parsed.text,
148 agent: policy.agent,
149 naturalLanguageConstraint: policy.naturalLanguageConstraint,
150 agentIdentity: policy.agentIdentity,
151 mcpTools: (policy.mcpTools ?? []).map((tool) => ({
152 mcpServer: tool.mcpServer,
153 tools: tool.tools,
154 })),
155 createTime: policy.createTime,
156 updateTime: policy.updateTime,
157 };
158};
159
160const getByName = (name: string) =>
161 name.length === 0
162 ? Effect.succeed(undefined)
163 : aiplatform
164 .getProjectsLocationsSemanticGovernancePolicies({ name })
165 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
166
167const waitUntilExists = (name: string) =>
168 getByName(name).pipe(
169 Effect.flatMap((policy) =>
170 policy
171 ? Effect.succeed(policy)
172 : Effect.fail(new SemanticGovernancePolicyNotResolved({ name })),
173 ),
174 Effect.retry({
175 while: (error) =>
176 error._tag === "GCP.AIPlatform.SemanticGovernancePolicyNotResolved",
177 times: 8,
178 schedule: Schedule.spaced("2 seconds"),
179 }),
180 );
181
182const listAt = (parent: string, project: string) =>
183 aiplatform.listProjectsLocationsSemanticGovernancePolicies
184 .pages({ parent, pageSize: 100 })
185 .pipe(
186 Stream.flatMap((page) =>
187 Stream.fromIterable(page.semanticGovernancePolicies ?? []),
188 ),
189 Stream.filter((policy) => hasOwnershipMarker(policy.description)),
190 Stream.map((policy) => toAttrs(policy, project)),
191 Stream.runCollect,
192 Effect.map((chunk) => Array.from(chunk)),
193 Effect.catchTag("NotFound", () => Effect.succeed([])),
194 );
195
196const mcpKey = (tools: McpTool[] | undefined) =>
197 JSON.stringify(
198 (tools ?? []).map((tool) => ({
199 mcpServer: tool.mcpServer ?? "",
200 tools: [...(tool.tools ?? [])].sort(),
201 })),
202 );
203
204export const SemanticGovernancePolicyProvider = () =>
205 Provider.succeed(SemanticGovernancePolicy, {
206 stables: [
207 "name",
208 "semanticGovernancePolicyId",
209 "location",
210 "project",
211 "createTime",
212 ],
213
214 diff: Effect.fn(function* ({ news, olds, output }) {
215 if (!isResolved(news)) return undefined;
216 const previousLocation = olds?.location ?? output?.location;
217 const nextLocation = news.location ?? previousLocation;
218 if (previousLocation !== undefined && previousLocation !== nextLocation) {
219 return { action: "replace" as const, deleteFirst: false };
220 }
221 const previousId =
222 olds?.semanticGovernancePolicyId ?? output?.semanticGovernancePolicyId;
223 if (
224 previousId !== undefined &&
225 news.semanticGovernancePolicyId !== undefined &&
226 news.semanticGovernancePolicyId !== previousId
227 ) {
228 return { action: "replace" as const, deleteFirst: true };
229 }
230 return undefined;
231 }),
232
233 read: Effect.fn(function* ({ id, olds, output }) {
234 const env = yield* GcpEnvironment.current;
235 const policyId = yield* toResourceId(
236 id,
237 olds?.semanticGovernancePolicyId,
238 output?.semanticGovernancePolicyId,
239 );
240 const location = olds?.location ?? output?.location ?? env.region;
241 const name =
242 output?.name ??
243 resourceName(locationParent(env.project, location), policyId);
244 const existing = yield* getByName(name);
245 if (existing === undefined) return undefined;
246 const attrs = toAttrs(existing, env.project);
247 return (yield* ownedByAlchemy(id, existing.description))
248 ? attrs
249 : Unowned(attrs);
250 }),
251
252 list: () =>
253 Effect.gen(function* () {
254 const env = yield* GcpEnvironment.current;
255 return (yield* Effect.forEach(listLocations(env.region), (location) =>
256 listAt(locationParent(env.project, location), env.project),
257 )).flat();
258 }),
259
260 reconcile: Effect.fn(function* ({ id, news, output }) {
261 const env = yield* GcpEnvironment.current;
262 const location = news.location ?? output?.location ?? env.region;
263 const parent = locationParent(env.project, location);
264 const policyId = yield* toResourceId(
265 id,
266 news.semanticGovernancePolicyId,
267 output?.semanticGovernancePolicyId,
268 );
269 const name = resourceName(parent, policyId);
270 const ownership = yield* createInternalLabels(id);
271 const desiredDescription = encodeOwnership(ownership, news.description);
272 const displayName = news.displayName ?? policyId;
273 const mcpTools = news.mcpTools?.map((tool) => ({
274 mcpServer: tool.mcpServer,
275 tools: tool.tools,
276 }));
277
278 let current = yield* getByName(output?.name ?? name);
279
280 if (current === undefined) {
281 const created = yield* aiplatform
282 .createProjectsLocationsSemanticGovernancePolicies({
283 parent,
284 semanticGovernancePolicyId: policyId,
285 body: {
286 displayName,
287 description: desiredDescription,
288 agent: news.agent,
289 naturalLanguageConstraint: news.naturalLanguageConstraint,
290 mcpTools,
291 },
292 })
293 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
294 if (created !== undefined) {
295 const done = yield* waitForOperation(created, {
296 alreadyExistsOk: true,
297 });
298 const createdName = resourceNameFromOperation(done) ?? name;
299 current = yield* waitUntilExists(createdName);
300 } else {
301 current = yield* getByName(name);
302 }
303 }
304
305 if (current === undefined) {
306 return yield* new SemanticGovernancePolicyNotResolved({ name });
307 }
308
309 const displayChanged = (current.displayName ?? "") !== displayName;
310 const descriptionChanged =
311 (current.description ?? "") !== desiredDescription;
312 const agentChanged = (current.agent ?? "") !== news.agent;
313 const constraintChanged =
314 (current.naturalLanguageConstraint ?? "") !==
315 news.naturalLanguageConstraint;
316 const mcpChanged = mcpKey(current.mcpTools) !== mcpKey(news.mcpTools);
317
318 if (
319 displayChanged ||
320 descriptionChanged ||
321 agentChanged ||
322 constraintChanged ||
323 mcpChanged
324 ) {
325 const patched =
326 yield* aiplatform.patchProjectsLocationsSemanticGovernancePolicies({
327 name,
328 updateMask: [
329 displayChanged ? "displayName" : undefined,
330 descriptionChanged ? "description" : undefined,
331 agentChanged ? "agent" : undefined,
332 constraintChanged ? "naturalLanguageConstraint" : undefined,
333 mcpChanged ? "mcpTools" : undefined,
334 ]
335 .filter((field): field is string => field !== undefined)
336 .join(","),
337 body: {
338 name,
339 displayName,
340 description: desiredDescription,
341 agent: news.agent,
342 naturalLanguageConstraint: news.naturalLanguageConstraint,
343 mcpTools,
344 etag: current.etag,
345 },
346 });
347 yield* waitForOperation(patched);
348 current = yield* waitUntilExists(name);
349 }
350
351 return toAttrs(current, env.project);
352 }),
353
354 delete: Effect.fn(function* ({ output }) {
355 const existing = yield* getByName(output.name);
356 if (existing === undefined) return;
357 const operation = yield* aiplatform
358 .deleteProjectsLocationsSemanticGovernancePolicies({
359 name: output.name,
360 })
361 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
362 if (operation !== undefined) {
363 yield* waitForOperation(operation, { notFoundOk: true });
364 }
365 }),
366 });