History

Use config keys as local source ids

Rhys Sullivan committed 8 months ago

Commit e05b2641 parent 9fe77d820 files · +80 −185Browse files

Unified · Split

20 files
  1. packages/control-plane/src/persistence/control-plane-rows.test.ts +0 −4
  2. packages/control-plane/src/persistence/post-migrations.test.ts +0 −1
  3. packages/control-plane/src/runtime/control-plane-runtime.test.ts +2 −2
  4. packages/control-plane/src/runtime/local-source-artifacts.ts +5 −8
  5. packages/control-plane/src/runtime/local-workspace-state.ts +0 −2
  6. packages/control-plane/src/runtime/local-workspace-sync.ts +9 −12
  7. packages/control-plane/src/runtime/source-auth-material.test.ts +0 −1
  8. packages/control-plane/src/runtime/source-auth-service.test.ts +0 −1
  9. packages/control-plane/src/runtime/source-definitions.test.ts +0 −1
  10. packages/control-plane/src/runtime/source-definitions.ts +0 −3
  11. packages/control-plane/src/runtime/source-inspection.test.ts +0 −2
  12. packages/control-plane/src/runtime/source-materialization.ts +6 −12
  13. packages/control-plane/src/runtime/source-recipes-runtime.test.ts +0 −5
  14. packages/control-plane/src/runtime/source-recipes-runtime.ts +2 −13
  15. packages/control-plane/src/runtime/source-store.test.ts +0 −1
  16. packages/control-plane/src/runtime/source-store.ts +56 −110
  17. packages/control-plane/src/runtime/workspace-execution-environment.test.ts +0 −2
  18. packages/control-plane/src/schema/models/source.ts +0 −3
  19. packages/kitchen-sink/src/source-runtime.test.ts +0 −1
  20. packages/react/src/index.ts +0 −1
