History

add policy scope type, organization-level ownership, and interaction purpose

- Add scopeType (organization|workspace) and organizationId columns to policies - Make workspaceId nullable for organization-scoped policies - Add check constraints for scope consistency - Add organization-level index on policies table - Add purpose column to execution interactions table - Add PolicyScopeTypeSchema to schema models - Update persistence test to include new required fields - Include database migration for all schema changes

Rhys Sullivan committed 8 months ago

Commit b262ec01 parent 74ceb1c5 files · +54 −3Browse files

Unified · Split

5 files
  1. packages/control-plane/drizzle/20260309103000_policy_scope_and_interaction_purpose/migration.sql +17 −0
  2. packages/control-plane/src/persistence/control-plane-rows.test.ts +2 −0
  3. packages/control-plane/src/persistence/schema.ts +22 −1
  4. packages/control-plane/src/schema/models/execution.ts +1 −0
  5. packages/control-plane/src/schema/models/policy.ts +12 −2
packages/control-plane/drizzle/20260309103000_policy_scope_and_interaction_purpose/migration.sql+17−0
@@ -0,0 +1,17 @@
1ALTER TABLE "policies" ADD COLUMN "scope_type" text;--> statement-breakpoint
2ALTER TABLE "policies" ADD COLUMN "organization_id" text;--> statement-breakpoint
3UPDATE "policies" AS "pol"
4SET
5 "scope_type" = 'workspace',
6 "organization_id" = "ws"."organization_id"
7FROM "workspaces" AS "ws"
8WHERE "pol"."workspace_id" = "ws"."id";--> statement-breakpoint
9ALTER TABLE "policies" ALTER COLUMN "scope_type" SET NOT NULL;--> statement-breakpoint
10ALTER TABLE "policies" ALTER COLUMN "organization_id" SET NOT NULL;--> statement-breakpoint
11ALTER TABLE "policies" ALTER COLUMN "workspace_id" DROP NOT NULL;--> statement-breakpoint
12ALTER TABLE "policies" ADD CONSTRAINT "policies_scope_type_check" CHECK ("scope_type" in ('organization', 'workspace'));--> statement-breakpoint
13ALTER TABLE "policies" ADD CONSTRAINT "policies_scope_consistency_check" CHECK (("scope_type" = 'organization' and "workspace_id" is null) or ("scope_type" = 'workspace' and "workspace_id" is not null));--> statement-breakpoint
14DROP INDEX IF EXISTS "policies_workspace_priority_idx";--> statement-breakpoint
15CREATE INDEX "policies_organization_priority_idx" ON "policies" ("organization_id","priority" DESC,"updated_at","id");--> statement-breakpoint
16CREATE INDEX "policies_workspace_priority_idx" ON "policies" ("workspace_id","priority" DESC,"updated_at","id");--> statement-breakpoint
17ALTER TABLE "execution_interactions" ADD COLUMN "purpose" text NOT NULL DEFAULT 'elicitation';
packages/control-plane/src/persistence/control-plane-rows.test.ts+2−0
@@ -196,6 +196,8 @@ describe("control-plane-persistence-drizzle", () => { · 195 unmodified lines
196 196
197 yield* persistence.rows.policies.insert({197 yield* persistence.rows.policies.insert({
198 id: PolicyIdSchema.make("pol_1"),198 id: PolicyIdSchema.make("pol_1"),
199 scopeType: "workspace",
200 organizationId,
199 workspaceId,201 workspaceId,
200 targetAccountId: null,202 targetAccountId: null,
201 clientId: null,203 clientId: null,
packages/control-plane/src/persistence/schema.ts+22−1
@@ -428,7 +428,9 @@ export const policiesTable = pgTable( · 427 unmodified lines
428 tableNames.policies,428 tableNames.policies,
429 {429 {
430 id: text("id").notNull().primaryKey(),430 id: text("id").notNull().primaryKey(),
431 workspaceId: text("workspace_id").notNull(),431 scopeType: text("scope_type").notNull(),
432 organizationId: text("organization_id").notNull(),
433 workspaceId: text("workspace_id"),
432 targetAccountId: text("target_account_id"),434 targetAccountId: text("target_account_id"),
433 clientId: text("client_id"),435 clientId: text("client_id"),
434 resourceType: text("resource_type").notNull(),436 resourceType: text("resource_type").notNull(),
@@ -443,12 +445,30 @@ export const policiesTable = pgTable( · 8 unmodified lines
443 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),445 updatedAt: bigint("updated_at", { mode: "number" }).notNull(),
444 },446 },
445 (table) => [447 (table) => [
448 index("policies_organization_priority_idx").on(
449 table.organizationId,
450 table.priority.desc(),
451 table.updatedAt,
452 table.id,
453 ),
446 index("policies_workspace_priority_idx").on(454 index("policies_workspace_priority_idx").on(
447 table.workspaceId,455 table.workspaceId,
448 table.priority.desc(),456 table.priority.desc(),
449 table.updatedAt,457 table.updatedAt,
450 table.id,458 table.id,
451 ),459 ),
460 check(
461 "policies_scope_type_check",
462 sql`${table.scopeType} in ('organization', 'workspace')`,
463 ),
464 check(
465 "policies_scope_consistency_check",
466 sql`(
467 ${table.scopeType} = 'organization' and ${table.workspaceId} is null
468 ) or (
469 ${table.scopeType} = 'workspace' and ${table.workspaceId} is not null
470 )`,
471 ),
452 check(472 check(
453 "policies_resource_type_check",473 "policies_resource_type_check",
454 sql`${table.resourceType} in ('all_tools', 'source', 'namespace', 'tool_path')`,474 sql`${table.resourceType} in ('all_tools', 'source', 'namespace', 'tool_path')`,
@@ -516,6 +536,7 @@ export const executionInteractionsTable = pgTable( · 61 unmodified lines
516 executionId: text("execution_id").notNull(),536 executionId: text("execution_id").notNull(),
517 status: text("status").notNull(),537 status: text("status").notNull(),
518 kind: text("kind").notNull(),538 kind: text("kind").notNull(),
539 purpose: text("purpose").notNull(),
519 payloadJson: text("payload_json").notNull(),540 payloadJson: text("payload_json").notNull(),
520 responseJson: text("response_json"),541 responseJson: text("response_json"),
521 createdAt: bigint("created_at", { mode: "number" }).notNull(),542 createdAt: bigint("created_at", { mode: "number" }).notNull(),
packages/control-plane/src/schema/models/execution.ts+1−0
@@ -65,6 +65,7 @@ const executionInteractionSchemaOverrides = { · 64 unmodified lines
65 id: ExecutionInteractionIdSchema,65 id: ExecutionInteractionIdSchema,
66 executionId: ExecutionIdSchema,66 executionId: ExecutionIdSchema,
67 status: ExecutionInteractionStatusSchema,67 status: ExecutionInteractionStatusSchema,
68 purpose: Schema.String,
68 responseJson: Schema.NullOr(Schema.String),69 responseJson: Schema.NullOr(Schema.String),
69 createdAt: TimestampMsSchema,70 createdAt: TimestampMsSchema,
70 updatedAt: TimestampMsSchema,71 updatedAt: TimestampMsSchema,
packages/control-plane/src/schema/models/policy.ts+12−2
@@ -7,7 +7,14 @@ import { Schema } from "effect"; · 6 unmodified lines
7 7
8import { policiesTable } from "../../persistence/schema";8import { policiesTable } from "../../persistence/schema";
9import { TimestampMsSchema } from "../common";9import { TimestampMsSchema } from "../common";
10import { AccountIdSchema, PolicyIdSchema, WorkspaceIdSchema } from "../ids";10import {
11 AccountIdSchema,
12 OrganizationIdSchema,
13 PolicyIdSchema,
14 WorkspaceIdSchema,
15} from "../ids";
16
17export const PolicyScopeTypeSchema = Schema.Literal("organization", "workspace");
11 18
12export const PolicyResourceTypeSchema = Schema.Literal(19export const PolicyResourceTypeSchema = Schema.Literal(
13 "all_tools",20 "all_tools",
@@ -35,7 +42,9 @@ export const PolicyArgumentConditionSchema = Schema.Struct({ · 21 unmodified lines
35 42
36const policySchemaOverrides = {43const policySchemaOverrides = {
37 id: PolicyIdSchema,44 id: PolicyIdSchema,
38 workspaceId: WorkspaceIdSchema,45 scopeType: PolicyScopeTypeSchema,
46 organizationId: OrganizationIdSchema,
47 workspaceId: Schema.NullOr(WorkspaceIdSchema),
39 targetAccountId: Schema.NullOr(AccountIdSchema),48 targetAccountId: Schema.NullOr(AccountIdSchema),
40 clientId: Schema.NullOr(Schema.String),49 clientId: Schema.NullOr(Schema.String),
41 resourceType: PolicyResourceTypeSchema,50 resourceType: PolicyResourceTypeSchema,
@@ -63,6 +72,7 @@ export type PolicyResourceType = typeof PolicyResourceTypeSchema.Type; · 21 unmodified lines
63export type PolicyMatchType = typeof PolicyMatchTypeSchema.Type;72export type PolicyMatchType = typeof PolicyMatchTypeSchema.Type;
64export type PolicyEffect = typeof PolicyEffectSchema.Type;73export type PolicyEffect = typeof PolicyEffectSchema.Type;
65export type PolicyApprovalMode = typeof PolicyApprovalModeSchema.Type;74export type PolicyApprovalMode = typeof PolicyApprovalModeSchema.Type;
75export type PolicyScopeType = typeof PolicyScopeTypeSchema.Type;
66export type ArgumentConditionOperator =76export type ArgumentConditionOperator =
67 typeof ArgumentConditionOperatorSchema.Type;77 typeof ArgumentConditionOperatorSchema.Type;
68export type PolicyArgumentCondition = typeof PolicyArgumentConditionSchema.Type;78export type PolicyArgumentCondition = typeof PolicyArgumentConditionSchema.Type;