mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 10:26:26 +01:00
Add user-specific secret contracts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
ac9a883f8b
commit
9346994b27
28 files changed
+629
-14
No files matched your search
@@ -0,0 +1,178 @@
|
||||
CREATE TABLE IF NOT EXISTS "user_secret_definitions" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"key" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"description" text,
|
||||
"status" text DEFAULT 'active' NOT NULL,
|
||||
"provider" text DEFAULT 'local_encrypted' NOT NULL,
|
||||
"managed_mode" text DEFAULT 'paperclip_managed' NOT NULL,
|
||||
"provider_config_id" uuid,
|
||||
"provider_metadata" jsonb,
|
||||
"usage_guidance" text,
|
||||
"created_by_agent_id" uuid,
|
||||
"created_by_user_id" text,
|
||||
"updated_by_agent_id" uuid,
|
||||
"updated_by_user_id" text,
|
||||
"deleted_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "user_secret_declarations" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"user_secret_definition_id" uuid NOT NULL,
|
||||
"target_type" text NOT NULL,
|
||||
"target_id" text NOT NULL,
|
||||
"config_path" text NOT NULL,
|
||||
"env_key" text NOT NULL,
|
||||
"version_selector" text DEFAULT 'latest' NOT NULL,
|
||||
"required" boolean DEFAULT true NOT NULL,
|
||||
"allow_missing_override" boolean DEFAULT false NOT NULL,
|
||||
"label" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "company_secrets" ADD COLUMN IF NOT EXISTS "scope" text DEFAULT 'company' NOT NULL;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "company_secrets" ADD COLUMN IF NOT EXISTS "owner_user_id" text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "company_secrets" ADD COLUMN IF NOT EXISTS "user_secret_definition_id" uuid;
|
||||
--> statement-breakpoint
|
||||
UPDATE "company_secrets"
|
||||
SET "scope" = 'company'
|
||||
WHERE "scope" IS NULL;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "secret_access_events" ALTER COLUMN "secret_id" DROP NOT NULL;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "secret_access_events" ADD COLUMN IF NOT EXISTS "user_secret_definition_id" uuid;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "secret_access_events" ADD COLUMN IF NOT EXISTS "secret_scope" text DEFAULT 'company' NOT NULL;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "secret_access_events" ADD COLUMN IF NOT EXISTS "responsible_user_id" text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "secret_access_events" ADD COLUMN IF NOT EXISTS "credential_owner_user_id" text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "secret_access_events" ADD COLUMN IF NOT EXISTS "credential_subject_type" text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "secret_access_events" ADD COLUMN IF NOT EXISTS "credential_subject_id" text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "issues" ADD COLUMN IF NOT EXISTS "responsible_user_id" text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "responsible_user_id" text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "routines" ADD COLUMN IF NOT EXISTS "responsible_user_id" text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "routine_revisions" ADD COLUMN IF NOT EXISTS "responsible_user_id" text;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "routine_runs" ADD COLUMN IF NOT EXISTS "responsible_user_id" text;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_secret_definitions_company_id_companies_id_fk') THEN
|
||||
ALTER TABLE "user_secret_definitions" ADD CONSTRAINT "user_secret_definitions_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_secret_definitions_provider_config_id_company_secret_provider_configs_id_fk') THEN
|
||||
ALTER TABLE "user_secret_definitions" ADD CONSTRAINT "user_secret_definitions_provider_config_id_company_secret_provider_configs_id_fk" FOREIGN KEY ("provider_config_id") REFERENCES "public"."company_secret_provider_configs"("id") ON DELETE set null ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_secret_definitions_created_by_agent_id_agents_id_fk') THEN
|
||||
ALTER TABLE "user_secret_definitions" ADD CONSTRAINT "user_secret_definitions_created_by_agent_id_agents_id_fk" FOREIGN KEY ("created_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_secret_definitions_updated_by_agent_id_agents_id_fk') THEN
|
||||
ALTER TABLE "user_secret_definitions" ADD CONSTRAINT "user_secret_definitions_updated_by_agent_id_agents_id_fk" FOREIGN KEY ("updated_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_secret_declarations_company_id_companies_id_fk') THEN
|
||||
ALTER TABLE "user_secret_declarations" ADD CONSTRAINT "user_secret_declarations_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_secret_declarations_user_secret_definition_id_user_secret_definitions_id_fk') THEN
|
||||
ALTER TABLE "user_secret_declarations" ADD CONSTRAINT "user_secret_declarations_user_secret_definition_id_user_secret_definitions_id_fk" FOREIGN KEY ("user_secret_definition_id") REFERENCES "public"."user_secret_definitions"("id") ON DELETE cascade ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'company_secrets_user_secret_definition_id_user_secret_definitions_id_fk') THEN
|
||||
ALTER TABLE "company_secrets" ADD CONSTRAINT "company_secrets_user_secret_definition_id_user_secret_definitions_id_fk" FOREIGN KEY ("user_secret_definition_id") REFERENCES "public"."user_secret_definitions"("id") ON DELETE set null ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'secret_access_events_user_secret_definition_id_user_secret_definitions_id_fk') THEN
|
||||
ALTER TABLE "secret_access_events" ADD CONSTRAINT "secret_access_events_user_secret_definition_id_user_secret_definitions_id_fk" FOREIGN KEY ("user_secret_definition_id") REFERENCES "public"."user_secret_definitions"("id") ON DELETE set null ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'company_secrets_scope_shape_check') THEN
|
||||
ALTER TABLE "company_secrets" ADD CONSTRAINT "company_secrets_scope_shape_check" CHECK (
|
||||
("scope" = 'company' AND "owner_user_id" IS NULL AND "user_secret_definition_id" IS NULL)
|
||||
OR
|
||||
("scope" = 'user' AND "owner_user_id" IS NOT NULL AND "user_secret_definition_id" IS NOT NULL)
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "company_secrets_company_name_uq";
|
||||
--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "company_secrets_company_key_uq";
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "user_secret_definitions_company_status_idx" ON "user_secret_definitions" USING btree ("company_id","status");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "user_secret_definitions_company_provider_idx" ON "user_secret_definitions" USING btree ("company_id","provider");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "user_secret_definitions_provider_config_idx" ON "user_secret_definitions" USING btree ("provider_config_id");
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "user_secret_definitions_company_key_uq" ON "user_secret_definitions" USING btree ("company_id","key") WHERE "user_secret_definitions"."deleted_at" IS NULL;
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "user_secret_declarations_company_idx" ON "user_secret_declarations" USING btree ("company_id");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "user_secret_declarations_definition_idx" ON "user_secret_declarations" USING btree ("user_secret_definition_id");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "user_secret_declarations_target_idx" ON "user_secret_declarations" USING btree ("company_id","target_type","target_id");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "user_secret_declarations_company_required_idx" ON "user_secret_declarations" USING btree ("company_id","required");
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "user_secret_declarations_target_path_uq" ON "user_secret_declarations" USING btree ("company_id","target_type","target_id","config_path");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "user_secret_declarations_required_override_idx" ON "user_secret_declarations" USING btree ("company_id","allow_missing_override") WHERE "user_secret_declarations"."allow_missing_override" = true;
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "company_secrets_company_scope_idx" ON "company_secrets" USING btree ("company_id","scope");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "company_secrets_company_owner_idx" ON "company_secrets" USING btree ("company_id","owner_user_id");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "company_secrets_user_definition_owner_idx" ON "company_secrets" USING btree ("company_id","user_secret_definition_id","owner_user_id");
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "company_secrets_company_name_uq" ON "company_secrets" USING btree ("company_id","name") WHERE "company_secrets"."scope" = 'company' AND "company_secrets"."deleted_at" IS NULL;
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "company_secrets_company_key_uq" ON "company_secrets" USING btree ("company_id","key") WHERE "company_secrets"."scope" = 'company' AND "company_secrets"."deleted_at" IS NULL;
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "company_secrets_user_definition_owner_uq" ON "company_secrets" USING btree ("company_id","user_secret_definition_id","owner_user_id") WHERE "company_secrets"."scope" = 'user' AND "company_secrets"."deleted_at" IS NULL;
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "secret_access_events_user_definition_created_idx" ON "secret_access_events" USING btree ("user_secret_definition_id","created_at");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "secret_access_events_company_credential_owner_idx" ON "secret_access_events" USING btree ("company_id","credential_owner_user_id","created_at");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "issues_company_responsible_user_idx" ON "issues" USING btree ("company_id","responsible_user_id");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "heartbeat_runs_company_responsible_user_idx" ON "heartbeat_runs" USING btree ("company_id","responsible_user_id","created_at");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "routines_company_responsible_user_idx" ON "routines" USING btree ("company_id","responsible_user_id");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "routine_revisions_company_responsible_user_idx" ON "routine_revisions" USING btree ("company_id","responsible_user_id","created_at");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "routine_runs_company_responsible_user_idx" ON "routine_runs" USING btree ("company_id","responsible_user_id","created_at");
|
||||
@@ -883,6 +883,13 @@
|
||||
"when": 1782440100000,
|
||||
"tag": "0125_environment_custom_image_templates",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 126,
|
||||
"version": "7",
|
||||
"when": 1782923938661,
|
||||
"tag": "0126_user_specific_secrets",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
import { pgTable, uuid, text, timestamp, integer, jsonb, index, uniqueIndex } from "drizzle-orm/pg-core";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { check, pgTable, uuid, text, timestamp, integer, jsonb, index, uniqueIndex } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { agents } from "./agents.js";
|
||||
import { companySecretProviderConfigs } from "./company_secret_provider_configs.js";
|
||||
import { userSecretDefinitions } from "./user_secret_definitions.js";
|
||||
|
||||
export const companySecrets = pgTable(
|
||||
"company_secrets",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
||||
scope: text("scope").notNull().default("company"),
|
||||
ownerUserId: text("owner_user_id"),
|
||||
userSecretDefinitionId: uuid("user_secret_definition_id").references(() => userSecretDefinitions.id, { onDelete: "set null" }),
|
||||
key: text("key").notNull(),
|
||||
name: text("name").notNull(),
|
||||
provider: text("provider").notNull().default("local_encrypted"),
|
||||
@@ -28,9 +33,35 @@ export const companySecrets = pgTable(
|
||||
},
|
||||
(table) => ({
|
||||
companyIdx: index("company_secrets_company_idx").on(table.companyId),
|
||||
companyScopeIdx: index("company_secrets_company_scope_idx").on(table.companyId, table.scope),
|
||||
companyOwnerIdx: index("company_secrets_company_owner_idx").on(table.companyId, table.ownerUserId),
|
||||
userDefinitionOwnerIdx: index("company_secrets_user_definition_owner_idx").on(
|
||||
table.companyId,
|
||||
table.userSecretDefinitionId,
|
||||
table.ownerUserId,
|
||||
),
|
||||
companyProviderIdx: index("company_secrets_company_provider_idx").on(table.companyId, table.provider),
|
||||
providerConfigIdx: index("company_secrets_provider_config_idx").on(table.providerConfigId),
|
||||
companyNameUq: uniqueIndex("company_secrets_company_name_uq").on(table.companyId, table.name),
|
||||
companyKeyUq: uniqueIndex("company_secrets_company_key_uq").on(table.companyId, table.key),
|
||||
companyNameUq: uniqueIndex("company_secrets_company_name_uq")
|
||||
.on(table.companyId, table.name)
|
||||
.where(sql`${table.scope} = 'company' and ${table.deletedAt} is null`),
|
||||
companyKeyUq: uniqueIndex("company_secrets_company_key_uq")
|
||||
.on(table.companyId, table.key)
|
||||
.where(sql`${table.scope} = 'company' and ${table.deletedAt} is null`),
|
||||
userDefinitionOwnerUq: uniqueIndex("company_secrets_user_definition_owner_uq")
|
||||
.on(table.companyId, table.userSecretDefinitionId, table.ownerUserId)
|
||||
.where(sql`${table.scope} = 'user' and ${table.deletedAt} is null`),
|
||||
scopeShapeCheck: check(
|
||||
"company_secrets_scope_shape_check",
|
||||
sql`(
|
||||
${table.scope} = 'company'
|
||||
and ${table.ownerUserId} is null
|
||||
and ${table.userSecretDefinitionId} is null
|
||||
) or (
|
||||
${table.scope} = 'user'
|
||||
and ${table.ownerUserId} is not null
|
||||
and ${table.userSecretDefinitionId} is not null
|
||||
)`,
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -12,6 +12,7 @@ export const heartbeatRuns = pgTable(
|
||||
invocationSource: text("invocation_source").notNull().default("on_demand"),
|
||||
triggerDetail: text("trigger_detail"),
|
||||
status: text("status").notNull().default("queued"),
|
||||
responsibleUserId: text("responsible_user_id"),
|
||||
startedAt: timestamp("started_at", { withTimezone: true }),
|
||||
finishedAt: timestamp("finished_at", { withTimezone: true }),
|
||||
error: text("error"),
|
||||
@@ -63,6 +64,11 @@ export const heartbeatRuns = pgTable(
|
||||
table.agentId,
|
||||
table.startedAt,
|
||||
),
|
||||
companyResponsibleUserIdx: index("heartbeat_runs_company_responsible_user_idx").on(
|
||||
table.companyId,
|
||||
table.responsibleUserId,
|
||||
table.createdAt,
|
||||
),
|
||||
companyLivenessIdx: index("heartbeat_runs_company_liveness_idx").on(
|
||||
table.companyId,
|
||||
table.livenessState,
|
||||
|
||||
@@ -84,9 +84,11 @@ export { approvals } from "./approvals.js";
|
||||
export { approvalComments } from "./approval_comments.js";
|
||||
export { activityLog } from "./activity_log.js";
|
||||
export { companySecretProviderConfigs } from "./company_secret_provider_configs.js";
|
||||
export { userSecretDefinitions } from "./user_secret_definitions.js";
|
||||
export { companySecrets } from "./company_secrets.js";
|
||||
export { companySecretVersions } from "./company_secret_versions.js";
|
||||
export { companySecretBindings } from "./company_secret_bindings.js";
|
||||
export { userSecretDeclarations } from "./user_secret_declarations.js";
|
||||
export { secretAccessEvents } from "./secret_access_events.js";
|
||||
export { companySkills, companySkillVersions, companySkillStars, companySkillComments } from "./company_skills.js";
|
||||
export { plugins } from "./plugins.js";
|
||||
|
||||
@@ -41,6 +41,7 @@ export const issues = pgTable(
|
||||
executionLockedAt: timestamp("execution_locked_at", { withTimezone: true }),
|
||||
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id),
|
||||
createdByUserId: text("created_by_user_id"),
|
||||
responsibleUserId: text("responsible_user_id"),
|
||||
issueNumber: integer("issue_number"),
|
||||
identifier: text("identifier"),
|
||||
originKind: text("origin_kind").notNull().default("manual"),
|
||||
@@ -82,6 +83,7 @@ export const issues = pgTable(
|
||||
table.assigneeUserId,
|
||||
table.status,
|
||||
),
|
||||
responsibleUserIdx: index("issues_company_responsible_user_idx").on(table.companyId, table.responsibleUserId),
|
||||
parentIdx: index("issues_company_parent_idx").on(table.companyId, table.parentId),
|
||||
projectIdx: index("issues_company_project_idx").on(table.companyId, table.projectId),
|
||||
originIdx: index("issues_company_origin_idx").on(table.companyId, table.originKind, table.originId),
|
||||
|
||||
@@ -42,6 +42,7 @@ export const routines = pgTable(
|
||||
latestRevisionNumber: integer("latest_revision_number").notNull().default(1),
|
||||
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||
createdByUserId: text("created_by_user_id"),
|
||||
responsibleUserId: text("responsible_user_id"),
|
||||
updatedByAgentId: uuid("updated_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||
updatedByUserId: text("updated_by_user_id"),
|
||||
lastTriggeredAt: timestamp("last_triggered_at", { withTimezone: true }),
|
||||
@@ -53,6 +54,7 @@ export const routines = pgTable(
|
||||
companyStatusIdx: index("routines_company_status_idx").on(table.companyId, table.status),
|
||||
companyAssigneeIdx: index("routines_company_assignee_idx").on(table.companyId, table.assigneeAgentId),
|
||||
companyProjectIdx: index("routines_company_project_idx").on(table.companyId, table.projectId),
|
||||
companyResponsibleUserIdx: index("routines_company_responsible_user_idx").on(table.companyId, table.responsibleUserId),
|
||||
companyOriginIdx: index("routines_company_origin_idx").on(table.companyId, table.originKind, table.originId),
|
||||
}),
|
||||
);
|
||||
@@ -75,6 +77,7 @@ export const routineRevisions = pgTable(
|
||||
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||
createdByUserId: text("created_by_user_id"),
|
||||
createdByRunId: uuid("created_by_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
|
||||
responsibleUserId: text("responsible_user_id"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
@@ -87,6 +90,11 @@ export const routineRevisions = pgTable(
|
||||
table.routineId,
|
||||
table.createdAt,
|
||||
),
|
||||
companyResponsibleUserIdx: index("routine_revisions_company_responsible_user_idx").on(
|
||||
table.companyId,
|
||||
table.responsibleUserId,
|
||||
table.createdAt,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -136,6 +144,7 @@ export const routineRuns = pgTable(
|
||||
status: text("status").notNull().default("received"),
|
||||
triggeredAt: timestamp("triggered_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
routineRevisionId: uuid("routine_revision_id").references(() => routineRevisions.id, { onDelete: "set null" }),
|
||||
responsibleUserId: text("responsible_user_id"),
|
||||
idempotencyKey: text("idempotency_key"),
|
||||
triggerPayload: jsonb("trigger_payload").$type<Record<string, unknown>>(),
|
||||
dispatchFingerprint: text("dispatch_fingerprint"),
|
||||
@@ -149,6 +158,11 @@ export const routineRuns = pgTable(
|
||||
(table) => ({
|
||||
companyRoutineIdx: index("routine_runs_company_routine_idx").on(table.companyId, table.routineId, table.createdAt),
|
||||
routineRevisionIdx: index("routine_runs_revision_idx").on(table.routineRevisionId),
|
||||
companyResponsibleUserIdx: index("routine_runs_company_responsible_user_idx").on(
|
||||
table.companyId,
|
||||
table.responsibleUserId,
|
||||
table.createdAt,
|
||||
),
|
||||
triggerIdx: index("routine_runs_trigger_idx").on(table.triggerId, table.createdAt),
|
||||
dispatchFingerprintIdx: index("routine_runs_dispatch_fingerprint_idx").on(table.routineId, table.dispatchFingerprint),
|
||||
linkedIssueIdx: index("routine_runs_linked_issue_idx").on(table.linkedIssueId),
|
||||
|
||||
@@ -4,15 +4,22 @@ import { companySecrets } from "./company_secrets.js";
|
||||
import { heartbeatRuns } from "./heartbeat_runs.js";
|
||||
import { issues } from "./issues.js";
|
||||
import { plugins } from "./plugins.js";
|
||||
import { userSecretDefinitions } from "./user_secret_definitions.js";
|
||||
|
||||
export const secretAccessEvents = pgTable(
|
||||
"secret_access_events",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
||||
secretId: uuid("secret_id").notNull().references(() => companySecrets.id, { onDelete: "cascade" }),
|
||||
secretId: uuid("secret_id").references(() => companySecrets.id, { onDelete: "cascade" }),
|
||||
userSecretDefinitionId: uuid("user_secret_definition_id").references(() => userSecretDefinitions.id, { onDelete: "set null" }),
|
||||
secretScope: text("secret_scope").notNull().default("company"),
|
||||
version: integer("version"),
|
||||
provider: text("provider").notNull(),
|
||||
responsibleUserId: text("responsible_user_id"),
|
||||
credentialOwnerUserId: text("credential_owner_user_id"),
|
||||
credentialSubjectType: text("credential_subject_type"),
|
||||
credentialSubjectId: text("credential_subject_id"),
|
||||
actorType: text("actor_type").notNull(),
|
||||
actorId: text("actor_id"),
|
||||
consumerType: text("consumer_type").notNull(),
|
||||
@@ -28,6 +35,15 @@ export const secretAccessEvents = pgTable(
|
||||
(table) => ({
|
||||
companyCreatedIdx: index("secret_access_events_company_created_idx").on(table.companyId, table.createdAt),
|
||||
secretCreatedIdx: index("secret_access_events_secret_created_idx").on(table.secretId, table.createdAt),
|
||||
userDefinitionCreatedIdx: index("secret_access_events_user_definition_created_idx").on(
|
||||
table.userSecretDefinitionId,
|
||||
table.createdAt,
|
||||
),
|
||||
credentialOwnerIdx: index("secret_access_events_company_credential_owner_idx").on(
|
||||
table.companyId,
|
||||
table.credentialOwnerUserId,
|
||||
table.createdAt,
|
||||
),
|
||||
consumerIdx: index("secret_access_events_consumer_idx").on(table.companyId, table.consumerType, table.consumerId),
|
||||
runIdx: index("secret_access_events_run_idx").on(table.heartbeatRunId),
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { boolean, index, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { userSecretDefinitions } from "./user_secret_definitions.js";
|
||||
|
||||
export const userSecretDeclarations = pgTable(
|
||||
"user_secret_declarations",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
userSecretDefinitionId: uuid("user_secret_definition_id")
|
||||
.notNull()
|
||||
.references(() => userSecretDefinitions.id, { onDelete: "cascade" }),
|
||||
targetType: text("target_type").notNull(),
|
||||
targetId: text("target_id").notNull(),
|
||||
configPath: text("config_path").notNull(),
|
||||
envKey: text("env_key").notNull(),
|
||||
versionSelector: text("version_selector").notNull().default("latest"),
|
||||
required: boolean("required").notNull().default(true),
|
||||
allowMissingOverride: boolean("allow_missing_override").notNull().default(false),
|
||||
label: text("label"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyIdx: index("user_secret_declarations_company_idx").on(table.companyId),
|
||||
definitionIdx: index("user_secret_declarations_definition_idx").on(table.userSecretDefinitionId),
|
||||
targetIdx: index("user_secret_declarations_target_idx").on(table.companyId, table.targetType, table.targetId),
|
||||
companyRequiredIdx: index("user_secret_declarations_company_required_idx").on(table.companyId, table.required),
|
||||
targetPathUq: uniqueIndex("user_secret_declarations_target_path_uq").on(
|
||||
table.companyId,
|
||||
table.targetType,
|
||||
table.targetId,
|
||||
table.configPath,
|
||||
),
|
||||
requiredOverrideCheck: index("user_secret_declarations_required_override_idx")
|
||||
.on(table.companyId, table.allowMissingOverride)
|
||||
.where(sql`${table.allowMissingOverride} = true`),
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,37 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { index, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
|
||||
import { agents } from "./agents.js";
|
||||
import { companies } from "./companies.js";
|
||||
import { companySecretProviderConfigs } from "./company_secret_provider_configs.js";
|
||||
|
||||
export const userSecretDefinitions = pgTable(
|
||||
"user_secret_definitions",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
key: text("key").notNull(),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
status: text("status").notNull().default("active"),
|
||||
provider: text("provider").notNull().default("local_encrypted"),
|
||||
managedMode: text("managed_mode").notNull().default("paperclip_managed"),
|
||||
providerConfigId: uuid("provider_config_id").references(() => companySecretProviderConfigs.id, { onDelete: "set null" }),
|
||||
providerMetadata: jsonb("provider_metadata").$type<Record<string, unknown>>(),
|
||||
usageGuidance: text("usage_guidance"),
|
||||
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||
createdByUserId: text("created_by_user_id"),
|
||||
updatedByAgentId: uuid("updated_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||
updatedByUserId: text("updated_by_user_id"),
|
||||
deletedAt: timestamp("deleted_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyStatusIdx: index("user_secret_definitions_company_status_idx").on(table.companyId, table.status),
|
||||
companyProviderIdx: index("user_secret_definitions_company_provider_idx").on(table.companyId, table.provider),
|
||||
providerConfigIdx: index("user_secret_definitions_provider_config_idx").on(table.providerConfigId),
|
||||
companyKeyUq: uniqueIndex("user_secret_definitions_company_key_uq")
|
||||
.on(table.companyId, table.key)
|
||||
.where(sql`${table.deletedAt} is null`),
|
||||
}),
|
||||
);
|
||||
@@ -1574,6 +1574,7 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
|
||||
executionLockedAt: null,
|
||||
createdByAgentId: null,
|
||||
createdByUserId: null,
|
||||
responsibleUserId: null,
|
||||
issueNumber: null,
|
||||
identifier: null,
|
||||
originKind,
|
||||
|
||||
@@ -20,6 +20,11 @@ export const API = {
|
||||
goals: `${API_PREFIX}/goals`,
|
||||
approvals: `${API_PREFIX}/approvals`,
|
||||
secrets: `${API_PREFIX}/secrets`,
|
||||
userSecretDefinitions: `${API_PREFIX}/companies/:companyId/user-secret-definitions`,
|
||||
userSecretDefinition: `${API_PREFIX}/companies/:companyId/user-secret-definitions/:definitionId`,
|
||||
userSecretDefinitionCoverage: `${API_PREFIX}/companies/:companyId/user-secret-definitions/:definitionId/coverage`,
|
||||
myUserSecrets: `${API_PREFIX}/companies/:companyId/me/user-secrets`,
|
||||
myUserSecret: `${API_PREFIX}/companies/:companyId/me/user-secrets/:secretId`,
|
||||
secretProviderConfigs: `${API_PREFIX}/secret-provider-configs`,
|
||||
secretProviderConfigDiscoveryPreview: `${API_PREFIX}/companies/:companyId/secret-provider-configs/discovery/preview`,
|
||||
costs: `${API_PREFIX}/costs`,
|
||||
|
||||
@@ -618,6 +618,9 @@ export type SecretProviderConfigHealthStatus =
|
||||
export const SECRET_STATUSES = ["active", "disabled", "archived", "deleted"] as const;
|
||||
export type SecretStatus = (typeof SECRET_STATUSES)[number];
|
||||
|
||||
export const SECRET_SCOPES = ["company", "user"] as const;
|
||||
export type SecretScope = (typeof SECRET_SCOPES)[number];
|
||||
|
||||
export const SECRET_MANAGED_MODES = ["paperclip_managed", "external_reference"] as const;
|
||||
export type SecretManagedMode = (typeof SECRET_MANAGED_MODES)[number];
|
||||
|
||||
@@ -642,7 +645,15 @@ export const SECRET_BINDING_TARGET_TYPES = [
|
||||
] as const;
|
||||
export type SecretBindingTargetType = (typeof SECRET_BINDING_TARGET_TYPES)[number];
|
||||
|
||||
export const SECRET_ACCESS_OUTCOMES = ["success", "failure"] as const;
|
||||
export const SECRET_ACCESS_OUTCOMES = [
|
||||
"success",
|
||||
"failure",
|
||||
"missing",
|
||||
"inactive",
|
||||
"not_allowed",
|
||||
"optional_omitted",
|
||||
"provider_error",
|
||||
] as const;
|
||||
export type SecretAccessOutcome = (typeof SECRET_ACCESS_OUTCOMES)[number];
|
||||
|
||||
export const STORAGE_PROVIDERS = ["local_disk", "s3"] as const;
|
||||
|
||||
@@ -183,6 +183,7 @@ export {
|
||||
SECRET_PROVIDERS,
|
||||
SECRET_PROVIDER_CONFIG_STATUSES,
|
||||
SECRET_PROVIDER_CONFIG_HEALTH_STATUSES,
|
||||
SECRET_SCOPES,
|
||||
STORAGE_PROVIDERS,
|
||||
BILLING_TYPES,
|
||||
FINANCE_EVENT_KINDS,
|
||||
@@ -320,6 +321,7 @@ export {
|
||||
type SecretProvider,
|
||||
type SecretProviderConfigStatus,
|
||||
type SecretProviderConfigHealthStatus,
|
||||
type SecretScope,
|
||||
type StorageProvider,
|
||||
type BillingType,
|
||||
type FinanceEventKind,
|
||||
@@ -811,8 +813,12 @@ export type {
|
||||
EnvBinding,
|
||||
EnvPlainBinding,
|
||||
EnvSecretRefBinding,
|
||||
EnvUserSecretRefBinding,
|
||||
AgentEnvConfig,
|
||||
CompanySecret,
|
||||
UserSecretDefinition,
|
||||
UserSecretDeclaration,
|
||||
UserSecretCoverageSummary,
|
||||
CompanySecretProviderConfig,
|
||||
SecretProviderConfigPayload,
|
||||
SecretProviderConfigHealthDetails,
|
||||
@@ -1279,9 +1285,15 @@ export {
|
||||
type AddApprovalComment,
|
||||
envBindingPlainSchema,
|
||||
envBindingSecretRefSchema,
|
||||
envBindingUserSecretRefSchema,
|
||||
envBindingSchema,
|
||||
envConfigSchema,
|
||||
createSecretSchema,
|
||||
createUserSecretDefinitionSchema,
|
||||
updateUserSecretDefinitionSchema,
|
||||
createUserSecretValueSchema,
|
||||
updateUserSecretValueSchema,
|
||||
createUserSecretDeclarationSchema,
|
||||
createSecretProviderConfigSchema,
|
||||
updateSecretProviderConfigSchema,
|
||||
secretProviderConfigDiscoveryPreviewSchema,
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface HeartbeatRun {
|
||||
invocationSource: HeartbeatInvocationSource;
|
||||
triggerDetail: WakeupTriggerDetail | null;
|
||||
status: HeartbeatRunStatus;
|
||||
responsibleUserId: string | null;
|
||||
startedAt: Date | null;
|
||||
finishedAt: Date | null;
|
||||
error: string | null;
|
||||
|
||||
@@ -406,9 +406,13 @@ export type {
|
||||
SecretVersionSelector,
|
||||
EnvPlainBinding,
|
||||
EnvSecretRefBinding,
|
||||
EnvUserSecretRefBinding,
|
||||
EnvBinding,
|
||||
AgentEnvConfig,
|
||||
CompanySecret,
|
||||
UserSecretDefinition,
|
||||
UserSecretDeclaration,
|
||||
UserSecretCoverageSummary,
|
||||
CompanySecretProviderConfig,
|
||||
SecretProviderConfigPayload,
|
||||
SecretProviderConfigHealthDetails,
|
||||
@@ -432,6 +436,7 @@ export type {
|
||||
SecretAccessOutcome,
|
||||
SecretBindingTargetType,
|
||||
SecretManagedMode,
|
||||
SecretScope,
|
||||
SecretProviderDescriptor,
|
||||
SecretStatus,
|
||||
SecretVersionStatus,
|
||||
|
||||
@@ -550,6 +550,7 @@ export interface Issue {
|
||||
executionLockedAt: Date | null;
|
||||
createdByAgentId: string | null;
|
||||
createdByUserId: string | null;
|
||||
responsibleUserId: string | null;
|
||||
issueNumber: number | null;
|
||||
identifier: string | null;
|
||||
originKind?: IssueOriginKind;
|
||||
|
||||
@@ -88,6 +88,7 @@ export interface Routine {
|
||||
latestRevisionNumber: number;
|
||||
createdByAgentId: string | null;
|
||||
createdByUserId: string | null;
|
||||
responsibleUserId: string | null;
|
||||
updatedByAgentId: string | null;
|
||||
updatedByUserId: string | null;
|
||||
lastTriggeredAt: Date | null;
|
||||
@@ -126,6 +127,7 @@ export interface RoutineRevisionSnapshotRoutineV1 {
|
||||
originId?: string | null;
|
||||
variables: RoutineVariable[];
|
||||
env: RoutineEnvConfig | null;
|
||||
responsibleUserId: string | null;
|
||||
}
|
||||
|
||||
export interface RoutineRevisionSnapshotTriggerV1 {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
SecretProvider,
|
||||
SecretProviderConfigHealthStatus,
|
||||
SecretProviderConfigStatus,
|
||||
SecretScope,
|
||||
SecretStatus,
|
||||
SecretVersionStatus,
|
||||
} from "../constants.js";
|
||||
@@ -16,6 +17,7 @@ export type {
|
||||
SecretProvider,
|
||||
SecretProviderConfigHealthStatus,
|
||||
SecretProviderConfigStatus,
|
||||
SecretScope,
|
||||
SecretStatus,
|
||||
SecretVersionStatus,
|
||||
};
|
||||
@@ -33,14 +35,25 @@ export interface EnvSecretRefBinding {
|
||||
version?: SecretVersionSelector;
|
||||
}
|
||||
|
||||
export interface EnvUserSecretRefBinding {
|
||||
type: "user_secret_ref";
|
||||
key: string;
|
||||
version?: SecretVersionSelector;
|
||||
required?: boolean;
|
||||
allowMissingOverride?: boolean;
|
||||
}
|
||||
|
||||
// Backward-compatible: legacy plaintext string values are still accepted.
|
||||
export type EnvBinding = string | EnvPlainBinding | EnvSecretRefBinding;
|
||||
export type EnvBinding = string | EnvPlainBinding | EnvSecretRefBinding | EnvUserSecretRefBinding;
|
||||
|
||||
export type AgentEnvConfig = Record<string, EnvBinding>;
|
||||
|
||||
export interface CompanySecret {
|
||||
id: string;
|
||||
companyId: string;
|
||||
scope: SecretScope;
|
||||
ownerUserId: string | null;
|
||||
userSecretDefinitionId: string | null;
|
||||
key: string;
|
||||
name: string;
|
||||
provider: SecretProvider;
|
||||
@@ -61,6 +74,50 @@ export interface CompanySecret {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface UserSecretDefinition {
|
||||
id: string;
|
||||
companyId: string;
|
||||
key: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
status: SecretStatus;
|
||||
provider: SecretProvider;
|
||||
managedMode: SecretManagedMode;
|
||||
providerConfigId: string | null;
|
||||
providerMetadata: Record<string, unknown> | null;
|
||||
usageGuidance: string | null;
|
||||
createdByAgentId: string | null;
|
||||
createdByUserId: string | null;
|
||||
updatedByAgentId: string | null;
|
||||
updatedByUserId: string | null;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface UserSecretDeclaration {
|
||||
id: string;
|
||||
companyId: string;
|
||||
userSecretDefinitionId: string;
|
||||
targetType: SecretBindingTargetType;
|
||||
targetId: string;
|
||||
configPath: string;
|
||||
envKey: string;
|
||||
versionSelector: SecretVersionSelector;
|
||||
required: boolean;
|
||||
allowMissingOverride: boolean;
|
||||
label: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface UserSecretCoverageSummary {
|
||||
definitionId: string;
|
||||
configuredCount: number;
|
||||
missingCount: number;
|
||||
inactiveCount: number;
|
||||
}
|
||||
|
||||
export interface SecretProviderDescriptor {
|
||||
id: SecretProvider;
|
||||
label: string;
|
||||
@@ -216,9 +273,15 @@ export interface CompanySecretUsageBinding extends CompanySecretBinding {
|
||||
export interface SecretAccessEvent {
|
||||
id: string;
|
||||
companyId: string;
|
||||
secretId: string;
|
||||
secretId: string | null;
|
||||
userSecretDefinitionId: string | null;
|
||||
secretScope: SecretScope;
|
||||
version: number | null;
|
||||
provider: SecretProvider;
|
||||
responsibleUserId: string | null;
|
||||
credentialOwnerUserId: string | null;
|
||||
credentialSubjectType: string | null;
|
||||
credentialSubjectId: string | null;
|
||||
actorType: "agent" | "user" | "system" | "plugin";
|
||||
actorId: string | null;
|
||||
consumerType: SecretBindingTargetType;
|
||||
|
||||
@@ -463,9 +463,15 @@ export {
|
||||
export {
|
||||
envBindingPlainSchema,
|
||||
envBindingSecretRefSchema,
|
||||
envBindingUserSecretRefSchema,
|
||||
envBindingSchema,
|
||||
envConfigSchema,
|
||||
createSecretSchema,
|
||||
createUserSecretDefinitionSchema,
|
||||
updateUserSecretDefinitionSchema,
|
||||
createUserSecretValueSchema,
|
||||
updateUserSecretValueSchema,
|
||||
createUserSecretDeclarationSchema,
|
||||
createSecretProviderConfigSchema,
|
||||
updateSecretProviderConfigSchema,
|
||||
secretProviderConfigDiscoveryPreviewSchema,
|
||||
@@ -483,6 +489,11 @@ export {
|
||||
updateSecretSchema,
|
||||
type CreateSecretBinding,
|
||||
type CreateSecret,
|
||||
type CreateUserSecretDefinition,
|
||||
type UpdateUserSecretDefinition,
|
||||
type CreateUserSecretValue,
|
||||
type UpdateUserSecretValue,
|
||||
type CreateUserSecretDeclaration,
|
||||
type CreateSecretProviderConfig,
|
||||
type UpdateSecretProviderConfig,
|
||||
type SecretProviderConfigDiscoveryPreview,
|
||||
|
||||
@@ -96,6 +96,7 @@ export const routineRevisionSnapshotRoutineV1Schema = z.object({
|
||||
catchUpPolicy: z.enum(ROUTINE_CATCH_UP_POLICIES),
|
||||
variables: z.array(routineVariableSchema),
|
||||
env: envConfigSchema.nullable().default(null),
|
||||
responsibleUserId: z.string().nullable().default(null),
|
||||
}).strict();
|
||||
|
||||
export const routineRevisionSnapshotTriggerV1Schema = z.object({
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createUserSecretDeclarationSchema,
|
||||
createUserSecretValueSchema,
|
||||
createSecretProviderConfigSchema,
|
||||
createSecretSchema,
|
||||
envBindingUserSecretRefSchema,
|
||||
remoteSecretImportPreviewSchema,
|
||||
remoteSecretImportSchema,
|
||||
secretProviderConfigDiscoveryPreviewSchema,
|
||||
@@ -10,6 +13,42 @@ import {
|
||||
} from "./secret.js";
|
||||
|
||||
describe("secret validators", () => {
|
||||
it("defaults user secret refs to required and no missing override", () => {
|
||||
expect(
|
||||
envBindingUserSecretRefSchema.parse({
|
||||
type: "user_secret_ref",
|
||||
key: "github_api_token",
|
||||
}),
|
||||
).toEqual({
|
||||
type: "user_secret_ref",
|
||||
key: "github_api_token",
|
||||
required: true,
|
||||
allowMissingOverride: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("validates user secret declarations and current-user value payloads", () => {
|
||||
expect(
|
||||
createUserSecretDeclarationSchema.parse({
|
||||
targetType: "agent",
|
||||
targetId: "agent-1",
|
||||
configPath: "env.GITHUB_TOKEN",
|
||||
envKey: "GITHUB_TOKEN",
|
||||
definitionKey: "github_api_token",
|
||||
}),
|
||||
).toMatchObject({
|
||||
versionSelector: "latest",
|
||||
required: true,
|
||||
allowMissingOverride: false,
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
createUserSecretValueSchema.parse({
|
||||
value: "secret-value",
|
||||
}),
|
||||
).toThrow(/definitionId or definitionKey/);
|
||||
});
|
||||
|
||||
it("rejects externalRef on managed secrets", () => {
|
||||
expect(() =>
|
||||
createSecretSchema.parse({
|
||||
|
||||
@@ -7,6 +7,9 @@ import {
|
||||
SECRET_STATUSES,
|
||||
} from "../constants.js";
|
||||
|
||||
const secretKeySchema = z.string().trim().min(1).max(120).regex(/^[a-zA-Z0-9_.-]+$/);
|
||||
const secretVersionSelectorSchema = z.union([z.literal("latest"), z.number().int().positive()]);
|
||||
|
||||
export const envBindingPlainSchema = z.object({
|
||||
type: z.literal("plain"),
|
||||
value: z.string(),
|
||||
@@ -15,7 +18,15 @@ export const envBindingPlainSchema = z.object({
|
||||
export const envBindingSecretRefSchema = z.object({
|
||||
type: z.literal("secret_ref"),
|
||||
secretId: z.string().uuid(),
|
||||
version: z.union([z.literal("latest"), z.number().int().positive()]).optional(),
|
||||
version: secretVersionSelectorSchema.optional(),
|
||||
});
|
||||
|
||||
export const envBindingUserSecretRefSchema = z.object({
|
||||
type: z.literal("user_secret_ref"),
|
||||
key: secretKeySchema,
|
||||
version: secretVersionSelectorSchema.optional(),
|
||||
required: z.boolean().optional().default(true),
|
||||
allowMissingOverride: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
// Backward-compatible union that accepts legacy inline values.
|
||||
@@ -23,13 +34,14 @@ export const envBindingSchema = z.union([
|
||||
z.string(),
|
||||
envBindingPlainSchema,
|
||||
envBindingSecretRefSchema,
|
||||
envBindingUserSecretRefSchema,
|
||||
]);
|
||||
|
||||
export const envConfigSchema = z.record(z.string(), envBindingSchema);
|
||||
|
||||
export const createSecretSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
key: z.string().min(1).regex(/^[a-zA-Z0-9_.-]+$/).optional(),
|
||||
key: secretKeySchema.optional(),
|
||||
provider: z.enum(SECRET_PROVIDERS).optional(),
|
||||
providerConfigId: z.string().uuid().optional().nullable(),
|
||||
managedMode: z.enum(SECRET_MANAGED_MODES).optional(),
|
||||
@@ -78,7 +90,7 @@ export type RotateSecret = z.infer<typeof rotateSecretSchema>;
|
||||
|
||||
export const updateSecretSchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
key: z.string().min(1).regex(/^[a-zA-Z0-9_.-]+$/).optional(),
|
||||
key: secretKeySchema.optional(),
|
||||
status: z.enum(SECRET_STATUSES).optional(),
|
||||
providerConfigId: z.string().uuid().optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
@@ -96,13 +108,86 @@ export const secretBindingTargetSchema = z.object({
|
||||
|
||||
export const createSecretBindingSchema = secretBindingTargetSchema.extend({
|
||||
secretId: z.string().uuid(),
|
||||
versionSelector: z.union([z.literal("latest"), z.number().int().positive()]).default("latest"),
|
||||
versionSelector: secretVersionSelectorSchema.default("latest"),
|
||||
required: z.boolean().default(true),
|
||||
label: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export type CreateSecretBinding = z.infer<typeof createSecretBindingSchema>;
|
||||
|
||||
export const createUserSecretDefinitionSchema = z.object({
|
||||
key: secretKeySchema,
|
||||
name: z.string().trim().min(1).max(160),
|
||||
description: z.string().trim().max(500).optional().nullable(),
|
||||
status: z.enum(SECRET_STATUSES).optional(),
|
||||
provider: z.enum(SECRET_PROVIDERS).optional(),
|
||||
providerConfigId: z.string().uuid().optional().nullable(),
|
||||
managedMode: z.enum(SECRET_MANAGED_MODES).optional(),
|
||||
providerMetadata: z.record(z.string(), z.unknown()).optional().nullable(),
|
||||
usageGuidance: z.string().trim().max(1000).optional().nullable(),
|
||||
});
|
||||
|
||||
export type CreateUserSecretDefinition = z.infer<typeof createUserSecretDefinitionSchema>;
|
||||
|
||||
export const updateUserSecretDefinitionSchema = z.object({
|
||||
key: secretKeySchema.optional(),
|
||||
name: z.string().trim().min(1).max(160).optional(),
|
||||
description: z.string().trim().max(500).optional().nullable(),
|
||||
status: z.enum(SECRET_STATUSES).optional(),
|
||||
providerConfigId: z.string().uuid().optional().nullable(),
|
||||
providerMetadata: z.record(z.string(), z.unknown()).optional().nullable(),
|
||||
usageGuidance: z.string().trim().max(1000).optional().nullable(),
|
||||
});
|
||||
|
||||
export type UpdateUserSecretDefinition = z.infer<typeof updateUserSecretDefinitionSchema>;
|
||||
|
||||
export const createUserSecretValueSchema = z.object({
|
||||
definitionKey: secretKeySchema.optional(),
|
||||
definitionId: z.string().uuid().optional(),
|
||||
value: z.string().min(1).optional().nullable(),
|
||||
externalRef: z.string().optional().nullable(),
|
||||
providerVersionRef: z.string().optional().nullable(),
|
||||
providerConfigId: z.string().uuid().optional().nullable(),
|
||||
}).superRefine((value, ctx) => {
|
||||
if (!value.definitionKey && !value.definitionId) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["definitionId"],
|
||||
message: "User secret value requires definitionId or definitionKey",
|
||||
});
|
||||
}
|
||||
if (!value.value?.trim() && !value.externalRef?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["value"],
|
||||
message: "User secret value requires value or externalRef",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type CreateUserSecretValue = z.infer<typeof createUserSecretValueSchema>;
|
||||
|
||||
export const updateUserSecretValueSchema = z.object({
|
||||
status: z.enum(SECRET_STATUSES).optional(),
|
||||
value: z.string().min(1).optional().nullable(),
|
||||
externalRef: z.string().optional().nullable(),
|
||||
providerVersionRef: z.string().optional().nullable(),
|
||||
providerConfigId: z.string().uuid().optional().nullable(),
|
||||
});
|
||||
|
||||
export type UpdateUserSecretValue = z.infer<typeof updateUserSecretValueSchema>;
|
||||
|
||||
export const createUserSecretDeclarationSchema = secretBindingTargetSchema.extend({
|
||||
definitionKey: secretKeySchema,
|
||||
envKey: z.string().trim().min(1),
|
||||
versionSelector: secretVersionSelectorSchema.default("latest"),
|
||||
required: z.boolean().default(true),
|
||||
allowMissingOverride: z.boolean().default(false),
|
||||
label: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export type CreateUserSecretDeclaration = z.infer<typeof createUserSecretDeclarationSchema>;
|
||||
|
||||
const safeShortText = z.string().trim().min(1).max(160);
|
||||
const optionalSafeShortText = safeShortText.optional().nullable();
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ function sanitizeValue(value: unknown): unknown {
|
||||
if (value === null || value === undefined) return value;
|
||||
if (Array.isArray(value)) return value.map(sanitizeValue);
|
||||
if (isSecretRefBinding(value)) return value;
|
||||
if (isUserSecretRefBinding(value)) return value;
|
||||
if (isPlainBinding(value)) return { type: "plain", value: sanitizeValue(value.value) };
|
||||
if (!isPlainObject(value)) return value;
|
||||
return sanitizeRecord(value);
|
||||
@@ -64,6 +65,11 @@ function isSecretRefBinding(value: unknown): value is { type: "secret_ref"; secr
|
||||
return value.type === "secret_ref" && typeof value.secretId === "string";
|
||||
}
|
||||
|
||||
function isUserSecretRefBinding(value: unknown): value is { type: "user_secret_ref"; key: string; version?: unknown } {
|
||||
if (!isPlainObject(value)) return false;
|
||||
return value.type === "user_secret_ref" && typeof value.key === "string";
|
||||
}
|
||||
|
||||
function isPlainBinding(value: unknown): value is { type: "plain"; value: unknown } {
|
||||
if (!isPlainObject(value)) return false;
|
||||
return value.type === "plain" && "value" in value;
|
||||
@@ -101,6 +107,10 @@ export function sanitizeRecord(record: Record<string, unknown>): Record<string,
|
||||
redacted[key] = sanitizeValue(value);
|
||||
continue;
|
||||
}
|
||||
if (isUserSecretRefBinding(value)) {
|
||||
redacted[key] = sanitizeValue(value);
|
||||
continue;
|
||||
}
|
||||
if (isPlainBinding(value)) {
|
||||
redacted[key] = { type: "plain", value: REDACTED_EVENT_VALUE };
|
||||
continue;
|
||||
|
||||
@@ -2091,6 +2091,7 @@ const issueListSelect = {
|
||||
executionLockedAt: issues.executionLockedAt,
|
||||
createdByAgentId: issues.createdByAgentId,
|
||||
createdByUserId: issues.createdByUserId,
|
||||
responsibleUserId: issues.responsibleUserId,
|
||||
issueNumber: issues.issueNumber,
|
||||
identifier: issues.identifier,
|
||||
originKind: issues.originKind,
|
||||
|
||||
@@ -1177,6 +1177,7 @@ function routineRevisionSnapshotRoutine(routine: typeof routines.$inferSelect):
|
||||
originId: routine.originId,
|
||||
variables: routine.variables ?? [],
|
||||
env: routine.env ?? null,
|
||||
responsibleUserId: routine.responsibleUserId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -452,6 +452,7 @@ function routineRevisionSnapshotRoutine(routine: RoutineRow): RoutineRevisionSna
|
||||
catchUpPolicy: routine.catchUpPolicy as RoutineRevisionSnapshotV1["routine"]["catchUpPolicy"],
|
||||
variables: routine.variables ?? [],
|
||||
env: routine.env ?? null,
|
||||
responsibleUserId: routine.responsibleUserId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,14 @@ async function cleanupPreparedProviderWrite(input: {
|
||||
|
||||
type CanonicalEnvBinding =
|
||||
| { type: "plain"; value: string }
|
||||
| { type: "secret_ref"; secretId: string; version: number | "latest" };
|
||||
| { type: "secret_ref"; secretId: string; version: number | "latest" }
|
||||
| {
|
||||
type: "user_secret_ref";
|
||||
key: string;
|
||||
version: number | "latest";
|
||||
required: boolean;
|
||||
allowMissingOverride: boolean;
|
||||
};
|
||||
|
||||
type SecretConsumerContext = {
|
||||
consumerType: SecretBindingTargetType;
|
||||
@@ -303,6 +310,15 @@ function canonicalizeBinding(binding: EnvBinding): CanonicalEnvBinding {
|
||||
if (binding.type === "plain") {
|
||||
return { type: "plain", value: String(binding.value) };
|
||||
}
|
||||
if (binding.type === "user_secret_ref") {
|
||||
return {
|
||||
type: "user_secret_ref",
|
||||
key: binding.key,
|
||||
version: binding.version ?? "latest",
|
||||
required: binding.required ?? true,
|
||||
allowMissingOverride: binding.allowMissingOverride ?? false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: "secret_ref",
|
||||
secretId: binding.secretId,
|
||||
@@ -310,6 +326,10 @@ function canonicalizeBinding(binding: EnvBinding): CanonicalEnvBinding {
|
||||
};
|
||||
}
|
||||
|
||||
function userSecretResolverUnavailable(key: string): never {
|
||||
throw unprocessable(`User secret ref is declared but runtime resolution is not enabled yet: ${key}`);
|
||||
}
|
||||
|
||||
function defaultProviderConfigStatus(provider: SecretProvider): SecretProviderConfigStatus {
|
||||
return COMING_SOON_SECRET_PROVIDERS.has(provider) ? "coming_soon" : "ready";
|
||||
}
|
||||
@@ -807,6 +827,10 @@ export function secretService(db: Db) {
|
||||
normalized[key] = binding;
|
||||
continue;
|
||||
}
|
||||
if (binding.type === "user_secret_ref") {
|
||||
normalized[key] = binding;
|
||||
continue;
|
||||
}
|
||||
|
||||
await assertSecretInCompany(companyId, binding.secretId);
|
||||
normalized[key] = {
|
||||
@@ -887,6 +911,9 @@ export function secretService(db: Db) {
|
||||
version: binding.version,
|
||||
};
|
||||
}
|
||||
if (binding.type === "user_secret_ref") {
|
||||
throw unprocessable(`${input.key} must be a string, plain binding, or company secret reference`);
|
||||
}
|
||||
const value = binding.value.trim();
|
||||
if (!value) return undefined;
|
||||
if (value === REDACTED_SENTINEL) {
|
||||
@@ -2620,7 +2647,7 @@ export function secretService(db: Db) {
|
||||
const binding = canonicalizeBinding(parsed.data as EnvBinding);
|
||||
if (binding.type === "plain") {
|
||||
resolved[key] = binding.value;
|
||||
} else {
|
||||
} else if (binding.type === "secret_ref") {
|
||||
const secretResolution = await resolveSecretValueInternal(
|
||||
companyId,
|
||||
binding.secretId,
|
||||
@@ -2635,6 +2662,8 @@ export function secretService(db: Db) {
|
||||
resolved[key] = secretResolution.value;
|
||||
manifest.push(secretResolution.manifestEntry);
|
||||
secretKeys.add(key);
|
||||
} else {
|
||||
userSecretResolverUnavailable(binding.key);
|
||||
}
|
||||
}
|
||||
return { env: resolved, secretKeys, manifest };
|
||||
@@ -2768,7 +2797,7 @@ export function secretService(db: Db) {
|
||||
const binding = canonicalizeBinding(parsed.data as EnvBinding);
|
||||
if (binding.type === "plain") {
|
||||
env[key] = binding.value;
|
||||
} else {
|
||||
} else if (binding.type === "secret_ref") {
|
||||
const secretResolution = await resolveSecretValueInternal(
|
||||
companyId,
|
||||
binding.secretId,
|
||||
@@ -2783,6 +2812,8 @@ export function secretService(db: Db) {
|
||||
env[key] = secretResolution.value;
|
||||
manifest.push(secretResolution.manifestEntry);
|
||||
secretKeys.add(key);
|
||||
} else {
|
||||
userSecretResolverUnavailable(binding.key);
|
||||
}
|
||||
}
|
||||
resolved.env = env;
|
||||
@@ -2794,6 +2825,7 @@ export function secretService(db: Db) {
|
||||
if (!parsed.success) continue;
|
||||
const binding = canonicalizeBinding(parsed.data as EnvBinding);
|
||||
if (binding.type === "plain") continue;
|
||||
if (binding.type === "user_secret_ref") userSecretResolverUnavailable(binding.key);
|
||||
const secretResolution = await resolveSecretValueInternal(
|
||||
companyId,
|
||||
binding.secretId,
|
||||
|
||||
Reference in new issue
Block a user