packages/control-plane/src/persistence/control-plane-rows.test.ts+0−4
@@ -140,7 +140,6 @@ const seedWorkspaceSourceState = (input: { · 139 unmodified lines
140140 yield* input.persistence.rows.sources.insert({
141141 id: input.sourceId,
142142 workspaceId: input.workspaceId,
143− configKey: null,
144143 recipeId,
145144 recipeRevisionId,
146145 name: "Github",
@@ -278,7 +277,6 @@ describe("control-plane-persistence-drizzle", () => { · 131 unmodified lines
278277 yield* persistence.rows.sources.insert({
279278 id: SourceIdSchema.make("src_1"),
280279 workspaceId,
281− configKey: null,
282280 recipeId: SourceRecipeIdSchema.make("src_recipe_1"),
283281 recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_1"),
284282 name: "Github",
@@ -863,7 +861,6 @@ describe("control-plane-persistence-drizzle", () => { · 578 unmodified lines
863861 yield* persistence.rows.sources.insert({
864862 id: SourceIdSchema.make("src_shared_removed"),
865863 workspaceId: removedWorkspaceId,
866− configKey: null,
867864 recipeId: sharedRecipeId,
868865 recipeRevisionId: sharedRecipeRevisionId,
869866 name: "Github Removed",
@@ -882,7 +879,6 @@ describe("control-plane-persistence-drizzle", () => { · 12 unmodified lines
882879 yield* persistence.rows.sources.insert({
883880 id: SourceIdSchema.make("src_shared_remaining"),
884881 workspaceId: remainingWorkspaceId,
885− configKey: null,
886882 recipeId: sharedRecipeId,
887883 recipeRevisionId: sharedRecipeRevisionId,
888884 name: "Github Remaining",
packages/control-plane/src/persistence/post-migrations.test.ts+0−1
@@ -188,7 +188,6 @@ const seedMigratedSourceRecipe = (input: { · 187 unmodified lines
188188 yield* input.persistence.rows.sources.insert({
189189 id: input.sourceId,
190190 workspaceId: input.workspaceId,
191− configKey: null,
192191 recipeId,
193192 recipeRevisionId,
194193 name: input.kind === "openapi" ? "GitHub" : "GraphQL Demo",
packages/control-plane/src/runtime/control-plane-runtime.test.ts+2−2
@@ -325,7 +325,7 @@ describe("control-plane-runtime", () => { · 324 unmodified lines
325325 const runtime = yield* makeRuntime;
326326 const installation = runtime.localInstallation;
327327 const executionId = ExecutionIdSchema.make("exec_local_credential");
328− const sourceId = SourceIdSchema.make("src_local_credential");
328+ const sourceId = SourceIdSchema.make("github");
329329 const interactionSuffix = "executor.sources.add:test";
330330 const interactionId = ExecutionInteractionIdSchema.make(
331331 `${executionId}:${interactionSuffix}`,
@@ -488,7 +488,7 @@ describe("control-plane-runtime", () => { · 156 unmodified lines
488488 const runtime = yield* makeRuntime;
489489 const installation = runtime.localInstallation;
490490 const executionId = ExecutionIdSchema.make("exec_local_credential_continue");
491− const sourceId = SourceIdSchema.make("src_local_credential_continue");
491+ const sourceId = SourceIdSchema.make("github");
492492 const interactionSuffix = "executor.sources.add:continue";
493493 const interactionId = ExecutionInteractionIdSchema.make(
494494 `${executionId}:${interactionSuffix}`,
packages/control-plane/src/runtime/local-source-artifacts.ts+5−8
@@ -29,7 +29,6 @@ const LOCAL_SOURCE_ARTIFACT_VERSION = 1 as const; · 28 unmodified lines
2929 export const LocalSourceArtifactSchema = Schema.Struct({
3030 version: Schema.Literal(LOCAL_SOURCE_ARTIFACT_VERSION),
3131 sourceId: SourceIdSchema,
32− configKey: Schema.String,
3332 recipeId: SourceRecipeIdSchema,
3433 generatedAt: TimestampMsSchema,
3534 revision: StoredSourceRecipeRevisionRecordSchema,
@@ -44,12 +43,12 @@ const decodeLocalSourceArtifact = Schema.decodeUnknownSync(LocalSourceArtifactSc · 8 unmodified lines
4443
4544 const localSourceArtifactPath = (input: {
4645 context: ResolvedLocalWorkspaceContext;
47− configKey: string;
46+ sourceId: string;
4847 }): string =>
4948 join(
5049 input.context.artifactsDirectory,
5150 "sources",
52− `${input.configKey}.json`,
51+ `${input.sourceId}.json`,
5352 );
5453
5554 const canonicalMaterializationHash = (input: {
@@ -112,7 +111,6 @@ const bindRevisionId = <T extends { recipeRevisionId: string }>( · 56 unmodified lines
112111
113112 export const buildLocalSourceArtifact = (input: {
114113 source: Source;
115− configKey: string;
116114 materialization: SourceRecipeMaterialization;
117115 }): LocalSourceArtifact => {
118116 const recipeId: SourceRecipeId = stableSourceRecipeId(input.source);
@@ -131,7 +129,6 @@ export const buildLocalSourceArtifact = (input: { · 12 unmodified lines
131129 return {
132130 version: LOCAL_SOURCE_ARTIFACT_VERSION,
133131 sourceId: input.source.id,
134− configKey: input.configKey,
135132 recipeId,
136133 generatedAt: now,
137134 revision,
@@ -143,7 +140,7 @@ export const buildLocalSourceArtifact = (input: { · 5 unmodified lines
143140
144141 export const readLocalSourceArtifact = async (input: {
145142 context: ResolvedLocalWorkspaceContext;
146− configKey: string;
143+ sourceId: string;
147144 }): Promise<LocalSourceArtifact | null> => {
148145 const path = localSourceArtifactPath(input);
149146
@@ -166,7 +163,7 @@ export const readLocalSourceArtifact = async (input: { · 16 unmodified lines
166163
167164 export const writeLocalSourceArtifact = async (input: {
168165 context: ResolvedLocalWorkspaceContext;
169− configKey: string;
166+ sourceId: string;
170167 artifact: LocalSourceArtifact;
171168 }): Promise<void> => {
172169 const path = localSourceArtifactPath(input);
@@ -178,7 +175,7 @@ export const writeLocalSourceArtifact = async (input: { · 5 unmodified lines
178175
179176 export const removeLocalSourceArtifact = async (input: {
180177 context: ResolvedLocalWorkspaceContext;
181− configKey: string;
178+ sourceId: string;
182179 }): Promise<void> => {
183180 const path = localSourceArtifactPath(input);
184181
packages/control-plane/src/runtime/local-workspace-state.ts+0−2
@@ -3,7 +3,6 @@ import { join } from "node:path"; · 2 unmodified lines
33
44 import {
55 PolicyIdSchema,
6− SourceIdSchema,
76 SourceStatusSchema,
87 TimestampMsSchema,
98 } from "#schema";
@@ -14,7 +13,6 @@ import type { ResolvedLocalWorkspaceContext } from "./local-config"; · 4 unmodified lines
1413 const WORKSPACE_STATE_BASENAME = "workspace-state.json";
1514
1615 const LocalWorkspaceSourceStateSchema = Schema.Struct({
17− id: SourceIdSchema,
1816 status: SourceStatusSchema,
1917 lastError: Schema.NullOr(Schema.String),
2018 sourceHash: Schema.NullOr(Schema.String),
packages/control-plane/src/runtime/local-workspace-sync.ts+9−12
@@ -42,12 +42,11 @@ const toApprovalMode = (approval: LocalConfigPolicy["approval"]): Policy["approv · 41 unmodified lines
4242 approval === "manual" ? "required" : "auto";
4343
4444 export const deriveSourceConfigKey = (
45− source: Pick<Source, "configKey" | "namespace" | "name">,
45+ source: Pick<Source, "namespace" | "name">,
4646 used: Set<string>,
4747 ): string => {
4848 const base =
49− trimOrNull(source.configKey)
50− ?? trimOrNull(source.namespace)
49+ trimOrNull(source.namespace)
5150 ?? trimOrNull(source.name)
5251 ?? "source";
5352 const slugBase = slugify(base) || "source";
@@ -112,14 +111,15 @@ const configAuthFromStoredSource = (input: { · 58 unmodified lines
112111
113112 export const configSourceFromStoredSource = (input: {
114113 source: Source;
114+ sourceId: string;
115115 config: LocalExecutorConfig | null;
116116 }): LocalConfigSource => {
117117 const auth = configAuthFromStoredSource(input);
118118 const common = {
119− ...(trimOrNull(input.source.name) !== trimOrNull(input.source.configKey)
119+ ...(trimOrNull(input.source.name) !== trimOrNull(input.sourceId)
120120 ? { name: input.source.name }
121121 : {}),
122− ...(trimOrNull(input.source.namespace) !== trimOrNull(input.source.configKey)
122+ ...(trimOrNull(input.source.namespace) !== trimOrNull(input.sourceId)
123123 ? { namespace: input.source.namespace ?? undefined }
124124 : {}),
125125 ...(input.source.enabled === false ? { enabled: false } : {}),
@@ -231,10 +231,8 @@ const exportWorkspaceConfig = (input: { · 105 unmodified lines
231231 const configKey = deriveSourceConfigKey(source, sourceKeys);
232232 sourceMappings.push({ configKey, source });
233233 sourcesConfig[configKey] = configSourceFromStoredSource({
234− source: {
235− ...source,
236− configKey,
237− },
234+ source,
235+ sourceId: configKey,
238236 config: input.loadedConfig.config,
239237 });
240238 }
@@ -304,7 +302,6 @@ const exportWorkspaceConfig = (input: { · 63 unmodified lines
304302 sources: {
305303 ...workspaceState.sources,
306304 [configKey]: {
307− id: source.id,
308305 status: source.status,
309306 lastError: source.lastError,
310307 sourceHash: source.sourceHash,
@@ -324,13 +321,13 @@ const exportWorkspaceConfig = (input: { · 13 unmodified lines
324321 try: () =>
325322 writeLocalSourceArtifact({
326323 context: input.context,
327− configKey,
324+ sourceId: configKey,
328325 artifact: buildLocalSourceArtifact({
329326 source: {
330327 ...source,
328+ id: configKey as Source["id"],
331329 sourceHash: revision.manifestHash ?? source.sourceHash,
332330 },
333− configKey,
334331 materialization: {
335332 manifestJson: revision.manifestJson,
336333 manifestHash: revision.manifestHash,
packages/control-plane/src/runtime/source-auth-material.test.ts+0−1
@@ -12,7 +12,6 @@ import { resolveSourceAuthMaterial } from "./source-auth-material"; · 11 unmodified lines
1212 const makeSource = (overrides: Partial<Source> = {}): Source => ({
1313 id: SourceIdSchema.make("src_tool_artifacts"),
1414 workspaceId: WorkspaceIdSchema.make("ws_tool_artifacts"),
15− configKey: null,
1615 name: "GitHub",
1716 kind: "openapi",
1817 endpoint: "https://api.github.com",
packages/control-plane/src/runtime/source-auth-service.test.ts+0−1
@@ -32,7 +32,6 @@ import { resolveSourceAuthMaterial } from "./source-auth-material"; · 31 unmodified lines
3232 const makeExistingOpenApiSource = (auth: Source["auth"]): Source => ({
3333 id: "src_test" as Source["id"],
3434 workspaceId: "ws_test" as Source["workspaceId"],
35− configKey: null,
3635 name: "GitHub",
3736 kind: "openapi",
3837 endpoint: "https://api.github.com",
packages/control-plane/src/runtime/source-definitions.test.ts+0−1
@@ -44,7 +44,6 @@ const mcpBinding = (input: { · 43 unmodified lines
4444 const makeSource = (overrides: Partial<Source> = {}): Source => ({
4545 id: SourceIdSchema.make("src_source_definitions"),
4646 workspaceId: WorkspaceIdSchema.make("ws_source_definitions"),
47− configKey: null,
4847 name: "GitHub",
4948 kind: "openapi",
5049 endpoint: "https://api.github.com",
packages/control-plane/src/runtime/source-definitions.ts+0−3
@@ -232,7 +232,6 @@ export const createSourceFromPayload = (input: { · 231 unmodified lines
232232 return yield* validateSourceByKind({
233233 id: input.sourceId,
234234 workspaceId: input.workspaceId,
235− configKey: null,
236235 name: input.payload.name.trim(),
237236 kind: input.payload.kind,
238237 endpoint: input.payload.endpoint.trim(),
@@ -353,7 +352,6 @@ export const splitSourceForStorage = (input: { · 114 unmodified lines
353352 const sourceRecord: StoredSourceRecord = {
354353 id: input.source.id,
355354 workspaceId: input.source.workspaceId,
356− configKey: input.source.configKey,
357355 recipeId: input.recipeId,
358356 recipeRevisionId: input.recipeRevisionId,
359357 name: input.source.name,
@@ -408,7 +406,6 @@ export const projectSourceFromStorage = (input: { · 48 unmodified lines
408406 return {
409407 id: input.sourceRecord.id,
410408 workspaceId: input.sourceRecord.workspaceId,
411− configKey: input.sourceRecord.configKey,
412409 name: input.sourceRecord.name,
413410 kind: input.sourceRecord.kind,
414411 endpoint: input.sourceRecord.endpoint,
packages/control-plane/src/runtime/source-inspection.test.ts+0−2
@@ -62,7 +62,6 @@ const makeSource = (input: { · 61 unmodified lines
6262 }): Source => ({
6363 id: input.sourceId,
6464 workspaceId: input.workspaceId,
65− configKey: null,
6665 name: "Cloudflare API",
6766 kind: "openapi",
6867 endpoint: "https://api.cloudflare.com/client/v4",
@@ -238,7 +237,6 @@ describe("source-inspection", () => { · 169 unmodified lines
238237 await Effect.runPromise(persistence.rows.sources.insert({
239238 id: sourceId,
240239 workspaceId,
241− configKey: null,
242240 recipeId,
243241 recipeRevisionId,
244242 name: "Cloudflare API",
packages/control-plane/src/runtime/source-materialization.ts+6−12
@@ -47,7 +47,6 @@ export const syncSourceMaterialization = (input: { · 46 unmodified lines
4747 if (
4848 runtimeLocalWorkspace !== null
4949 && runtimeLocalWorkspace.installation.workspaceId === input.source.workspaceId
50− && input.source.configKey !== null
5150 ) {
5251 if (!shouldIndexSource(input.source)) {
5352 const state = yield* Effect.tryPromise({
@@ -55,13 +54,12 @@ export const syncSourceMaterialization = (input: { · 1 unmodified line
5554 catch: (cause) =>
5655 cause instanceof Error ? cause : new Error(String(cause)),
5756 });
58− const existingSourceState = state.sources[input.source.configKey];
57+ const existingSourceState = state.sources[input.source.id];
5958 const nextState: LocalWorkspaceState = {
6059 ...state,
6160 sources: {
6261 ...state.sources,
63− [input.source.configKey]: {
64− id: input.source.id,
62+ [input.source.id]: {
6563 status: (input.source.enabled ? input.source.status : "draft") as SourceStatus,
6664 lastError: null,
6765 sourceHash: input.source.sourceHash,
@@ -100,10 +98,9 @@ export const syncSourceMaterialization = (input: { · 32 unmodified lines
10098 try: () =>
10199 writeLocalSourceArtifact({
102100 context: runtimeLocalWorkspace.context,
103− configKey: input.source.configKey!,
101+ sourceId: input.source.id,
104102 artifact: buildLocalSourceArtifact({
105103 source: input.source,
106− configKey: input.source.configKey!,
107104 materialization,
108105 }),
109106 }),
@@ -116,13 +113,12 @@ export const syncSourceMaterialization = (input: { · 6 unmodified lines
116113 catch: (cause) =>
117114 cause instanceof Error ? cause : new Error(String(cause)),
118115 });
119− const existingSourceState = state.sources[input.source.configKey];
116+ const existingSourceState = state.sources[input.source.id];
120117 const nextState: LocalWorkspaceState = {
121118 ...state,
122119 sources: {
123120 ...state.sources,
124− [input.source.configKey]: {
125− id: input.source.id,
121+ [input.source.id]: {
126122 status: "connected",
127123 lastError: null,
128124 sourceHash: materialization.sourceHash,
@@ -179,7 +175,6 @@ export const persistMcpRecipeMaterializationFromManifest = (input: { · 50 unmodified lines
179175 if (
180176 runtimeLocalWorkspace !== null
181177 && runtimeLocalWorkspace.installation.workspaceId === input.source.workspaceId
182− && input.source.configKey !== null
183178 ) {
184179 const materialization = materializationFromMcpManifestEntries({
185180 recipeRevisionId: "src_recipe_rev_materialization" as never,
@@ -191,10 +186,9 @@ export const persistMcpRecipeMaterializationFromManifest = (input: { · 5 unmodified lines
191186 try: () =>
192187 writeLocalSourceArtifact({
193188 context: runtimeLocalWorkspace.context,
194− configKey: input.source.configKey!,
189+ sourceId: input.source.id,
195190 artifact: buildLocalSourceArtifact({
196191 source: input.source,
197− configKey: input.source.configKey!,
198192 materialization,
199193 }),
200194 }),
packages/control-plane/src/runtime/source-recipes-runtime.test.ts+0−5
@@ -87,7 +87,6 @@ const makeSource = (overrides: Partial<Source> = {}): Source => { · 86 unmodified lines
8787 return {
8888 id: SourceIdSchema.make("src_runtime_recipe"),
8989 workspaceId: WorkspaceIdSchema.make("ws_runtime_recipe"),
90− configKey: null,
9190 name: "GitHub",
9291 kind,
9392 endpoint,
@@ -373,7 +372,6 @@ describe("source-recipes-runtime", () => { · 279 unmodified lines
373372 sourceRecord: {
374373 id: SourceIdSchema.make("src_runtime_recipe"),
375374 workspaceId: WorkspaceIdSchema.make("ws_runtime_recipe"),
376− configKey: null,
377375 recipeId: SourceRecipeIdSchema.make("src_recipe_runtime"),
378376 recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_runtime"),
379377 name: "GITHUB API",
@@ -545,7 +543,6 @@ describe("source-recipes-runtime", () => { · 165 unmodified lines
545543 await Effect.runPromise(persistence.rows.sources.insert({
546544 id: SourceIdSchema.make(`src_shared_revision_${index}`),
547545 workspaceId,
548− configKey: null,
549546 recipeId,
550547 recipeRevisionId,
551548 name,
@@ -624,7 +621,6 @@ describe("source-recipes-runtime", () => { · 72 unmodified lines
624621 await Effect.runPromise(persistence.rows.sources.insert({
625622 id: sourceId,
626623 workspaceId,
627− configKey: null,
628624 recipeId,
629625 recipeRevisionId,
630626 name: "GraphQL Demo",
@@ -692,7 +688,6 @@ describe("source-recipes-runtime", () => { · 61 unmodified lines
692688 await Effect.runPromise(persistence.rows.sources.insert({
693689 id: sourceId,
694690 workspaceId,
695− configKey: null,
696691 recipeId,
697692 recipeRevisionId,
698693 name: "Broken GitHub",
packages/control-plane/src/runtime/source-recipes-runtime.ts+2−13
@@ -173,15 +173,11 @@ export const loadWorkspaceSourceRecipes = (input: { · 172 unmodified lines
173173
174174 const localRecipes = yield* Effect.forEach(sources, (source) =>
175175 Effect.gen(function* () {
176− if (source.configKey === null) {
177− return null;
178− }
179−
180176 const artifact = yield* Effect.tryPromise({
181177 try: () =>
182178 readLocalSourceArtifact({
183179 context: runtimeLocalWorkspace.context,
184− configKey: source.configKey!,
180+ sourceId: source.id,
185181 }),
186182 catch: (cause) =>
187183 cause instanceof Error ? cause : new Error(String(cause)),
@@ -193,7 +189,6 @@ export const loadWorkspaceSourceRecipes = (input: { · 5 unmodified lines
193189 const sourceRecord: StoredSourceRecord = {
194190 id: source.id,
195191 workspaceId: source.workspaceId,
196− configKey: source.configKey,
197192 recipeId: artifact.recipeId,
198193 recipeRevisionId: artifact.revision.id,
199194 name: source.name,
@@ -313,17 +308,12 @@ export const loadSourceWithRecipe = (input: { · 113 unmodified lines
313308 sourceId: input.sourceId,
314309 actorAccountId: input.actorAccountId,
315310 });
316− if (source.configKey === null) {
317− return yield* Effect.fail(
318− new Error(`Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`),
319− );
320− }
321311
322312 const artifact = yield* Effect.tryPromise({
323313 try: () =>
324314 readLocalSourceArtifact({
325315 context: runtimeLocalWorkspace.context,
326− configKey: source.configKey!,
316+ sourceId: source.id,
327317 }),
328318 catch: (cause) =>
329319 cause instanceof Error ? cause : new Error(String(cause)),
@@ -337,7 +327,6 @@ export const loadSourceWithRecipe = (input: { · 7 unmodified lines
337327 const sourceRecord: StoredSourceRecord = {
338328 id: source.id,
339329 workspaceId: source.workspaceId,
340− configKey: source.configKey,
341330 recipeId: artifact.recipeId,
342331 recipeRevisionId: artifact.revision.id,
343332 name: source.name,
packages/control-plane/src/runtime/source-store.test.ts+0−1
@@ -52,7 +52,6 @@ const makeOpenApiSource = (input: { · 51 unmodified lines
5252 }): Source => ({
5353 id: input.sourceId,
5454 workspaceId: input.workspaceId,
55− configKey: null,
5655 name: input.name ?? "GitHub",
5756 kind: "openapi",
5857 endpoint: input.endpoint ?? "https://api.github.com",
packages/control-plane/src/runtime/source-store.ts+56−110
@@ -1,5 +1,3 @@
1−import { createHash } from "node:crypto";
2−
31 import type {
42 AccountId,
53 AuthArtifact,
@@ -7,10 +5,12 @@ import type { · 1 unmodified line
75 LocalConfigSecretInput,
86 LocalConfigSource,
97 Source,
8+ SourceId,
109 SourceRecipeId,
1110 SourceRecipeRevisionId,
1211 WorkspaceId,
1312 } from "#schema";
13+import { SourceIdSchema } from "#schema";
1414 import { type SqlControlPlaneRows } from "#persistence";
1515 import * as Effect from "effect/Effect";
1616 import * as Option from "effect/Option";
@@ -118,24 +118,22 @@ const trimOrNull = (value: string | null | undefined): string | null => { · 101 unmodified lines
118118 const cloneJson = <T>(value: T): T =>
119119 JSON.parse(JSON.stringify(value)) as T;
120120
121−const localSourceIdForConfigKey = (input: {
122− workspaceRoot: string;
123− configKey: string;
124−}): Source["id"] => {
125− const hash = createHash("sha256")
126− .update(`${input.workspaceRoot}:${input.configKey}`)
127− .digest("hex")
128− .slice(0, 16);
129− return `src_local_${hash}` as Source["id"];
130−};
131−
132−const deriveLocalSourceConfigKey = (source: Pick<Source, "configKey" | "namespace" | "name">): string => {
121+const deriveLocalSourceId = (
122+ source: Pick<Source, "namespace" | "name">,
123+ used: ReadonlySet<string>,
124+): SourceId => {
133125 const base =
134− trimOrNull(source.configKey)
135− ?? trimOrNull(source.namespace)
126+ trimOrNull(source.namespace)
136127 ?? trimOrNull(source.name)
137128 ?? "source";
138− return slugify(base) || "source";
129+ const slugBase = slugify(base) || "source";
130+ let candidate = slugBase;
131+ let counter = 2;
132+ while (used.has(candidate)) {
133+ candidate = `${slugBase}-${counter}`;
134+ counter += 1;
135+ }
136+ return SourceIdSchema.make(candidate);
139137 };
140138
141139 const resolveLocalConfigSecretProviderAlias = (config: Awaited<ReturnType<typeof loadLocalExecutorConfig>>["config"]): string | null => {
@@ -257,34 +255,30 @@ const buildLocalSourceRecord = (input: { · 115 unmodified lines
257255 workspaceId: WorkspaceId;
258256 loadedConfig: Awaited<ReturnType<typeof loadLocalExecutorConfig>>;
259257 workspaceState: LocalWorkspaceState;
260− configKey: string;
258+ sourceId: SourceId;
261259 actorAccountId?: AccountId | null;
262260 authArtifacts: ReadonlyArray<AuthArtifact>;
263261 }): Effect.Effect<{
264262 source: Source;
265− configKey: string;
263+ sourceId: SourceId;
266264 }, Error, never> =>
267265 Effect.gen(function* () {
268− const sourceConfig = input.loadedConfig.config?.sources?.[input.configKey];
266+ const sourceConfig = input.loadedConfig.config?.sources?.[input.sourceId];
269267 if (!sourceConfig) {
270− return yield* Effect.fail(new Error(`Configured source not found for key ${input.configKey}`));
268+ return yield* Effect.fail(new Error(`Configured source not found for id ${input.sourceId}`));
271269 }
272270
273− const existingState = input.workspaceState.sources[input.configKey];
271+ const existingState = input.workspaceState.sources[input.sourceId];
274272 const adapter = getSourceAdapter(sourceConfig.kind);
275273 const baseSource = yield* adapter.validateSource({
276− id: existingState?.id ?? localSourceIdForConfigKey({
277− workspaceRoot: input.context.workspaceRoot,
278− configKey: input.configKey,
279− }),
274+ id: SourceIdSchema.make(input.sourceId),
280275 workspaceId: input.workspaceId,
281− configKey: input.configKey,
282− name: trimOrNull(sourceConfig.name) ?? input.configKey,
276+ name: trimOrNull(sourceConfig.name) ?? input.sourceId,
283277 kind: sourceConfig.kind,
284278 endpoint: sourceConfig.connection.endpoint.trim(),
285279 status: existingState?.status ?? ((sourceConfig.enabled ?? true) ? "connected" : "draft"),
286280 enabled: sourceConfig.enabled ?? true,
287− namespace: trimOrNull(sourceConfig.namespace) ?? input.configKey,
281+ namespace: trimOrNull(sourceConfig.namespace) ?? input.sourceId,
288282 bindingVersion: adapter.bindingConfigVersion,
289283 binding: sourceConfig.binding,
290284 importAuthPolicy: adapter.defaultImportAuthPolicy,
@@ -303,7 +297,7 @@ const buildLocalSourceRecord = (input: { · 12 unmodified lines
303297 const artifact = yield* Effect.tryPromise({
304298 try: () => readLocalSourceArtifact({
305299 context: input.context,
306− configKey: input.configKey,
300+ sourceId: input.sourceId,
307301 }),
308302 catch: (cause) =>
309303 cause instanceof Error ? cause : new Error(String(cause)),
@@ -325,7 +319,6 @@ const buildLocalSourceRecord = (input: { · 15 unmodified lines
325319 const sourceRecord = {
326320 id: baseSource.id,
327321 workspaceId: baseSource.workspaceId,
328− configKey: input.configKey,
329322 recipeId: artifact?.recipeId ?? stableSourceRecipeId(baseSource),
330323 recipeRevisionId: artifact?.revision.id ?? stableSourceRecipeRevisionId(baseSource),
331324 name: baseSource.name,
@@ -350,39 +343,10 @@ const buildLocalSourceRecord = (input: { · 18 unmodified lines
350343
351344 return {
352345 source,
353− configKey: input.configKey,
346+ sourceId: input.sourceId,
354347 };
355348 });
356349
357−const resolveLocalSourceConfigKey = (input: {
358− context: {
359− workspaceRoot: string;
360− };
361− loadedConfig: Awaited<ReturnType<typeof loadLocalExecutorConfig>>;
362− workspaceState: LocalWorkspaceState;
363− sourceId: Source["id"];
364−}): string | null => {
365− const configKeys = Object.keys(input.loadedConfig.config?.sources ?? {});
366− for (const configKey of configKeys) {
367− const storedId = input.workspaceState.sources[configKey]?.id;
368− if (storedId === input.sourceId) {
369− return configKey;
370− }
371−
372− if (
373− storedId === undefined
374− && localSourceIdForConfigKey({
375− workspaceRoot: input.context.workspaceRoot,
376− configKey,
377− }) === input.sourceId
378− ) {
379− return configKey;
380− }
381− }
382−
383− return null;
384−};
385−
386350 export const loadSourcesInWorkspace = (
387351 rows: SqlControlPlaneRows,
388352 workspaceId: WorkspaceId,
@@ -396,14 +360,14 @@ export const loadSourcesInWorkspace = ( · 7 unmodified lines
396360 const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(workspaceId);
397361 return yield* Effect.forEach(
398362 Object.keys(localWorkspace.loadedConfig.config?.sources ?? {}),
399− (configKey) =>
363+ (sourceId) =>
400364 Effect.map(
401365 buildLocalSourceRecord({
402366 context: localWorkspace.context,
403367 workspaceId,
404368 loadedConfig: localWorkspace.loadedConfig,
405369 workspaceState: localWorkspace.workspaceState,
406− configKey,
370+ sourceId: SourceIdSchema.make(sourceId),
407371 actorAccountId: options.actorAccountId,
408372 authArtifacts,
409373 }),
@@ -446,14 +410,7 @@ export const loadSourceById = (rows: SqlControlPlaneRows, input: { · 36 unmodified lines
446410 const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId);
447411 if (localWorkspace !== null) {
448412 const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(input.workspaceId);
449− const configKey = resolveLocalSourceConfigKey({
450− context: localWorkspace.context,
451− loadedConfig: localWorkspace.loadedConfig,
452− workspaceState: localWorkspace.workspaceState,
453− sourceId: input.sourceId,
454− });
455−
456− if (configKey === null) {
413+ if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) {
457414 return yield* Effect.fail(
458415 new Error(`Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`),
459416 );
@@ -464,7 +421,7 @@ export const loadSourceById = (rows: SqlControlPlaneRows, input: { · 4 unmodified lines
464421 workspaceId: input.workspaceId,
465422 loadedConfig: localWorkspace.loadedConfig,
466423 workspaceState: localWorkspace.workspaceState,
467− configKey,
424+ sourceId: input.sourceId,
468425 actorAccountId: input.actorAccountId,
469426 authArtifacts,
470427 });
@@ -507,7 +464,6 @@ export const loadSourceById = (rows: SqlControlPlaneRows, input: { · 36 unmodified lines
507464
508465 const configSourceFromLocalSource = (input: {
509466 source: Source;
510− configKey: string;
511467 existingConfigAuth: LocalConfigSecretInput | undefined;
512468 config: Awaited<ReturnType<typeof loadLocalExecutorConfig>>["config"];
513469 }): LocalConfigSource => {
@@ -518,10 +474,10 @@ const configSourceFromLocalSource = (input: { · 4 unmodified lines
518474 });
519475
520476 const common = {
521− ...(trimOrNull(input.source.name) !== trimOrNull(input.configKey)
477+ ...(trimOrNull(input.source.name) !== trimOrNull(input.source.id)
522478 ? { name: input.source.name }
523479 : {}),
524− ...(trimOrNull(input.source.namespace) !== trimOrNull(input.configKey)
480+ ...(trimOrNull(input.source.namespace) !== trimOrNull(input.source.id)
525481 ? { namespace: input.source.namespace ?? undefined }
526482 : {}),
527483 ...(input.source.enabled === false ? { enabled: false } : {}),
@@ -639,13 +595,7 @@ export const removeSourceById = (rows: SqlControlPlaneRows, input: { · 111 unmodified lines
639595 Effect.gen(function* () {
640596 const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId);
641597 if (localWorkspace !== null) {
642− const configKey = resolveLocalSourceConfigKey({
643− context: localWorkspace.context,
644− loadedConfig: localWorkspace.loadedConfig,
645− workspaceState: localWorkspace.workspaceState,
646− sourceId: input.sourceId,
647− });
648− if (configKey === null) {
598+ if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) {
649599 return false;
650600 }
651601
@@ -653,7 +603,7 @@ export const removeSourceById = (rows: SqlControlPlaneRows, input: { · 1 unmodified line
653603 const sources = {
654604 ...(projectConfig.sources ?? {}),
655605 };
656− delete sources[configKey];
606+ delete sources[input.sourceId];
657607 yield* Effect.tryPromise({
658608 try: () =>
659609 writeProjectLocalExecutorConfig({
@@ -668,7 +618,7 @@ export const removeSourceById = (rows: SqlControlPlaneRows, input: { · 8 unmodified lines
668618 });
669619
670620 const {
671− [configKey]: _removedSource,
621+ [input.sourceId]: _removedSource,
672622 ...remainingSources
673623 } = localWorkspace.workspaceState.sources;
674624 const workspaceState: LocalWorkspaceState = {
@@ -688,7 +638,7 @@ export const removeSourceById = (rows: SqlControlPlaneRows, input: { · 13 unmodified lines
688638 try: () =>
689639 removeLocalSourceArtifact({
690640 context: localWorkspace.context,
691− configKey,
641+ sourceId: input.sourceId,
692642 }),
693643 catch: (cause) =>
694644 cause instanceof Error ? cause : new Error(String(cause)),
@@ -743,9 +693,20 @@ export const persistSource = ( · 48 unmodified lines
743693 Effect.gen(function* () {
744694 const localWorkspace = yield* resolveRuntimeLocalWorkspace(source.workspaceId);
745695 if (localWorkspace !== null) {
696+ const nextSource = {
697+ ...source,
698+ id:
699+ localWorkspace.loadedConfig.config?.sources?.[source.id]
700+ || localWorkspace.workspaceState.sources[source.id]
701+ ? source.id
702+ : deriveLocalSourceId(
703+ source,
704+ new Set(Object.keys(localWorkspace.loadedConfig.config?.sources ?? {})),
705+ ),
706+ } satisfies Source;
746707 const existingAuthArtifacts = yield* rows.authArtifacts.listByWorkspaceAndSourceId({
747− workspaceId: source.workspaceId,
748− sourceId: source.id,
708+ workspaceId: nextSource.workspaceId,
709+ sourceId: nextSource.id,
749710 });
750711 const existingRuntimeAuthArtifact = selectExactAuthArtifact({
751712 authArtifacts: existingAuthArtifacts,
@@ -757,27 +718,13 @@ export const persistSource = ( · 5 unmodified lines
757718 actorAccountId: options.actorAccountId,
758719 slot: "import",
759720 });
760− const configKey =
761− source.configKey
762− ?? resolveLocalSourceConfigKey({
763− context: localWorkspace.context,
764− loadedConfig: localWorkspace.loadedConfig,
765− workspaceState: localWorkspace.workspaceState,
766− sourceId: source.id,
767− })
768− ?? deriveLocalSourceConfigKey(source);
769− const nextSource = {
770− ...source,
771− configKey,
772− } satisfies Source;
773721 const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {});
774722 const sources = {
775723 ...(projectConfig.sources ?? {}),
776724 };
777− const existingConfigSource = sources[configKey];
778− sources[configKey] = configSourceFromLocalSource({
725+ const existingConfigSource = sources[nextSource.id];
726+ sources[nextSource.id] = configSourceFromLocalSource({
779727 source: nextSource,
780− configKey,
781728 existingConfigAuth: existingConfigSource?.connection.auth,
782729 config: localWorkspace.loadedConfig.config,
783730 });
@@ -810,8 +757,8 @@ export const persistSource = ( · 26 unmodified lines
810757 });
811758 }
812759 yield* rows.authArtifacts.removeByWorkspaceSourceAndActor({
813− workspaceId: source.workspaceId,
814− sourceId: source.id,
760+ workspaceId: nextSource.workspaceId,
761+ sourceId: nextSource.id,
815762 actorAccountId: options.actorAccountId ?? null,
816763 slot: "runtime",
817764 });
@@ -839,8 +786,8 @@ export const persistSource = ( · 21 unmodified lines
839786 });
840787 }
841788 yield* rows.authArtifacts.removeByWorkspaceSourceAndActor({
842− workspaceId: source.workspaceId,
843− sourceId: source.id,
789+ workspaceId: nextSource.workspaceId,
790+ sourceId: nextSource.id,
844791 actorAccountId: options.actorAccountId ?? null,
845792 slot: "import",
846793 });
@@ -861,13 +808,12 @@ export const persistSource = ( · 14 unmodified lines
861808 next: importAuthArtifact,
862809 });
863810
864− const existingSourceState = localWorkspace.workspaceState.sources[configKey];
811+ const existingSourceState = localWorkspace.workspaceState.sources[nextSource.id];
865812 const workspaceState: LocalWorkspaceState = {
866813 ...localWorkspace.workspaceState,
867814 sources: {
868815 ...localWorkspace.workspaceState.sources,
869− [configKey]: {
870− id: nextSource.id,
816+ [nextSource.id]: {
871817 status: nextSource.status,
872818 lastError: nextSource.lastError,
873819 sourceHash: nextSource.sourceHash,
packages/control-plane/src/runtime/workspace-execution-environment.test.ts+0−2
@@ -589,7 +589,6 @@ const persistConnectedEchoTool = (input: { · 588 unmodified lines
589589 const source: Source = {
590590 id: input.sourceId,
591591 workspaceId: input.workspaceId,
592− configKey: null,
593592 name: "Counted MCP",
594593 kind: "mcp",
595594 endpoint: input.endpoint,
@@ -802,7 +801,6 @@ const persistConnectedGithubOpenApiSource = (input: { · 206 unmodified lines
802801 const source: Source = {
803802 id: input.sourceId,
804803 workspaceId: input.workspaceId,
805− configKey: null,
806804 name: "GitHub",
807805 kind: "openapi",
808806 endpoint: input.endpoint ?? "https://api.github.com",
packages/control-plane/src/schema/models/source.ts+0−3
@@ -97,7 +97,6 @@ export const StoredSourceRecordSchema = Schema.transform( · 96 unmodified lines
9797 Schema.Struct({
9898 id: SourceIdSchema,
9999 workspaceId: WorkspaceIdSchema,
100− configKey: Schema.NullOr(Schema.String),
101100 recipeId: SourceRecipeIdSchema,
102101 recipeRevisionId: SourceRecipeRevisionIdSchema,
103102 name: Schema.String,
@@ -118,7 +117,6 @@ export const StoredSourceRecordSchema = Schema.transform( · 14 unmodified lines
118117 decode: (row, _input) => ({
119118 id: row.sourceId,
120119 workspaceId: row.workspaceId,
121− configKey: null,
122120 recipeId: row.recipeId,
123121 recipeRevisionId: row.recipeRevisionId,
124122 name: row.name,
@@ -158,7 +156,6 @@ export const StoredSourceRecordSchema = Schema.transform( · 33 unmodified lines
158156 export const SourceSchema = Schema.Struct({
159157 id: SourceIdSchema,
160158 workspaceId: WorkspaceIdSchema,
161− configKey: Schema.NullOr(Schema.String),
162159 name: Schema.String,
163160 kind: SourceKindSchema,
164161 endpoint: Schema.String,
packages/kitchen-sink/src/source-runtime.test.ts+0−1
@@ -492,7 +492,6 @@ const openApiSource = (input: { · 491 unmodified lines
492492 }): Source => ({
493493 id: input.sourceId,
494494 workspaceId: input.workspaceId,
495− configKey: null,
496495 name: input.displayName,
497496 kind: "openapi",
498497 endpoint: input.baseUrl,
packages/react/src/index.ts+0−1
@@ -661,7 +661,6 @@ const createOptimisticSource = (input: { · 660 unmodified lines
661661 return {
662662 id: `src_optimistic_${crypto.randomUUID()}` as Source["id"],
663663 workspaceId: input.workspaceId,
664− configKey: null,
665664 name: input.payload.name,
666665 kind: input.payload.kind,
667666 endpoint: input.payload.endpoint,