History

Refactor scope config boundary

Rhys Sullivan committed 7 months ago

Commit 2d004cd1 parent 3f52c6036 files · +1834 −360Browse files

Unified · Split

36 files
  1. packages/platform/sdk-file/src/capture-release-workspace-fixture.ts +6 −6
  2. packages/platform/sdk-file/src/config-codec.ts +93 −0
  3. packages/platform/sdk-file/src/config-migrations.test.ts +188 −0
  4. packages/platform/sdk-file/src/config-migrations/index.ts +48 −0
  5. packages/platform/sdk-file/src/config-migrations/legacy-local-config.ts +538 −0
  6. packages/platform/sdk-file/src/config.test.ts +5 −5
  7. packages/platform/sdk-file/src/config.ts +38 −93
  8. packages/platform/sdk-file/src/effect.ts +3 −3
  9. packages/platform/sdk-file/src/errors.ts +2 −2
  10. packages/platform/sdk-file/src/executor-state-codec.ts +82 −0
  11. packages/platform/sdk-file/src/executor-state-migrations.test.ts +231 −0
  12. packages/platform/sdk-file/src/executor-state-migrations/index.ts +32 −0
  13. packages/platform/sdk-file/src/executor-state-migrations/legacy-local-executor-state.ts +356 −0
  14. packages/platform/sdk-file/src/executor-state-store.ts +15 −59
  15. packages/platform/sdk-file/src/index.ts +31 −11
  16. packages/platform/sdk/src/plugins.ts +15 −15
  17. packages/platform/sdk/src/policies/operations.ts +3 −3
  18. packages/platform/sdk/src/runtime/execution/runtime.ts +6 −6
  19. packages/platform/sdk/src/runtime/index.ts +5 −5
  20. packages/platform/sdk/src/runtime/scope-config.ts +5 −5
  21. packages/platform/sdk/src/runtime/scope/runtime-context.ts +2 −2
  22. packages/platform/sdk/src/runtime/scope/scope-sync.ts +4 −4
  23. packages/platform/sdk/src/runtime/scope/storage.ts +4 −4
  24. packages/platform/sdk/src/runtime/sources/source-store/config.ts +10 −20
  25. packages/platform/sdk/src/runtime/sources/source-store/deps.ts +2 −2
  26. packages/platform/sdk/src/runtime/sources/source-store/lifecycle.ts +4 −4
  27. packages/platform/sdk/src/runtime/sources/source-store/plugin-scope-config.ts +7 −9
  28. packages/platform/sdk/src/runtime/sources/source-store/records.ts +2 −2
  29. packages/platform/sdk/src/schema/index.ts +1 −1
  30. packages/platform/sdk/src/schema/models/local-config.ts +0 −68
  31. packages/platform/sdk/src/schema/models/scope-config.ts +65 −0
  32. packages/platform/server/src/index.test.ts +5 −5
  33. plugins/google-discovery/sdk/index.ts +7 −7
  34. plugins/graphql/sdk/index.ts +7 −7
  35. plugins/mcp/sdk/index.ts +5 −5
  36. plugins/openapi/sdk/index.ts +7 −7
packages/platform/sdk-file/src/capture-release-workspace-fixture.ts+6−6
@@ -6,10 +6,10 @@ import * as Data from "effect/Data"; · 5 unmodified lines
6import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
7import * as Exit from "effect/Exit";7import * as Exit from "effect/Exit";
8 8
9import type { LocalExecutorConfig } from "@executor/platform-sdk/schema";9import type { ExecutorScopeConfig } from "@executor/platform-sdk/schema";
10 10
11import {11import {
12 readOptionalLocalExecutorConfig,12 readOptionalExecutorScopeConfig,
13 resolveLocalWorkspaceContext,13 resolveLocalWorkspaceContext,
14} from "./config";14} from "./config";
15import {15import {
@@ -186,8 +186,8 @@ const copyRecursive = ( · 170 unmodified lines
186 });186 });
187 187
188const sanitizeSourceConfig = (188const sanitizeSourceConfig = (
189 sourceConfig: NonNullable<LocalExecutorConfig["sources"]>[string],189 sourceConfig: NonNullable<ExecutorScopeConfig["sources"]>[string],
190): NonNullable<LocalExecutorConfig["sources"]>[string] => sourceConfig;190): NonNullable<ExecutorScopeConfig["sources"]>[string] => sourceConfig;
191 191
192const main = Effect.gen(function* () {192const main = Effect.gen(function* () {
193 const args = yield* Effect.try({193 const args = yield* Effect.try({
@@ -218,7 +218,7 @@ const main = Effect.gen(function* () { · 24 unmodified lines
218 ),218 ),
219 );219 );
220 220
221 const projectConfig = yield* readOptionalLocalExecutorConfig(221 const projectConfig = yield* readOptionalExecutorScopeConfig(
222 context.projectConfigPath,222 context.projectConfigPath,
223 ).pipe(223 ).pipe(
224 Effect.mapError((cause) =>224 Effect.mapError((cause) =>
@@ -293,7 +293,7 @@ const main = Effect.gen(function* () { · 68 unmodified lines
293 sources: {293 sources: {
294 [args.sourceId]: sanitizeSourceConfig(sourceConfig),294 [args.sourceId]: sanitizeSourceConfig(sourceConfig),
295 },295 },
296 } satisfies LocalExecutorConfig;296 } satisfies ExecutorScopeConfig;
297 297
298 const fixtureState = {298 const fixtureState = {
299 version: 1 as const,299 version: 1 as const,
packages/platform/sdk-file/src/config-codec.ts+93−0
@@ -0,0 +1,93 @@
1import {
2 type ParseError as JsoncParseError,
3 parse as parseJsoncDocument,
4 printParseErrorCode,
5} from "jsonc-parser/lib/esm/main.js";
6
7import {
8 ExecutorScopeConfigSchema,
9 type ExecutorScopeConfig,
10} from "@executor/platform-sdk/schema";
11import * as Schema from "effect/Schema";
12import {
13 ExecutorScopeConfigDecodeError,
14 unknownLocalErrorDetails,
15} from "./errors";
16
17export const decodeExecutorScopeConfig = Schema.decodeUnknownSync(
18 ExecutorScopeConfigSchema,
19 {
20 onExcessProperty: "error",
21 },
22);
23
24export const encodeExecutorScopeConfig = (
25 config: ExecutorScopeConfig,
26): string => `${JSON.stringify(config, null, 2)}\n`;
27
28const formatJsoncParseErrors = (
29 content: string,
30 errors: readonly JsoncParseError[],
31): string => {
32 const lines = content.split("\n");
33
34 return errors
35 .map((error) => {
36 const beforeOffset = content.slice(0, error.offset).split("\n");
37 const line = beforeOffset.length;
38 const column = beforeOffset[beforeOffset.length - 1]?.length ?? 0;
39 const lineText = lines[line - 1];
40 const location = `line ${line}, column ${column + 1}`;
41 const detail = printParseErrorCode(error.error);
42
43 if (!lineText) {
44 return `${detail} at ${location}`;
45 }
46
47 return `${detail} at ${location}\n${lineText}`;
48 })
49 .join("\n");
50};
51
52export const parseJsoncValue = (input: {
53 path: string;
54 content: string;
55}): unknown => {
56 const errors: JsoncParseError[] = [];
57 const parsed = parseJsoncDocument(input.content, errors, {
58 allowTrailingComma: true,
59 });
60
61 if (errors.length > 0) {
62 throw new ExecutorScopeConfigDecodeError({
63 message: `Invalid executor config at ${input.path}: ${formatJsoncParseErrors(input.content, errors)}`,
64 path: input.path,
65 details: formatJsoncParseErrors(input.content, errors),
66 });
67 }
68
69 return parsed;
70};
71
72export const parseExecutorScopeConfig = (input: {
73 path: string;
74 content: string;
75}): ExecutorScopeConfig => {
76 try {
77 return decodeExecutorScopeConfig(
78 parseJsoncValue({
79 path: input.path,
80 content: input.content,
81 }),
82 );
83 } catch (cause) {
84 if (cause instanceof ExecutorScopeConfigDecodeError) {
85 throw cause;
86 }
87 throw new ExecutorScopeConfigDecodeError({
88 message: `Invalid executor config at ${input.path}: ${unknownLocalErrorDetails(cause)}`,
89 path: input.path,
90 details: unknownLocalErrorDetails(cause),
91 });
92 }
93};
packages/platform/sdk-file/src/config-migrations.test.ts+188−0
@@ -0,0 +1,188 @@
1import { tmpdir } from "node:os";
2import { join } from "node:path";
3import { FileSystem } from "@effect/platform";
4import { NodeFileSystem } from "@effect/platform-node";
5import { describe, expect, it } from "@effect/vitest";
6import * as Effect from "effect/Effect";
7
8import {
9 loadExecutorScopeConfig,
10 resolveLocalWorkspaceContext,
11} from "./config";
12import {
13 migrateLegacyExecutorScopeConfigs,
14} from "./config-migrations";
15import {
16 createLocalExecutorRepositoriesEffect,
17} from "./index";
18
19const resolveMaybeEffect = <T>(
20 value: T | Promise<T> | Effect.Effect<T, Error, never>,
21): Effect.Effect<T, Error, never> =>
22 Effect.isEffect(value)
23 ? value
24 : value instanceof Promise
25 ? Effect.promise(() => value)
26 : Effect.succeed(value);
27
28const makeWorkspaceRoot = () =>
29 FileSystem.FileSystem.pipe(
30 Effect.flatMap((fs) =>
31 fs.makeTempDirectory({
32 directory: tmpdir(),
33 prefix: "executor-config-migrations-",
34 })
35 ),
36 );
37
38describe("config-migrations", () => {
39 it.effect("rewrites legacy source entries into the current config shape", () =>
40 Effect.gen(function* () {
41 const fs = yield* FileSystem.FileSystem;
42 const workspaceRoot = yield* makeWorkspaceRoot();
43 const configDirectory = join(workspaceRoot, ".executor");
44 yield* fs.makeDirectory(configDirectory, { recursive: true });
45 yield* fs.writeFileString(
46 join(configDirectory, "executor.jsonc"),
47 `{
48 "sources": {
49 "github": {
50 "kind": "openapi",
51 "name": "GitHub",
52 "connection": {
53 "endpoint": "https://api.github.com"
54 },
55 "binding": {
56 "specUrl": "https://example.com/openapi.json",
57 "defaultHeaders": {
58 "x-test": "1"
59 }
60 }
61 },
62 "google-calendar": {
63 "kind": "google_discovery",
64 "name": "Google Calendar",
65 "namespace": "google.calendar",
66 "connection": {
67 "endpoint": "https://calendar-json.googleapis.com/$discovery/rest?version=v3"
68 },
69 "binding": {
70 "service": "calendar",
71 "version": "v3",
72 "discoveryUrl": "https://calendar-json.googleapis.com/$discovery/rest?version=v3",
73 "defaultHeaders": {
74 "x-goog-api-client": "executor"
75 },
76 "scopes": [
77 "https://www.googleapis.com/auth/calendar.readonly"
78 ]
79 }
80 }
81 }
82}
83`,
84 );
85
86 const context = yield* resolveLocalWorkspaceContext({ workspaceRoot });
87 const beforeMigration = yield* Effect.flip(loadExecutorScopeConfig(context));
88 expect(beforeMigration.message).toContain("Invalid executor config");
89
90 const migratedPaths = yield* migrateLegacyExecutorScopeConfigs(context);
91 expect(migratedPaths).toContain(context.projectConfigPath);
92 expect(yield* fs.exists(`${context.projectConfigPath}.legacy-backup`)).toBe(true);
93
94 const loaded = yield* loadExecutorScopeConfig(context);
95 const rewritten = yield* fs.readFileString(context.projectConfigPath, "utf8");
96
97 expect(rewritten).not.toContain("\"connection\"");
98 expect(rewritten).not.toContain("\"binding\"");
99 expect(loaded.config?.sources?.github?.config).toEqual({
100 specUrl: "https://example.com/openapi.json",
101 baseUrl: "https://api.github.com",
102 auth: {
103 kind: "none",
104 },
105 defaultHeaders: {
106 "x-test": "1",
107 },
108 });
109
110 expect(loaded.config?.sources?.["google-calendar"]?.config).toEqual({
111 service: "calendar",
112 version: "v3",
113 discoveryUrl: "https://calendar-json.googleapis.com/$discovery/rest?version=v3",
114 defaultHeaders: {
115 "x-goog-api-client": "executor",
116 },
117 scopes: ["https://www.googleapis.com/auth/calendar.readonly"],
118 auth: {
119 kind: "none",
120 },
121 });
122 }).pipe(Effect.provide(NodeFileSystem.layer)),
123 );
124
125 it.effect("runs startup migrations before the strict config loader", () =>
126 Effect.gen(function* () {
127 const fs = yield* FileSystem.FileSystem;
128 const workspaceRoot = yield* makeWorkspaceRoot();
129 const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc");
130 const homeStateDirectory = join(workspaceRoot, ".executor-home-state");
131 const configDirectory = join(workspaceRoot, ".executor");
132 const projectConfigPath = join(configDirectory, "executor.jsonc");
133 yield* fs.makeDirectory(configDirectory, { recursive: true });
134 yield* fs.writeFileString(
135 projectConfigPath,
136 `{
137 "secrets": {
138 "providers": {},
139 "defaults": {}
140 },
141 "sources": {
142 "github": {
143 "kind": "openapi",
144 "config": {
145 "specUrl": "https://example.com/openapi.json",
146 "baseUrl": "https://api.github.com",
147 "auth": {
148 "kind": "none"
149 },
150 "defaultHeaders": null
151 }
152 }
153 }
154}
155`,
156 );
157
158 const context = yield* resolveLocalWorkspaceContext({
159 workspaceRoot,
160 homeConfigPath,
161 homeStateDirectory,
162 });
163 const beforeStartup = yield* Effect.flip(loadExecutorScopeConfig(context));
164 expect(beforeStartup.message).toContain("Invalid executor config");
165
166 const repositories = yield* createLocalExecutorRepositoriesEffect({
167 workspaceRoot,
168 homeConfigPath,
169 homeStateDirectory,
170 });
171 const rewritten = yield* fs.readFileString(projectConfigPath, "utf8");
172 const loaded = yield* resolveMaybeEffect(
173 repositories.workspace.config.load(),
174 );
175
176 expect(yield* fs.exists(`${projectConfigPath}.legacy-backup`)).toBe(true);
177 expect(rewritten).not.toContain("\"secrets\"");
178 expect(loaded.projectConfig?.sources?.github?.config).toEqual({
179 specUrl: "https://example.com/openapi.json",
180 baseUrl: "https://api.github.com",
181 auth: {
182 kind: "none",
183 },
184 defaultHeaders: null,
185 });
186 }).pipe(Effect.provide(NodeFileSystem.layer)),
187 );
188});
packages/platform/sdk-file/src/config-migrations/index.ts+48−0
@@ -0,0 +1,48 @@
1import { FileSystem } from "@effect/platform";
2import * as Effect from "effect/Effect";
3import type {
4 ExecutorScopeConfigDecodeError,
5 LocalFileSystemError,
6} from "../errors";
7import {
8 migrateLegacyExecutorScopeConfigs,
9 type StartupConfigMigrationContext,
10} from "./legacy-local-config";
11
12type StartupConfigMigration = (
13 context: StartupConfigMigrationContext,
14) => Effect.Effect<
15 ReadonlyArray<string>,
16 LocalFileSystemError | ExecutorScopeConfigDecodeError,
17 FileSystem.FileSystem
18>;
19
20// Keep legacy upgrade steps out of steady-state config loading so they can be
21// extended or deleted without touching the main application path.
22const startupConfigMigrations: readonly StartupConfigMigration[] = [
23 migrateLegacyExecutorScopeConfigs,
24];
25
26export const runStartupConfigMigrations = (
27 context: StartupConfigMigrationContext,
28): Effect.Effect<
29 ReadonlyArray<string>,
30 LocalFileSystemError | ExecutorScopeConfigDecodeError,
31 FileSystem.FileSystem
32> =>
33 Effect.gen(function* () {
34 const migratedPaths = new Set<string>();
35
36 for (const migrate of startupConfigMigrations) {
37 for (const path of yield* migrate(context)) {
38 migratedPaths.add(path);
39 }
40 }
41
42 return [...migratedPaths];
43 });
44
45export {
46 migrateLegacyExecutorScopeConfigs,
47 type StartupConfigMigrationContext,
48} from "./legacy-local-config";
packages/platform/sdk-file/src/config-migrations/legacy-local-config.ts+538−0
@@ -0,0 +1,538 @@
1import { FileSystem } from "@effect/platform";
2import {
3 type ExecutorScopeConfigSource,
4 type ExecutorScopeConfig,
5} from "@executor/platform-sdk/schema";
6import * as Effect from "effect/Effect";
7import {
8 decodeExecutorScopeConfig,
9 encodeExecutorScopeConfig,
10 parseJsoncValue,
11} from "../config-codec";
12import {
13 ExecutorScopeConfigDecodeError,
14 LocalFileSystemError,
15 unknownLocalErrorDetails,
16} from "../errors";
17
18const LEGACY_CONFIG_BACKUP_SUFFIX = ".legacy-backup";
19
20const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
21 new LocalFileSystemError({
22 message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`,
23 action,
24 path,
25 details: unknownLocalErrorDetails(cause),
26 });
27
28const asRecord = (value: unknown): Record<string, unknown> | null =>
29 typeof value === "object" && value !== null && !Array.isArray(value)
30 ? value as Record<string, unknown>
31 : null;
32
33const asString = (value: unknown): string | null =>
34 typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
35
36const asStringArray = (value: unknown): Array<string> | null => {
37 if (!Array.isArray(value)) {
38 return null;
39 }
40
41 const normalized = value
42 .map((entry) => asString(entry))
43 .filter((entry): entry is string => entry !== null);
44
45 return normalized.length > 0 ? normalized : [];
46};
47
48const asStringRecord = (value: unknown): Record<string, string> | null => {
49 const record = asRecord(value);
50 if (!record) {
51 return null;
52 }
53
54 const normalized = Object.fromEntries(
55 Object.entries(record).flatMap(([key, entry]) => {
56 const normalizedKey = asString(key);
57 const normalizedValue = asString(entry);
58 return normalizedKey && normalizedValue
59 ? [[normalizedKey, normalizedValue]]
60 : [];
61 }),
62 );
63
64 return Object.keys(normalized).length > 0 ? normalized : null;
65};
66
67const asFiniteNumber = (value: unknown): number | null =>
68 typeof value === "number" && Number.isFinite(value) ? value : null;
69
70const asBoolean = (value: unknown): boolean | null =>
71 typeof value === "boolean" ? value : null;
72
73const asSecretRef = (value: unknown): { secretId: string } | null => {
74 const record = asRecord(value);
75 const secretId = asString(record?.secretId);
76 return secretId ? { secretId } : null;
77};
78
79const normalizeLegacyHeaderName = (value: unknown): string | null =>
80 asString(value);
81
82const normalizeLegacyMcpTransport = (
83 value: unknown,
84): "streamable-http" | "sse" | "stdio" | "auto" | null => {
85 const candidate = asString(value);
86 return candidate === "streamable-http"
87 || candidate === "sse"
88 || candidate === "stdio"
89 || candidate === "auto"
90 ? candidate
91 : null;
92};
93
94const defaultGoogleDiscoveryUrl = (
95 service: string,
96 version: string,
97): string =>
98 `https://www.googleapis.com/discovery/v1/apis/${encodeURIComponent(service)}/${encodeURIComponent(version)}/rest`;
99
100const legacyMigrationError = (input: {
101 path: string;
102 sourceId: string;
103 details: string;
104}) =>
105 new ExecutorScopeConfigDecodeError({
106 message: `Unsupported legacy executor config at ${input.path} for source ${input.sourceId}: ${input.details}`,
107 path: input.path,
108 details: input.details,
109 });
110
111const normalizeCurrentSourceBase = (
112 source: Record<string, unknown>,
113): Omit<ExecutorScopeConfigSource, "kind" | "config"> => ({
114 ...(asString(source.name) ? { name: asString(source.name)! } : {}),
115 ...(asString(source.namespace) ? { namespace: asString(source.namespace)! } : {}),
116 ...(asBoolean(source.enabled) !== null ? { enabled: source.enabled as boolean } : {}),
117});
118
119const normalizeLegacyBearerAuth = (input: {
120 path: string;
121 sourceId: string;
122 value: unknown;
123}) => {
124 const record = asRecord(input.value);
125 const kind = asString(record?.kind);
126
127 if (input.value === undefined || kind === "none") {
128 return { kind: "none" } as const;
129 }
130
131 const tokenSecretRef = asSecretRef(record?.tokenSecretRef ?? input.value);
132 if (!tokenSecretRef) {
133 throw legacyMigrationError({
134 path: input.path,
135 sourceId: input.sourceId,
136 details:
137 "legacy bearer auth references config providers and cannot be auto-migrated yet",
138 });
139 }
140
141 return {
142 kind: "bearer" as const,
143 tokenSecretRef,
144 headerName: normalizeLegacyHeaderName(record?.headerName),
145 prefix: asString(record?.prefix),
146 };
147};
148
149const normalizeLegacyMcpAuth = (input: {
150 path: string;
151 sourceId: string;
152 value: unknown;
153}) => {
154 const record = asRecord(input.value);
155 const kind = asString(record?.kind);
156
157 if (input.value === undefined || kind === "none") {
158 return { kind: "none" } as const;
159 }
160
161 if (kind !== "oauth2") {
162 throw legacyMigrationError({
163 path: input.path,
164 sourceId: input.sourceId,
165 details: "legacy MCP auth is not compatible with the current config format",
166 });
167 }
168
169 const accessTokenRef = asSecretRef(record?.accessTokenRef);
170 const redirectUri = asString(record?.redirectUri);
171 if (!accessTokenRef || !redirectUri) {
172 throw legacyMigrationError({
173 path: input.path,
174 sourceId: input.sourceId,
175 details: "legacy MCP oauth2 auth is missing required secret refs",
176 });
177 }
178
179 return {
180 kind: "oauth2" as const,
181 redirectUri,
182 accessTokenRef,
183 refreshTokenRef: asSecretRef(record?.refreshTokenRef),
184 tokenType: asString(record?.tokenType) ?? "Bearer",
185 expiresAt: asFiniteNumber(record?.expiresAt),
186 scope: asString(record?.scope),
187 resourceMetadataUrl: asString(record?.resourceMetadataUrl),
188 authorizationServerUrl: asString(record?.authorizationServerUrl),
189 resourceMetadata: asRecord(record?.resourceMetadata),
190 authorizationServerMetadata: asRecord(record?.authorizationServerMetadata),
191 clientInformation: asRecord(record?.clientInformation),
192 };
193};
194
195const normalizeLegacyGoogleDiscoveryAuth = (input: {
196 path: string;
197 sourceId: string;
198 value: unknown;
199}) => {
200 const record = asRecord(input.value);
201 const kind = asString(record?.kind);
202
203 if (kind === "oauth2") {
204 const clientId = asString(record?.clientId);
205 const authorizationEndpoint = asString(record?.authorizationEndpoint);
206 const tokenEndpoint = asString(record?.tokenEndpoint);
207 const accessTokenRef = asSecretRef(record?.accessTokenRef);
208
209 if (!clientId || !authorizationEndpoint || !tokenEndpoint || !accessTokenRef) {
210 throw legacyMigrationError({
211 path: input.path,
212 sourceId: input.sourceId,
213 details: "legacy Google Discovery oauth2 auth is missing required secret refs",
214 });
215 }
216
217 const clientAuthentication =
218 asString(record?.clientAuthentication) === "client_secret_post"
219 ? "client_secret_post"
220 : "none";
221
222 return {
223 kind: "oauth2" as const,
224 clientId,
225 clientSecretRef: asSecretRef(record?.clientSecretRef),
226 clientAuthentication,
227 authorizationEndpoint,
228 tokenEndpoint,
229 scopes: asStringArray(record?.scopes) ?? [],
230 accessTokenRef,
231 refreshTokenRef: asSecretRef(record?.refreshTokenRef),
232 expiresAt: asFiniteNumber(record?.expiresAt),
233 };
234 }
235
236 return normalizeLegacyBearerAuth(input);
237};
238
239const normalizeSourceRecord = (input: {
240 path: string;
241 sourceId: string;
242 source: Record<string, unknown>;
243}): {
244 source: ExecutorScopeConfigSource | Record<string, unknown>;
245 migrated: boolean;
246} => {
247 const kind = asString(input.source.kind);
248 const config = input.source.config;
249 const connection = asRecord(input.source.connection);
250 const binding = asRecord(input.source.binding);
251 const base = normalizeCurrentSourceBase(input.source);
252
253 if (config !== undefined) {
254 return {
255 source: {
256 ...base,
257 ...(kind ? { kind } : {}),
258 config,
259 },
260 migrated:
261 Object.prototype.hasOwnProperty.call(input.source, "connection")
262 || Object.prototype.hasOwnProperty.call(input.source, "binding"),
263 };
264 }
265
266 if (!kind || (connection === null && binding === null)) {
267 return {
268 source: input.source,
269 migrated: false,
270 };
271 }
272
273 switch (kind) {
274 case "openapi": {
275 const baseUrl = asString(connection?.endpoint);
276 const specUrl = asString(binding?.specUrl) ?? baseUrl;
277 if (!specUrl) {
278 return {
279 source: input.source,
280 migrated: false,
281 };
282 }
283
284 return {
285 source: {
286 ...base,
287 kind: "openapi",
288 config: {
289 specUrl,
290 baseUrl,
291 auth: normalizeLegacyBearerAuth({
292 path: input.path,
293 sourceId: input.sourceId,
294 value: connection?.auth,
295 }),
296 defaultHeaders: asStringRecord(binding?.defaultHeaders),
297 },
298 },
299 migrated: true,
300 };
301 }
302
303 case "graphql": {
304 const endpoint = asString(connection?.endpoint);
305 if (!endpoint) {
306 return {
307 source: input.source,
308 migrated: false,
309 };
310 }
311
312 return {
313 source: {
314 ...base,
315 kind: "graphql",
316 config: {
317 endpoint,
318 defaultHeaders: asStringRecord(binding?.defaultHeaders),
319 auth: normalizeLegacyBearerAuth({
320 path: input.path,
321 sourceId: input.sourceId,
322 value: connection?.auth,
323 }),
324 },
325 },
326 migrated: true,
327 };
328 }
329
330 case "mcp":
331 return {
332 source: {
333 ...base,
334 kind: "mcp",
335 config: {
336 endpoint: asString(connection?.endpoint),
337 transport: normalizeLegacyMcpTransport(binding?.transport),
338 queryParams: asStringRecord(binding?.queryParams),
339 headers: asStringRecord(binding?.headers),
340 command: asString(binding?.command),
341 args: asStringArray(binding?.args),
342 env: asStringRecord(binding?.env),
343 cwd: asString(binding?.cwd),
344 auth: normalizeLegacyMcpAuth({
345 path: input.path,
346 sourceId: input.sourceId,
347 value: connection?.auth,
348 }),
349 },
350 },
351 migrated: true,
352 };
353
354 case "google_discovery": {
355 const service = asString(binding?.service);
356 const version = asString(binding?.version);
357 if (!service || !version) {
358 return {
359 source: input.source,
360 migrated: false,
361 };
362 }
363
364 return {
365 source: {
366 ...base,
367 kind: "google_discovery",
368 config: {
369 service,
370 version,
371 discoveryUrl:
372 asString(binding?.discoveryUrl)
373 ?? asString(connection?.endpoint)
374 ?? defaultGoogleDiscoveryUrl(service, version),
375 defaultHeaders: asStringRecord(binding?.defaultHeaders),
376 scopes: asStringArray(binding?.scopes) ?? [],
377 auth: normalizeLegacyGoogleDiscoveryAuth({
378 path: input.path,
379 sourceId: input.sourceId,
380 value: connection?.auth,
381 }),
382 },
383 },
384 migrated: true,
385 };
386 }
387
388 default:
389 return {
390 source: input.source,
391 migrated: false,
392 };
393 }
394};
395
396const migrateLegacyExecutorConfigValue = (input: {
397 path: string;
398 value: unknown;
399}): {
400 config: ExecutorScopeConfig;
401 migrated: boolean;
402} => {
403 const root = asRecord(input.value);
404 if (root === null) {
405 return {
406 config: decodeExecutorScopeConfig(input.value),
407 migrated: false,
408 };
409 }
410
411 let migrated = Object.prototype.hasOwnProperty.call(root, "secrets");
412 const rawSources = asRecord(root.sources);
413 const sources = rawSources
414 ? Object.fromEntries(
415 Object.entries(rawSources).map(([sourceId, sourceValue]) => {
416 const sourceRecord = asRecord(sourceValue);
417 if (sourceRecord === null) {
418 return [sourceId, sourceValue];
419 }
420
421 const normalized = normalizeSourceRecord({
422 path: input.path,
423 sourceId,
424 source: sourceRecord,
425 });
426 migrated ||= normalized.migrated;
427 return [sourceId, normalized.source];
428 }),
429 )
430 : root.sources;
431
432 return {
433 config: decodeExecutorScopeConfig({
434 ...(root.runtime !== undefined ? { runtime: root.runtime } : {}),
435 ...(root.workspace !== undefined ? { workspace: root.workspace } : {}),
436 ...(sources !== undefined ? { sources } : {}),
437 ...(root.policies !== undefined ? { policies: root.policies } : {}),
438 }),
439 migrated,
440 };
441};
442
443const migrateLegacyExecutorConfigFile = (
444 path: string,
445): Effect.Effect<
446 boolean,
447 LocalFileSystemError | ExecutorScopeConfigDecodeError,
448 FileSystem.FileSystem
449> =>
450 Effect.gen(function* () {
451 const fs = yield* FileSystem.FileSystem;
452 const exists = yield* fs.exists(path).pipe(
453 Effect.mapError(mapFileSystemError(path, "check config path")),
454 );
455 if (!exists) {
456 return false;
457 }
458
459 const content = yield* fs.readFileString(path, "utf8").pipe(
460 Effect.mapError(mapFileSystemError(path, "read config")),
461 );
462 const parsed = yield* Effect.try({
463 try: () =>
464 parseJsoncValue({
465 path,
466 content,
467 }),
468 catch: (cause) =>
469 cause instanceof ExecutorScopeConfigDecodeError
470 ? cause
471 : new ExecutorScopeConfigDecodeError({
472 message: `Invalid executor config at ${path}: ${unknownLocalErrorDetails(cause)}`,
473 path,
474 details: unknownLocalErrorDetails(cause),
475 }),
476 });
477 const migrated = yield* Effect.try({
478 try: () =>
479 migrateLegacyExecutorConfigValue({
480 path,
481 value: parsed,
482 }),
483 catch: (cause) =>
484 cause instanceof ExecutorScopeConfigDecodeError
485 ? cause
486 : new ExecutorScopeConfigDecodeError({
487 message: `Invalid executor config at ${path}: ${unknownLocalErrorDetails(cause)}`,
488 path,
489 details: unknownLocalErrorDetails(cause),
490 }),
491 });
492
493 if (!migrated.migrated) {
494 return false;
495 }
496
497 const backupPath = `${path}${LEGACY_CONFIG_BACKUP_SUFFIX}`;
498 const backupExists = yield* fs.exists(backupPath).pipe(
499 Effect.mapError(mapFileSystemError(backupPath, "check backup path")),
500 );
501 if (!backupExists) {
502 yield* fs.writeFileString(backupPath, content).pipe(
503 Effect.mapError(mapFileSystemError(backupPath, "write legacy config backup")),
504 );
505 }
506
507 yield* fs.writeFileString(path, encodeExecutorScopeConfig(migrated.config)).pipe(
508 Effect.mapError(mapFileSystemError(path, "write migrated config")),
509 );
510
511 return true;
512 });
513
514export type StartupConfigMigrationContext = {
515 homeConfigPath: string;
516 projectConfigPath: string;
517};
518
519export const migrateLegacyExecutorScopeConfigs = (
520 context: StartupConfigMigrationContext,
521): Effect.Effect<
522 ReadonlyArray<string>,
523 LocalFileSystemError | ExecutorScopeConfigDecodeError,
524 FileSystem.FileSystem
525> =>
526 Effect.gen(function* () {
527 const migratedPaths: Array<string> = [];
528 const paths = [...new Set([context.homeConfigPath, context.projectConfigPath])];
529
530 for (const path of paths) {
531 const migrated = yield* migrateLegacyExecutorConfigFile(path);
532 if (migrated) {
533 migratedPaths.push(path);
534 }
535 }
536
537 return migratedPaths;
538 });
packages/platform/sdk-file/src/config.test.ts+5−5
@@ -6,8 +6,8 @@ import { describe, expect, it } from "@effect/vitest"; · 5 unmodified lines
6import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
7 7
8import {8import {
9 loadLocalExecutorConfig,9 loadExecutorScopeConfig,
10 mergeLocalExecutorConfigs,10 mergeExecutorScopeConfigs,
11 resolveDefaultHomeConfigCandidates,11 resolveDefaultHomeConfigCandidates,
12 resolveDefaultHomeStateDirectory,12 resolveDefaultHomeStateDirectory,
13 resolveLocalWorkspaceContext,13 resolveLocalWorkspaceContext,
@@ -53,7 +53,7 @@ describe("local-config", () => { · 39 unmodified lines
53 );53 );
54 54
55 const context = yield* resolveLocalWorkspaceContext({ workspaceRoot });55 const context = yield* resolveLocalWorkspaceContext({ workspaceRoot });
56 const loaded = yield* loadLocalExecutorConfig(context);56 const loaded = yield* loadExecutorScopeConfig(context);
57 57
58 expect(loaded.config?.sources?.github?.kind).toBe("openapi");58 expect(loaded.config?.sources?.github?.kind).toBe("openapi");
59 expect(loaded.config?.runtime).toBe("ses");59 expect(loaded.config?.runtime).toBe("ses");
@@ -83,7 +83,7 @@ describe("local-config", () => { · 23 unmodified lines
83 );83 );
84 84
85 const context = yield* resolveLocalWorkspaceContext({ workspaceRoot });85 const context = yield* resolveLocalWorkspaceContext({ workspaceRoot });
86 const failure = yield* Effect.flip(loadLocalExecutorConfig(context));86 const failure = yield* Effect.flip(loadExecutorScopeConfig(context));
87 87
88 expect(failure.message).toContain("Invalid executor config");88 expect(failure.message).toContain("Invalid executor config");
89 expect(failure.message).toContain("line 5, column 7");89 expect(failure.message).toContain("line 5, column 7");
@@ -129,7 +129,7 @@ describe("local-config", () => { · 39 unmodified lines
129 });129 });
130 130
131 it("lets project config override the merged runtime", () => {131 it("lets project config override the merged runtime", () => {
132 const merged = mergeLocalExecutorConfigs(132 const merged = mergeExecutorScopeConfigs(
133 {133 {
134 runtime: "quickjs",134 runtime: "quickjs",
135 sources: {},135 sources: {},
packages/platform/sdk-file/src/config.ts+38−93
@@ -1,37 +1,30 @@
1import { homedir } from "node:os";1import { homedir } from "node:os";
2import { basename, dirname, isAbsolute, join, resolve } from "node:path";2import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3import { FileSystem } from "@effect/platform";3import { FileSystem } from "@effect/platform";
4import {
5 type ParseError as JsoncParseError,
6 parse as parseJsoncDocument,
7 printParseErrorCode,
8} from "jsonc-parser/lib/esm/main.js";
9 4
10import {5import {
11 LocalExecutorConfigSchema,6 type ExecutorScopeConfig,
12 type LocalExecutorConfig,7 type ExecutorScopeConfigPolicy,
13 type LocalConfigPolicy,8 type ExecutorScopeConfigSource,
14 type LocalConfigSource,
15} from "@executor/platform-sdk/schema";9} from "@executor/platform-sdk/schema";
16import type { LoadedLocalExecutorConfig } from "@executor/platform-sdk/runtime";10import type { LoadedExecutorScopeConfig } from "@executor/platform-sdk/runtime";
17import * as Effect from "effect/Effect";11import * as Effect from "effect/Effect";
18import * as Schema from "effect/Schema";
19import {12import {
20 LocalExecutorConfigDecodeError,13 decodeExecutorScopeConfig,
14 encodeExecutorScopeConfig,
15 parseExecutorScopeConfig,
16} from "./config-codec";
17import {
18 ExecutorScopeConfigDecodeError,
21 LocalFileSystemError,19 LocalFileSystemError,
22 unknownLocalErrorDetails,20 unknownLocalErrorDetails,
23} from "./errors";21} from "./errors";
24 22
25const decodeLocalExecutorConfig = Schema.decodeUnknownSync(LocalExecutorConfigSchema);23export type FileLoadedExecutorConfig = LoadedExecutorScopeConfig & {
26
27export type FileLoadedExecutorConfig = LoadedLocalExecutorConfig & {
28 homeConfigPath: string;24 homeConfigPath: string;
29 projectConfigPath: string;25 projectConfigPath: string;
30};26};
31 27
32export const encodeLocalExecutorConfig = (config: LocalExecutorConfig): string =>
33 `${JSON.stringify(config, null, 2)}\n`;
34
35export const resolveConfigRelativePath = (input: {28export const resolveConfigRelativePath = (input: {
36 path: string;29 path: string;
37 scopeRoot: string;30 scopeRoot: string;
@@ -49,7 +42,8 @@ export const resolveConfigRelativePath = (input: { · 11 unmodified lines
49 return resolve(input.scopeRoot, trimmed);42 return resolve(input.scopeRoot, trimmed);
50};43};
51 44
52export type { LoadedLocalExecutorConfig } from "@executor/platform-sdk/runtime";45export type { LoadedExecutorScopeConfig } from "@executor/platform-sdk/runtime";
46export { encodeExecutorScopeConfig } from "./config-codec";
53 47
54const PROJECT_CONFIG_BASENAME = "executor.jsonc";48const PROJECT_CONFIG_BASENAME = "executor.jsonc";
55const PROJECT_CONFIG_DIRECTORY = ".executor";49const PROJECT_CONFIG_DIRECTORY = ".executor";
@@ -173,59 +167,10 @@ export const resolveDefaultHomeStateDirectory = (input: { · 117 unmodified lines
173} = {}): string =>167} = {}): string =>
174 defaultExecutorStateDirectory(input);168 defaultExecutorStateDirectory(input);
175 169
176const formatJsoncParseErrors = (content: string, errors: readonly JsoncParseError[]): string => {
177 const lines = content.split("\n");
178
179 return errors
180 .map((error) => {
181 const beforeOffset = content.slice(0, error.offset).split("\n");
182 const line = beforeOffset.length;
183 const column = beforeOffset[beforeOffset.length - 1]?.length ?? 0;
184 const lineText = lines[line - 1];
185 const location = `line ${line}, column ${column + 1}`;
186 const detail = printParseErrorCode(error.error);
187
188 if (!lineText) {
189 return `${detail} at ${location}`;
190 }
191
192 return `${detail} at ${location}\n${lineText}`;
193 })
194 .join("\n");
195};
196
197const parseJsonc = (input: { path: string; content: string }): LocalExecutorConfig => {
198 const errors: JsoncParseError[] = [];
199
200 try {
201 const parsed = parseJsoncDocument(input.content, errors, {
202 allowTrailingComma: true,
203 });
204 if (errors.length > 0) {
205 throw new LocalExecutorConfigDecodeError({
206 message: `Invalid executor config at ${input.path}: ${formatJsoncParseErrors(input.content, errors)}`,
207 path: input.path,
208 details: formatJsoncParseErrors(input.content, errors),
209 });
210 }
211
212 return decodeLocalExecutorConfig(parsed);
213 } catch (cause) {
214 if (cause instanceof LocalExecutorConfigDecodeError) {
215 throw cause;
216 }
217 throw new LocalExecutorConfigDecodeError({
218 message: `Invalid executor config at ${input.path}: ${unknownLocalErrorDetails(cause)}`,
219 path: input.path,
220 details: unknownLocalErrorDetails(cause),
221 });
222 }
223};
224
225const mergeSourceMaps = (170const mergeSourceMaps = (
226 base: Record<string, LocalConfigSource> | undefined,171 base: Record<string, ExecutorScopeConfigSource> | undefined,
227 extra: Record<string, LocalConfigSource> | undefined,172 extra: Record<string, ExecutorScopeConfigSource> | undefined,
228): Record<string, LocalConfigSource> | undefined => {173): Record<string, ExecutorScopeConfigSource> | undefined => {
229 if (!base && !extra) {174 if (!base && !extra) {
230 return undefined;175 return undefined;
231 }176 }
@@ -236,9 +181,9 @@ const mergeSourceMaps = ( · 4 unmodified lines
236};181};
237 182
238const mergePolicyMaps = (183const mergePolicyMaps = (
239 base: Record<string, LocalConfigPolicy> | undefined,184 base: Record<string, ExecutorScopeConfigPolicy> | undefined,
240 extra: Record<string, LocalConfigPolicy> | undefined,185 extra: Record<string, ExecutorScopeConfigPolicy> | undefined,
241): Record<string, LocalConfigPolicy> | undefined => {186): Record<string, ExecutorScopeConfigPolicy> | undefined => {
242 if (!base && !extra) {187 if (!base && !extra) {
243 return undefined;188 return undefined;
244 }189 }
@@ -248,15 +193,15 @@ const mergePolicyMaps = ( · 3 unmodified lines
248 };193 };
249};194};
250 195
251export const mergeLocalExecutorConfigs = (196export const mergeExecutorScopeConfigs = (
252 base: LocalExecutorConfig | null,197 base: ExecutorScopeConfig | null,
253 extra: LocalExecutorConfig | null,198 extra: ExecutorScopeConfig | null,
254): LocalExecutorConfig | null => {199): ExecutorScopeConfig | null => {
255 if (!base && !extra) {200 if (!base && !extra) {
256 return null;201 return null;
257 }202 }
258 203
259 return decodeLocalExecutorConfig({204 return decodeExecutorScopeConfig({
260 runtime: extra?.runtime ?? base?.runtime,205 runtime: extra?.runtime ?? base?.runtime,
261 workspace: {206 workspace: {
262 ...base?.workspace,207 ...base?.workspace,
@@ -370,11 +315,11 @@ export const resolveLocalWorkspaceContext = (input: { · 107 unmodified lines
370 };315 };
371 });316 });
372 317
373export const readOptionalLocalExecutorConfig = (318export const readOptionalExecutorScopeConfig = (
374 path: string,319 path: string,
375): Effect.Effect<320): Effect.Effect<
376 LocalExecutorConfig | null,321 ExecutorScopeConfig | null,
377 LocalFileSystemError | LocalExecutorConfigDecodeError,322 LocalFileSystemError | ExecutorScopeConfigDecodeError,
378 FileSystem.FileSystem323 FileSystem.FileSystem
379> =>324> =>
380 Effect.gen(function* () {325 Effect.gen(function* () {
@@ -390,11 +335,11 @@ export const readOptionalLocalExecutorConfig = ( · 9 unmodified lines
390 Effect.mapError(mapFileSystemError(path, "read config")),335 Effect.mapError(mapFileSystemError(path, "read config")),
391 );336 );
392 return yield* Effect.try({337 return yield* Effect.try({
393 try: () => parseJsonc({ path, content }),338 try: () => parseExecutorScopeConfig({ path, content }),
394 catch: (cause) =>339 catch: (cause) =>
395 cause instanceof LocalExecutorConfigDecodeError340 cause instanceof ExecutorScopeConfigDecodeError
396 ? cause341 ? cause
397 : new LocalExecutorConfigDecodeError({342 : new ExecutorScopeConfigDecodeError({
398 message: `Invalid executor config at ${path}: ${unknownLocalErrorDetails(cause)}`,343 message: `Invalid executor config at ${path}: ${unknownLocalErrorDetails(cause)}`,
399 path,344 path,
400 details: unknownLocalErrorDetails(cause),345 details: unknownLocalErrorDetails(cause),
@@ -402,21 +347,21 @@ export const readOptionalLocalExecutorConfig = ( · 1 unmodified line
402 });347 });
403 });348 });
404 349
405export const loadLocalExecutorConfig = (350export const loadExecutorScopeConfig = (
406 context: ResolvedLocalWorkspaceContext,351 context: ResolvedLocalWorkspaceContext,
407): Effect.Effect<352): Effect.Effect<
408 FileLoadedExecutorConfig,353 FileLoadedExecutorConfig,
409 LocalFileSystemError | LocalExecutorConfigDecodeError,354 LocalFileSystemError | ExecutorScopeConfigDecodeError,
410 FileSystem.FileSystem355 FileSystem.FileSystem
411> =>356> =>
412 Effect.gen(function* () {357 Effect.gen(function* () {
413 const [homeConfig, projectConfig] = yield* Effect.all([358 const [homeConfig, projectConfig] = yield* Effect.all([
414 readOptionalLocalExecutorConfig(context.homeConfigPath),359 readOptionalExecutorScopeConfig(context.homeConfigPath),
415 readOptionalLocalExecutorConfig(context.projectConfigPath),360 readOptionalExecutorScopeConfig(context.projectConfigPath),
416 ]);361 ]);
417 362
418 return {363 return {
419 config: mergeLocalExecutorConfigs(homeConfig, projectConfig),364 config: mergeExecutorScopeConfigs(homeConfig, projectConfig),
420 homeConfig,365 homeConfig,
421 projectConfig,366 projectConfig,
422 homeConfigPath: context.homeConfigPath,367 homeConfigPath: context.homeConfigPath,
@@ -424,9 +369,9 @@ export const loadLocalExecutorConfig = ( · 1 unmodified line
424 };369 };
425 });370 });
426 371
427export const writeProjectLocalExecutorConfig = (input: {372export const writeProjectExecutorScopeConfig = (input: {
428 context: ResolvedLocalWorkspaceContext;373 context: ResolvedLocalWorkspaceContext;
429 config: LocalExecutorConfig;374 config: ExecutorScopeConfig;
430}): Effect.Effect<void, LocalFileSystemError, FileSystem.FileSystem> =>375}): Effect.Effect<void, LocalFileSystemError, FileSystem.FileSystem> =>
431 Effect.gen(function* () {376 Effect.gen(function* () {
432 const fs = yield* FileSystem.FileSystem;377 const fs = yield* FileSystem.FileSystem;
@@ -435,7 +380,7 @@ export const writeProjectLocalExecutorConfig = (input: { · 2 unmodified lines
435 );380 );
436 yield* fs.writeFileString(381 yield* fs.writeFileString(
437 input.context.projectConfigPath,382 input.context.projectConfigPath,
438 encodeLocalExecutorConfig(input.config),383 encodeExecutorScopeConfig(input.config),
439 ).pipe(384 ).pipe(
440 Effect.mapError(mapFileSystemError(input.context.projectConfigPath, "write config")),385 Effect.mapError(mapFileSystemError(input.context.projectConfigPath, "write config")),
441 );386 );
packages/platform/sdk-file/src/effect.ts+3−3
@@ -5,7 +5,7 @@ export { · 4 unmodified lines
5 createLocalExecutorRepositoriesEffect,5 createLocalExecutorRepositoriesEffect,
6 deriveLocalInstallation,6 deriveLocalInstallation,
7 getOrProvisionLocalInstallation,7 getOrProvisionLocalInstallation,
8 loadLocalExecutorConfig,8 loadExecutorScopeConfig,
9 loadLocalExecutorStateSnapshot,9 loadLocalExecutorStateSnapshot,
10 loadLocalInstallation,10 loadLocalInstallation,
11 loadLocalWorkspaceState,11 loadLocalWorkspaceState,
@@ -20,9 +20,9 @@ export { · 8 unmodified lines
20 writeLocalExecutorStateSnapshot,20 writeLocalExecutorStateSnapshot,
21 writeLocalSourceArtifact,21 writeLocalSourceArtifact,
22 writeLocalWorkspaceState,22 writeLocalWorkspaceState,
23 writeProjectLocalExecutorConfig,23 writeProjectExecutorScopeConfig,
24} from "./index";24} from "./index";
25export type {25export type {
26 LoadedLocalExecutorConfig,26 LoadedExecutorScopeConfig,
27 ResolvedLocalWorkspaceContext,27 ResolvedLocalWorkspaceContext,
28} from "./index";28} from "./index";
packages/platform/sdk-file/src/errors.ts+2−2
@@ -12,8 +12,8 @@ export class LocalFileSystemError extends Data.TaggedError( · 11 unmodified lines
12 readonly details: string;12 readonly details: string;
13}> {}13}> {}
14 14
15export class LocalExecutorConfigDecodeError extends Data.TaggedError(15export class ExecutorScopeConfigDecodeError extends Data.TaggedError(
16 "LocalExecutorConfigDecodeError",16 "ExecutorScopeConfigDecodeError",
17)<{17)<{
18 readonly message: string;18 readonly message: string;
19 readonly path: string;19 readonly path: string;
packages/platform/sdk-file/src/executor-state-codec.ts+82−0
@@ -0,0 +1,82 @@
1import { join } from "node:path";
2
3import {
4 type Execution,
5 type ExecutionInteraction,
6 type ExecutionStep,
7 ExecutionInteractionSchema,
8 ExecutionSchema,
9 ExecutionStepSchema,
10 SecretMaterialSchema,
11 type SecretMaterial,
12 SecretStoreSchema,
13 type SecretStore,
14} from "@executor/platform-sdk/schema";
15import * as Schema from "effect/Schema";
16
17import type { ResolvedLocalWorkspaceContext } from "./config";
18import { deriveLocalInstallation } from "./installation";
19
20export const LOCAL_EXECUTOR_STATE_VERSION = 2 as const;
21export const LOCAL_EXECUTOR_STATE_BASENAME = "executor-state.json";
22
23export const SecretMaterialStoredDataRecordSchema = Schema.Struct({
24 secretId: Schema.String,
25 data: Schema.Unknown,
26});
27
28export type SecretMaterialStoredDataRecord =
29 typeof SecretMaterialStoredDataRecordSchema.Type;
30
31export const LocalExecutorStateSnapshotSchema = Schema.Struct({
32 version: Schema.Literal(LOCAL_EXECUTOR_STATE_VERSION),
33 secretStores: Schema.Array(SecretStoreSchema),
34 secretMaterials: Schema.Array(SecretMaterialSchema),
35 secretMaterialStoredData: Schema.Array(SecretMaterialStoredDataRecordSchema),
36 executions: Schema.Array(ExecutionSchema),
37 executionInteractions: Schema.Array(ExecutionInteractionSchema),
38 executionSteps: Schema.Array(ExecutionStepSchema),
39});
40
41export type LocalExecutorStateSnapshot =
42 typeof LocalExecutorStateSnapshotSchema.Type;
43
44export const decodeLocalExecutorStateSnapshot = Schema.decodeUnknownSync(
45 LocalExecutorStateSnapshotSchema,
46 {
47 onExcessProperty: "error",
48 },
49);
50
51export const encodeLocalExecutorStateSnapshot = (
52 state: LocalExecutorStateSnapshot,
53): string => `${JSON.stringify(state, null, 2)}\n`;
54
55export const defaultLocalExecutorStateSnapshot =
56 (): LocalExecutorStateSnapshot => ({
57 version: LOCAL_EXECUTOR_STATE_VERSION,
58 secretStores: [],
59 secretMaterials: [],
60 secretMaterialStoredData: [],
61 executions: [],
62 executionInteractions: [],
63 executionSteps: [],
64 });
65
66export const localExecutorStatePath = (
67 context: ResolvedLocalWorkspaceContext,
68): string =>
69 join(
70 context.homeStateDirectory,
71 "workspaces",
72 deriveLocalInstallation(context).scopeId,
73 LOCAL_EXECUTOR_STATE_BASENAME,
74 );
75
76export type {
77 Execution,
78 ExecutionInteraction,
79 ExecutionStep,
80 SecretMaterial,
81 SecretStore,
82};
packages/platform/sdk-file/src/executor-state-migrations.test.ts+231−0
@@ -0,0 +1,231 @@
1import { dirname, join } from "node:path";
2
3import { FileSystem } from "@effect/platform";
4import { NodeFileSystem } from "@effect/platform-node";
5import { describe, expect, it } from "@effect/vitest";
6import * as Effect from "effect/Effect";
7
8import { resolveLocalWorkspaceContext } from "./config";
9import {
10 createLocalExecutorRepositoriesEffect,
11 loadLocalExecutorStateSnapshot,
12} from "./index";
13import { migrateLegacyLocalExecutorState } from "./executor-state-migrations";
14import { localExecutorStatePath } from "./executor-state-store";
15import { deriveLocalInstallation } from "./installation";
16
17const makeWorkspaceRoot = () =>
18 FileSystem.FileSystem.pipe(
19 Effect.flatMap((fs) =>
20 fs.makeTempDirectory({
21 prefix: "executor-state-migrations-",
22 })
23 ),
24 );
25
26describe("executor-state-migrations", () => {
27 it.effect("rewrites legacy executor state into the current snapshot shape", () =>
28 Effect.gen(function* () {
29 const fs = yield* FileSystem.FileSystem;
30 const workspaceRoot = yield* makeWorkspaceRoot();
31 const homeStateDirectory = join(workspaceRoot, ".home");
32 const context = yield* resolveLocalWorkspaceContext({
33 workspaceRoot,
34 homeConfigPath: join(workspaceRoot, ".executor-home.jsonc"),
35 homeStateDirectory,
36 });
37 const scopeId = deriveLocalInstallation(context).scopeId;
38 const path = localExecutorStatePath(context);
39 yield* fs.makeDirectory(dirname(path), { recursive: true });
40 yield* fs.writeFileString(
41 path,
42 `${JSON.stringify({
43 version: 1,
44 secretStores: [
45 {
46 id: "sts_builtin_local",
47 scopeId,
48 name: "Local Store",
49 kind: "local",
50 status: "connected",
51 enabled: true,
52 createdAt: 1,
53 updatedAt: 1,
54 },
55 {
56 id: "sts_builtin_keychain",
57 scopeId,
58 name: "Desktop Keyring",
59 kind: "keychain",
60 status: "connected",
61 enabled: true,
62 createdAt: 2,
63 updatedAt: 2,
64 },
65 ],
66 secretMaterials: [
67 {
68 id: "sec_local",
69 name: "Local Secret",
70 purpose: "auth_material",
71 storeId: "sts_builtin_local",
72 handle: "local:abc",
73 value: "token-from-local",
74 createdAt: 3,
75 updatedAt: 4,
76 },
77 {
78 id: "sec_keychain",
79 name: "Keychain Secret",
80 purpose: "auth_material",
81 storeId: "sts_builtin_keychain",
82 handle: "account-123",
83 value: null,
84 createdAt: 5,
85 updatedAt: 6,
86 },
87 ],
88 executions: [
89 {
90 id: "exe_1",
91 workspaceId: scopeId,
92 createdByAccountId: scopeId,
93 status: "completed",
94 code: "1 + 1",
95 resultJson: "2",
96 errorText: null,
97 logsJson: null,
98 startedAt: 10,
99 completedAt: 11,
100 createdAt: 10,
101 updatedAt: 11,
102 },
103 ],
104 executionInteractions: [],
105 executionSteps: [],
106 }, null, 2)}\n`,
107 );
108
109 const beforeMigration = yield* Effect.flip(loadLocalExecutorStateSnapshot(context));
110 expect(beforeMigration.message).toContain("Expected 2, actual 1");
111
112 const migratedPaths = yield* migrateLegacyLocalExecutorState(context);
113 expect(migratedPaths).toContain(path);
114 expect(yield* fs.exists(`${path}.legacy-backup`)).toBe(true);
115
116 const loaded = yield* loadLocalExecutorStateSnapshot(context);
117 const rewritten = yield* fs.readFileString(path, "utf8");
118
119 expect(rewritten).not.toContain("\"handle\"");
120 expect(rewritten).not.toContain("\"workspaceId\"");
121 expect(rewritten).not.toContain("\"createdByAccountId\"");
122 expect(loaded.secretMaterials).toEqual([
123 {
124 id: "sec_local",
125 name: "Local Secret",
126 purpose: "auth_material",
127 storeId: "sts_builtin_local",
128 createdAt: 3,
129 updatedAt: 4,
130 },
131 {
132 id: "sec_keychain",
133 name: "Keychain Secret",
134 purpose: "auth_material",
135 storeId: "sts_builtin_keychain",
136 createdAt: 5,
137 updatedAt: 6,
138 },
139 ]);
140 expect(loaded.secretMaterialStoredData).toEqual([
141 {
142 secretId: "sec_local",
143 data: {
144 value: "token-from-local",
145 },
146 },
147 {
148 secretId: "sec_keychain",
149 data: {
150 account: "account-123",
151 },
152 },
153 ]);
154 expect(loaded.executions[0]).toMatchObject({
155 scopeId,
156 createdByScopeId: scopeId,
157 });
158 }).pipe(Effect.provide(NodeFileSystem.layer)),
159 );
160
161 it.effect("runs executor state migrations before startup loads the state store", () =>
162 Effect.gen(function* () {
163 const fs = yield* FileSystem.FileSystem;
164 const workspaceRoot = yield* makeWorkspaceRoot();
165 const homeStateDirectory = join(workspaceRoot, ".home");
166 const homeConfigPath = join(workspaceRoot, ".executor-home.jsonc");
167 const context = yield* resolveLocalWorkspaceContext({
168 workspaceRoot,
169 homeConfigPath,
170 homeStateDirectory,
171 });
172 const scopeId = deriveLocalInstallation(context).scopeId;
173 const path = localExecutorStatePath(context);
174 yield* fs.makeDirectory(dirname(path), { recursive: true });
175 yield* fs.writeFileString(
176 path,
177 `${JSON.stringify({
178 version: 1,
179 secretStores: [],
180 secretMaterials: [
181 {
182 id: "sec_local",
183 name: "Startup Secret",
184 purpose: "auth_material",
185 providerId: "local",
186 handle: "local:start",
187 value: "startup-token",
188 createdAt: 1,
189 updatedAt: 1,
190 },
191 ],
192 executions: [],
193 executionInteractions: [],
194 executionSteps: [],
195 }, null, 2)}\n`,
196 );
197
198 const beforeStartup = yield* Effect.flip(loadLocalExecutorStateSnapshot(context));
199 expect(beforeStartup.message).toContain("Expected 2, actual 1");
200
201 yield* createLocalExecutorRepositoriesEffect({
202 workspaceRoot,
203 homeConfigPath,
204 homeStateDirectory,
205 });
206
207 const loaded = yield* loadLocalExecutorStateSnapshot(context);
208 expect(yield* fs.exists(`${path}.legacy-backup`)).toBe(true);
209 expect(loaded.secretStores).toEqual([
210 {
211 id: "sts_builtin_local",
212 scopeId,
213 name: "Local Store",
214 kind: "local",
215 status: "connected",
216 enabled: true,
217 createdAt: 1,
218 updatedAt: 1,
219 },
220 ]);
221 expect(loaded.secretMaterialStoredData).toEqual([
222 {
223 secretId: "sec_local",
224 data: {
225 value: "startup-token",
226 },
227 },
228 ]);
229 }).pipe(Effect.provide(NodeFileSystem.layer)),
230 );
231});
packages/platform/sdk-file/src/executor-state-migrations/index.ts+32−0
@@ -0,0 +1,32 @@
1import { FileSystem } from "@effect/platform";
2import * as Effect from "effect/Effect";
3
4import type { ResolvedLocalWorkspaceContext } from "../config";
5import type { LocalFileSystemError } from "../errors";
6import { migrateLegacyLocalExecutorState } from "./legacy-local-executor-state";
7
8type StartupExecutorStateMigration = (
9 context: ResolvedLocalWorkspaceContext,
10) => Effect.Effect<ReadonlyArray<string>, LocalFileSystemError, FileSystem.FileSystem>;
11
12// Keep legacy executor-state upgrades isolated from the live store path.
13const startupExecutorStateMigrations: readonly StartupExecutorStateMigration[] = [
14 migrateLegacyLocalExecutorState,
15];
16
17export const runStartupExecutorStateMigrations = (
18 context: ResolvedLocalWorkspaceContext,
19): Effect.Effect<ReadonlyArray<string>, LocalFileSystemError, FileSystem.FileSystem> =>
20 Effect.gen(function* () {
21 const migratedPaths = new Set<string>();
22
23 for (const migrate of startupExecutorStateMigrations) {
24 for (const path of yield* migrate(context)) {
25 migratedPaths.add(path);
26 }
27 }
28
29 return [...migratedPaths];
30 });
31
32export { migrateLegacyLocalExecutorState } from "./legacy-local-executor-state";
packages/platform/sdk-file/src/executor-state-migrations/legacy-local-executor-state.ts+356−0
@@ -0,0 +1,356 @@
1import { FileSystem } from "@effect/platform";
2import * as Effect from "effect/Effect";
3
4import type { ResolvedLocalWorkspaceContext } from "../config";
5import {
6 decodeLocalExecutorStateSnapshot,
7 encodeLocalExecutorStateSnapshot,
8 localExecutorStatePath,
9 LOCAL_EXECUTOR_STATE_VERSION,
10 type LocalExecutorStateSnapshot,
11} from "../executor-state-codec";
12import {
13 LocalFileSystemError,
14 unknownLocalErrorDetails,
15} from "../errors";
16import { deriveLocalInstallation } from "../installation";
17
18const LEGACY_EXECUTOR_STATE_BACKUP_SUFFIX = ".legacy-backup";
19const LEGACY_LOCAL_SECRET_STORE_ID = "sts_builtin_local";
20const LEGACY_KEYCHAIN_SECRET_STORE_ID = "sts_builtin_keychain";
21
22const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
23 new LocalFileSystemError({
24 message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`,
25 action,
26 path,
27 details: unknownLocalErrorDetails(cause),
28 });
29
30const asRecord = (value: unknown): Record<string, unknown> | null =>
31 value !== null && typeof value === "object" && !Array.isArray(value)
32 ? value as Record<string, unknown>
33 : null;
34
35const asString = (value: unknown): string | null =>
36 typeof value === "string" && value.length > 0 ? value : null;
37
38const asNullableString = (value: unknown): string | null =>
39 value === null
40 ? null
41 : typeof value === "string"
42 ? value
43 : null;
44
45const asBoolean = (value: unknown): boolean | null =>
46 typeof value === "boolean" ? value : null;
47
48const asFiniteNumber = (value: unknown): number | null =>
49 typeof value === "number" && Number.isFinite(value) ? value : null;
50
51const legacySecretStoreIdForProviderId = (providerId: string): string =>
52 providerId === "local"
53 ? LEGACY_LOCAL_SECRET_STORE_ID
54 : providerId === "keychain"
55 ? LEGACY_KEYCHAIN_SECRET_STORE_ID
56 : `sts_legacy_${providerId}`;
57
58const legacySecretStoreNameForProviderId = (providerId: string): string => {
59 switch (providerId) {
60 case "local":
61 return "Local Store";
62 case "keychain":
63 return process.platform === "darwin"
64 ? "macOS Keychain"
65 : process.platform === "win32"
66 ? "Windows Credential Manager"
67 : "Desktop Keyring";
68 default:
69 return `Legacy ${providerId}`;
70 }
71};
72
73const migrateLegacyExecutorStateValue = (
74 value: unknown,
75): {
76 value: unknown;
77 migrated: boolean;
78} => {
79 if (Array.isArray(value)) {
80 let migrated = false;
81 const next = value.map((item) => {
82 const result = migrateLegacyExecutorStateValue(item);
83 migrated ||= result.migrated;
84 return result.value;
85 });
86 return { value: next, migrated };
87 }
88
89 const record = asRecord(value);
90 if (record === null) {
91 return { value, migrated: false };
92 }
93
94 let migrated = false;
95 const next: Record<string, unknown> = {};
96
97 for (const [key, entry] of Object.entries(record)) {
98 let nextKey = key;
99 if (key === "workspaceId") {
100 nextKey = "scopeId";
101 migrated = true;
102 } else if (key === "actorAccountId") {
103 nextKey = "actorScopeId";
104 migrated = true;
105 } else if (key === "createdByAccountId") {
106 nextKey = "createdByScopeId";
107 migrated = true;
108 } else if (key === "workspaceOauthClients") {
109 nextKey = "scopeOauthClients";
110 migrated = true;
111 }
112
113 const migratedEntry = migrateLegacyExecutorStateValue(entry);
114 migrated ||= migratedEntry.migrated;
115 next[nextKey] = migratedEntry.value;
116 }
117
118 return { value: next, migrated };
119};
120
121const normalizeLegacySecretStore = (input: {
122 scopeId: string;
123 record: Record<string, unknown>;
124}) => {
125 const id = asString(input.record.id);
126 const kind = asString(input.record.kind);
127 if (!id || !kind) {
128 return null;
129 }
130
131 const createdAt = asFiniteNumber(input.record.createdAt) ?? 0;
132 const updatedAt = asFiniteNumber(input.record.updatedAt) ?? createdAt;
133
134 return {
135 id,
136 scopeId: asString(input.record.scopeId) ?? input.scopeId,
137 name: asString(input.record.name) ?? legacySecretStoreNameForProviderId(kind),
138 kind,
139 status:
140 input.record.status === "connected" || input.record.status === "error"
141 ? input.record.status
142 : kind === "local" || kind === "keychain"
143 ? "connected"
144 : "error",
145 enabled: asBoolean(input.record.enabled) ?? true,
146 createdAt,
147 updatedAt,
148 };
149};
150
151const legacySecretStoredData = (input: {
152 material: Record<string, unknown>;
153 storeKind: string | null;
154}) => {
155 const handle = asString(input.material.handle);
156 const value = asNullableString(input.material.value);
157
158 switch (input.storeKind) {
159 case "local":
160 return value !== null ? { value } : null;
161 case "keychain":
162 return handle ? { account: handle } : null;
163 default:
164 return handle !== null || value !== null
165 ? {
166 ...(handle !== null ? { handle } : {}),
167 ...(value !== null ? { value } : {}),
168 }
169 : null;
170 }
171};
172
173const canDecodeCurrentExecutorStateSnapshot = (value: unknown): boolean => {
174 try {
175 decodeLocalExecutorStateSnapshot(value);
176 return true;
177 } catch {
178 return false;
179 }
180};
181
182const decodeLegacyExecutorStateSnapshot = (input: {
183 scopeId: string;
184 value: unknown;
185}): LocalExecutorStateSnapshot => {
186 const root = asRecord(input.value);
187 if (root === null) {
188 return decodeLocalExecutorStateSnapshot(input.value);
189 }
190
191 const secretStores = new Map<string, Record<string, unknown>>();
192 for (const entry of Array.isArray(root.secretStores) ? root.secretStores : []) {
193 const record = asRecord(entry);
194 const normalized = record
195 ? normalizeLegacySecretStore({
196 scopeId: input.scopeId,
197 record,
198 })
199 : null;
200 if (normalized) {
201 secretStores.set(normalized.id, normalized);
202 }
203 }
204
205 const secretMaterialStoredData = new Map<string, { secretId: string; data: unknown }>();
206 for (const entry of Array.isArray(root.secretMaterialStoredData)
207 ? root.secretMaterialStoredData
208 : []) {
209 const record = asRecord(entry);
210 const secretId = asString(record?.secretId);
211 if (!secretId) {
212 continue;
213 }
214 secretMaterialStoredData.set(secretId, {
215 secretId,
216 data: record?.data,
217 });
218 }
219
220 const secretMaterials = (Array.isArray(root.secretMaterials)
221 ? root.secretMaterials
222 : []
223 ).flatMap((entry) => {
224 const record = asRecord(entry);
225 const id = asString(record?.id);
226 const purpose = asString(record?.purpose);
227 if (!record || !id || !purpose) {
228 return [];
229 }
230
231 const providerId = asString(record.providerId);
232 const storeId = asString(record.storeId)
233 ?? (providerId ? legacySecretStoreIdForProviderId(providerId) : null);
234 if (!storeId) {
235 return [];
236 }
237
238 const createdAt = asFiniteNumber(record.createdAt) ?? 0;
239 const updatedAt = asFiniteNumber(record.updatedAt) ?? createdAt;
240
241 if (providerId && !secretStores.has(storeId)) {
242 secretStores.set(storeId, {
243 id: storeId,
244 scopeId: input.scopeId,
245 name: legacySecretStoreNameForProviderId(providerId),
246 kind: providerId,
247 status:
248 providerId === "local" || providerId === "keychain"
249 ? "connected"
250 : "error",
251 enabled: true,
252 createdAt,
253 updatedAt,
254 });
255 }
256
257 const storeKind = asString(secretStores.get(storeId)?.kind) ?? providerId;
258 if (!secretMaterialStoredData.has(id)) {
259 const data = legacySecretStoredData({
260 material: record,
261 storeKind,
262 });
263 if (data !== null) {
264 secretMaterialStoredData.set(id, {
265 secretId: id,
266 data,
267 });
268 }
269 }
270
271 return [{
272 id,
273 name: asNullableString(record.name),
274 purpose,
275 storeId,
276 createdAt,
277 updatedAt,
278 }];
279 });
280
281 return decodeLocalExecutorStateSnapshot({
282 version: LOCAL_EXECUTOR_STATE_VERSION,
283 secretStores: [...secretStores.values()],
284 secretMaterials,
285 secretMaterialStoredData: [...secretMaterialStoredData.values()],
286 executions: Array.isArray(root.executions) ? root.executions : [],
287 executionInteractions: Array.isArray(root.executionInteractions)
288 ? root.executionInteractions
289 : [],
290 executionSteps: Array.isArray(root.executionSteps) ? root.executionSteps : [],
291 });
292};
293
294const migrateLegacyExecutorStateFile = (
295 context: ResolvedLocalWorkspaceContext,
296): Effect.Effect<boolean, LocalFileSystemError, FileSystem.FileSystem> =>
297 Effect.gen(function* () {
298 const fs = yield* FileSystem.FileSystem;
299 const path = localExecutorStatePath(context);
300 const exists = yield* fs.exists(path).pipe(
301 Effect.mapError(mapFileSystemError(path, "check executor state path")),
302 );
303 if (!exists) {
304 return false;
305 }
306
307 const content = yield* fs.readFileString(path, "utf8").pipe(
308 Effect.mapError(mapFileSystemError(path, "read executor state")),
309 );
310 const parsed = yield* Effect.try({
311 try: () => JSON.parse(content) as unknown,
312 catch: mapFileSystemError(path, "parse executor state"),
313 });
314
315 if (canDecodeCurrentExecutorStateSnapshot(parsed)) {
316 return false;
317 }
318
319 const migrated = migrateLegacyExecutorStateValue(parsed);
320 const migratedState = yield* Effect.try({
321 try: () =>
322 decodeLegacyExecutorStateSnapshot({
323 scopeId: deriveLocalInstallation(context).scopeId,
324 value: migrated.value,
325 }),
326 catch: mapFileSystemError(path, "decode executor state"),
327 });
328
329 const backupPath = `${path}${LEGACY_EXECUTOR_STATE_BACKUP_SUFFIX}`;
330 const backupExists = yield* fs.exists(backupPath).pipe(
331 Effect.mapError(mapFileSystemError(backupPath, "check executor state backup path")),
332 );
333 if (!backupExists) {
334 yield* fs.writeFileString(backupPath, content, { mode: 0o600 }).pipe(
335 Effect.mapError(mapFileSystemError(backupPath, "write executor state backup")),
336 );
337 }
338
339 yield* fs.writeFileString(
340 path,
341 encodeLocalExecutorStateSnapshot(migratedState),
342 { mode: 0o600 },
343 ).pipe(
344 Effect.mapError(mapFileSystemError(path, "write migrated executor state")),
345 );
346
347 return true;
348 });
349
350export const migrateLegacyLocalExecutorState = (
351 context: ResolvedLocalWorkspaceContext,
352): Effect.Effect<ReadonlyArray<string>, LocalFileSystemError, FileSystem.FileSystem> =>
353 Effect.gen(function* () {
354 const migrated = yield* migrateLegacyExecutorStateFile(context);
355 return migrated ? [localExecutorStatePath(context)] : [];
356 });
packages/platform/sdk-file/src/executor-state-store.ts+15−59
@@ -1,5 +1,5 @@
1import { randomUUID } from "node:crypto";1import { randomUUID } from "node:crypto";
2import { dirname, join } from "node:path";2import { dirname } from "node:path";
3import { FileSystem } from "@effect/platform";3import { FileSystem } from "@effect/platform";
4import { NodeFileSystem } from "@effect/platform-node";4import { NodeFileSystem } from "@effect/platform-node";
5 5
@@ -7,66 +7,32 @@ import { · 1 unmodified line
7 type Execution,7 type Execution,
8 type ExecutionInteraction,8 type ExecutionInteraction,
9 type ExecutionStep,9 type ExecutionStep,
10 ExecutionInteractionSchema,
11 ExecutionSchema,
12 ExecutionStepSchema,
13 SecretMaterialSchema,
14 type SecretMaterial,10 type SecretMaterial,
15 SecretStoreSchema,
16 type SecretStore,11 type SecretStore,
17} from "@executor/platform-sdk/schema";12} from "@executor/platform-sdk/schema";
18import * as Effect from "effect/Effect";13import * as Effect from "effect/Effect";
19import * as Option from "effect/Option";14import * as Option from "effect/Option";
20import * as Schema from "effect/Schema";
21 15
22import type { ResolvedLocalWorkspaceContext } from "./config";16import type { ResolvedLocalWorkspaceContext } from "./config";
23import { deriveLocalInstallation } from "./installation";17import {
18 decodeLocalExecutorStateSnapshot,
19 defaultLocalExecutorStateSnapshot,
20 encodeLocalExecutorStateSnapshot,
21 localExecutorStatePath,
22 LOCAL_EXECUTOR_STATE_VERSION,
23 type LocalExecutorStateSnapshot,
24 type SecretMaterialStoredDataRecord,
25} from "./executor-state-codec";
24import {26import {
25 LocalFileSystemError,27 LocalFileSystemError,
26 unknownLocalErrorDetails,28 unknownLocalErrorDetails,
27} from "./errors";29} from "./errors";
28 30
29const LOCAL_EXECUTOR_STATE_VERSION = 2 as const;
30const LOCAL_EXECUTOR_STATE_BASENAME = "executor-state.json";
31
32const SecretMaterialStoredDataRecordSchema = Schema.Struct({
33 secretId: Schema.String,
34 data: Schema.Unknown,
35});
36
37type SecretMaterialStoredDataRecord = typeof SecretMaterialStoredDataRecordSchema.Type;
38
39const LocalExecutorStateSnapshotSchema = Schema.Struct({
40 version: Schema.Literal(LOCAL_EXECUTOR_STATE_VERSION),
41 secretStores: Schema.Array(SecretStoreSchema),
42 secretMaterials: Schema.Array(SecretMaterialSchema),
43 secretMaterialStoredData: Schema.Array(SecretMaterialStoredDataRecordSchema),
44 executions: Schema.Array(ExecutionSchema),
45 executionInteractions: Schema.Array(ExecutionInteractionSchema),
46 executionSteps: Schema.Array(ExecutionStepSchema),
47});
48
49export type LocalExecutorStateSnapshot = typeof LocalExecutorStateSnapshotSchema.Type;
50
51export type LocalExecutorStatePersistence = {31export type LocalExecutorStatePersistence = {
52 executorState: LocalExecutorStateStore;32 executorState: LocalExecutorStateStore;
53 close: () => Promise<void>;33 close: () => Promise<void>;
54};34};
55 35
56const decodeLocalExecutorStateSnapshot = Schema.decodeUnknown(
57 LocalExecutorStateSnapshotSchema,
58);
59
60const defaultLocalExecutorStateSnapshot = (): LocalExecutorStateSnapshot => ({
61 version: LOCAL_EXECUTOR_STATE_VERSION,
62 secretStores: [],
63 secretMaterials: [],
64 secretMaterialStoredData: [],
65 executions: [],
66 executionInteractions: [],
67 executionSteps: [],
68});
69
70const cloneValue = <T>(value: T): T =>36const cloneValue = <T>(value: T): T =>
71 JSON.parse(JSON.stringify(value)) as T;37 JSON.parse(JSON.stringify(value)) as T;
72const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>38const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
@@ -84,16 +50,6 @@ const sortByUpdatedAtAndIdDesc = <T extends { updatedAt: number; id: string }>( · 11 unmodified lines
84 right.updatedAt - left.updatedAt || right.id.localeCompare(left.id),50 right.updatedAt - left.updatedAt || right.id.localeCompare(left.id),
85 );51 );
86 52
87const localExecutorStatePath = (
88 context: ResolvedLocalWorkspaceContext,
89): string =>
90 join(
91 context.homeStateDirectory,
92 "workspaces",
93 deriveLocalInstallation(context).scopeId,
94 LOCAL_EXECUTOR_STATE_BASENAME,
95 );
96
97const bindFileSystem = <A, E>(53const bindFileSystem = <A, E>(
98 fileSystem: FileSystem.FileSystem,54 fileSystem: FileSystem.FileSystem,
99 effect: Effect.Effect<A, E, FileSystem.FileSystem>,55 effect: Effect.Effect<A, E, FileSystem.FileSystem>,
@@ -125,10 +81,10 @@ const readStateFromDisk = ( · 25 unmodified lines
125 try: () => JSON.parse(content) as unknown,81 try: () => JSON.parse(content) as unknown,
126 catch: mapFileSystemError(path, "parse executor state"),82 catch: mapFileSystemError(path, "parse executor state"),
127 });83 });
128 const decoded = yield* decodeLocalExecutorStateSnapshot(parsed).pipe(84 return yield* Effect.try({
129 Effect.mapError(mapFileSystemError(path, "decode executor state")),85 try: () => decodeLocalExecutorStateSnapshot(parsed),
130 );86 catch: mapFileSystemError(path, "decode executor state"),
131 return decoded;87 });
132 });88 });
133 89
134const writeStateToDisk = (90const writeStateToDisk = (
@@ -143,7 +99,7 @@ const writeStateToDisk = ( · 8 unmodified lines
143 yield* fs.makeDirectory(dirname(path), { recursive: true }).pipe(99 yield* fs.makeDirectory(dirname(path), { recursive: true }).pipe(
144 Effect.mapError(mapFileSystemError(dirname(path), "create executor state directory")),100 Effect.mapError(mapFileSystemError(dirname(path), "create executor state directory")),
145 );101 );
146 yield* fs.writeFileString(tempPath, `${JSON.stringify(state, null, 2)}\n`, {102 yield* fs.writeFileString(tempPath, encodeLocalExecutorStateSnapshot(state), {
147 mode: 0o600,103 mode: 0o600,
148 }).pipe(104 }).pipe(
149 Effect.mapError(mapFileSystemError(tempPath, "write executor state")),105 Effect.mapError(mapFileSystemError(tempPath, "write executor state")),
packages/platform/sdk-file/src/index.ts+31−11
@@ -19,7 +19,7 @@ import type { · 18 unmodified lines
19 ExecutorSdkPluginExtensions,19 ExecutorSdkPluginExtensions,
20} from "@executor/platform-sdk/plugins";20} from "@executor/platform-sdk/plugins";
21import type {21import type {
22 LocalExecutorConfig,22 ExecutorScopeConfig,
23 LocalInstallation,23 LocalInstallation,
24} from "@executor/platform-sdk/schema";24} from "@executor/platform-sdk/schema";
25import {25import {
@@ -30,14 +30,18 @@ import { · 4 unmodified lines
30} from "@executor/platform-sdk/runtime";30} from "@executor/platform-sdk/runtime";
31import * as Effect from "effect/Effect";31import * as Effect from "effect/Effect";
32import {32import {
33 type LoadedLocalExecutorConfig,33 type LoadedExecutorScopeConfig,
34 type ResolvedLocalWorkspaceContext,34 type ResolvedLocalWorkspaceContext,
35 loadLocalExecutorConfig,35 loadExecutorScopeConfig,
36 resolveConfigRelativePath,36 resolveConfigRelativePath,
37 resolveLocalWorkspaceContext,37 resolveLocalWorkspaceContext,
38 writeProjectLocalExecutorConfig,38 writeProjectExecutorScopeConfig,
39} from "./config";39} from "./config";
40import {
41 runStartupConfigMigrations,
42} from "./config-migrations";
40import { createLocalExecutorStatePersistence } from "./executor-state-store";43import { createLocalExecutorStatePersistence } from "./executor-state-store";
44import { runStartupExecutorStateMigrations } from "./executor-state-migrations";
41import {45import {
42 createDefaultSecretMaterialDeleter,46 createDefaultSecretMaterialDeleter,
43 createLocalInstanceConfigResolver,47 createLocalInstanceConfigResolver,
@@ -114,18 +118,18 @@ const createBoundWorkspaceConfigStore = ( · 70 unmodified lines
114 context: ResolvedLocalWorkspaceContext,118 context: ResolvedLocalWorkspaceContext,
115 fileSystem: FileSystem.FileSystem,119 fileSystem: FileSystem.FileSystem,
116): {120): {
117 load: () => Effect.Effect<LoadedLocalExecutorConfig, Error, never>;121 load: () => Effect.Effect<LoadedExecutorScopeConfig, Error, never>;
118 writeProject: (config: LocalExecutorConfig) => Effect.Effect<void, Error, never>;122 writeProject: (config: ExecutorScopeConfig) => Effect.Effect<void, Error, never>;
119 resolveRelativePath: typeof resolveConfigRelativePath;123 resolveRelativePath: typeof resolveConfigRelativePath;
120} => ({124} => ({
121 load: () =>125 load: () =>
122 bindFileSystem(fileSystem, loadLocalExecutorConfig(context)).pipe(126 bindFileSystem(fileSystem, loadExecutorScopeConfig(context)).pipe(
123 Effect.mapError(toError),127 Effect.mapError(toError),
124 ),128 ),
125 writeProject: (config) =>129 writeProject: (config) =>
126 bindFileSystem(130 bindFileSystem(
127 fileSystem,131 fileSystem,
128 writeProjectLocalExecutorConfig({ context, config }),132 writeProjectExecutorScopeConfig({ context, config }),
129 ).pipe(Effect.mapError(toError)),133 ).pipe(Effect.mapError(toError)),
130 resolveRelativePath: resolveConfigRelativePath,134 resolveRelativePath: resolveConfigRelativePath,
131});135});
@@ -237,6 +241,14 @@ export const createLocalExecutorRepositoriesEffect = ( · 105 unmodified lines
237 homeStateDirectory: options.homeStateDirectory,241 homeStateDirectory: options.homeStateDirectory,
238 }),242 }),
239 ).pipe(Effect.mapError(toError));243 ).pipe(Effect.mapError(toError));
244 yield* bindFileSystem(
245 fileSystem,
246 runStartupConfigMigrations(workspaceContext),
247 ).pipe(Effect.mapError(toError));
248 yield* bindFileSystem(
249 fileSystem,
250 runStartupExecutorStateMigrations(workspaceContext),
251 ).pipe(Effect.mapError(toError));
240 252
241 const executorStateStorage = createLocalExecutorStatePersistence(253 const executorStateStorage = createLocalExecutorStatePersistence(
242 workspaceContext,254 workspaceContext,
@@ -384,11 +396,19 @@ export { · 141 unmodified lines
384 writeLocalExecutorStateSnapshot,396 writeLocalExecutorStateSnapshot,
385} from "./executor-state-store";397} from "./executor-state-store";
386export {398export {
387 loadLocalExecutorConfig,399 migrateLegacyLocalExecutorState,
400 runStartupExecutorStateMigrations,
401} from "./executor-state-migrations";
402export {
403 loadExecutorScopeConfig,
388 resolveConfigRelativePath,404 resolveConfigRelativePath,
389 resolveLocalWorkspaceContext,405 resolveLocalWorkspaceContext,
390 writeProjectLocalExecutorConfig,406 writeProjectExecutorScopeConfig,
391} from "./config";407} from "./config";
408export {
409 migrateLegacyExecutorScopeConfigs,
410 runStartupConfigMigrations,
411} from "./config-migrations";
392export {412export {
393 loadLocalWorkspaceState,413 loadLocalWorkspaceState,
394 writeLocalWorkspaceState,414 writeLocalWorkspaceState,
@@ -406,6 +426,6 @@ export { · 11 unmodified lines
406 syncWorkspaceSourceTypeDeclarationsNode,426 syncWorkspaceSourceTypeDeclarationsNode,
407} from "./source-type-declarations";427} from "./source-type-declarations";
408export type {428export type {
409 LoadedLocalExecutorConfig,429 LoadedExecutorScopeConfig,
410 ResolvedLocalWorkspaceContext,430 ResolvedLocalWorkspaceContext,
411} from "./config";431} from "./config";
packages/platform/sdk/src/plugins.ts+15−15
@@ -8,8 +8,8 @@ import type { · 7 unmodified lines
8import type { ExecutorEffect } from "./executor-effect";8import type { ExecutorEffect } from "./executor-effect";
9import type { ExecutorScopeContext } from "./scope";9import type { ExecutorScopeContext } from "./scope";
10import {10import {
11 type LocalConfigSource,11 type ExecutorScopeConfigSource,
12 type LocalExecutorConfig,12 type ExecutorScopeConfig,
13 type Source as ExecutorSource,13 type Source as ExecutorSource,
14 type SecretMaterial,14 type SecretMaterial,
15 type SecretMaterialPurpose,15 type SecretMaterialPurpose,
@@ -300,15 +300,15 @@ export type ExecutorSourcePluginDefinition< · 284 unmodified lines
300 stored: TStored | null;300 stored: TStored | null;
301 }) => Effect.Effect<void, Error, any>;301 }) => Effect.Effect<void, Error, any>;
302 };302 };
303 localConfig?: {303 scopeConfig?: {
304 toConfigSource: (input: {304 toConfigSource: (input: {
305 source: ExecutorSource;305 source: ExecutorSource;
306 stored: TStored;306 stored: TStored;
307 }) => LocalConfigSource;307 }) => ExecutorScopeConfigSource;
308 recoverStored: (input: {308 recoverStored: (input: {
309 source: ExecutorSource;309 source: ExecutorSource;
310 config: LocalConfigSource;310 config: ExecutorScopeConfigSource;
311 loadedConfig: LocalExecutorConfig | null;311 loadedConfig: ExecutorScopeConfig | null;
312 }) => Effect.Effect<TStored, Error, any> | TStored;312 }) => Effect.Effect<TStored, Error, any> | TStored;
313 };313 };
314 catalog: {314 catalog: {
@@ -588,7 +588,7 @@ const fromMaybeEffect = <A>( · 273 unmodified lines
588 ? value588 ? value
589 : Effect.succeed(value);589 : Effect.succeed(value);
590 590
591const persistSourceLocalConfig = <591const persistSourceScopeConfig = <
592 TAddInput,592 TAddInput,
593 TConnectInput,593 TConnectInput,
594 TSourceConfig,594 TSourceConfig,
@@ -610,15 +610,15 @@ const persistSourceLocalConfig = < · 15 unmodified lines
610 stored: TStored;610 stored: TStored;
611 },611 },
612): Effect.Effect<void, Error, ScopeConfigStore> =>612): Effect.Effect<void, Error, ScopeConfigStore> =>
613 definition.localConfig613 definition.scopeConfig
614 ? Effect.gen(function* () {614 ? Effect.gen(function* () {
615 const localConfig = definition.localConfig!;615 const scopeConfig = definition.scopeConfig!;
616 const scopeConfigStore = yield* ScopeConfigStore;616 const scopeConfigStore = yield* ScopeConfigStore;
617 const loadedConfig = yield* scopeConfigStore.load();617 const loadedConfig = yield* scopeConfigStore.load();
618 const projectConfig = cloneJson(loadedConfig.projectConfig ?? {});618 const projectConfig = cloneJson(loadedConfig.projectConfig ?? {});
619 const sources = {619 const sources = {
620 ...projectConfig.sources,620 ...projectConfig.sources,
621 [input.source.id]: localConfig.toConfigSource({621 [input.source.id]: scopeConfig.toConfigSource({
622 source: input.source,622 source: input.source,
623 stored: input.stored,623 stored: input.stored,
624 }),624 }),
@@ -653,12 +653,12 @@ const loadStoredOrRecover = < · 28 unmodified lines
653 source: ExecutorSource,653 source: ExecutorSource,
654): Effect.Effect<TStored | null, Error, ScopeConfigStore> =>654): Effect.Effect<TStored | null, Error, ScopeConfigStore> =>
655 Effect.gen(function* () {655 Effect.gen(function* () {
656 const localConfig = definition.localConfig;656 const scopeConfig = definition.scopeConfig;
657 const stored = yield* definition.storage.get({657 const stored = yield* definition.storage.get({
658 scopeId: source.scopeId,658 scopeId: source.scopeId,
659 sourceId: source.id,659 sourceId: source.id,
660 });660 });
661 if (stored !== null || localConfig === undefined) {661 if (stored !== null || scopeConfig === undefined) {
662 return stored;662 return stored;
663 }663 }
664 664
@@ -677,7 +677,7 @@ const loadStoredOrRecover = < · 12 unmodified lines
677 }677 }
678 678
679 const recovered = yield* fromMaybeEffect(679 const recovered = yield* fromMaybeEffect(
680 localConfig.recoverStored({680 scopeConfig.recoverStored({
681 source,681 source,
682 config,682 config,
683 loadedConfig: loadedConfig.config,683 loadedConfig: loadedConfig.config,
@@ -748,7 +748,7 @@ const createExecutorSourcePluginApi = < · 64 unmodified lines
748 sourceId: source.id,748 sourceId: source.id,
749 value: created.stored,749 value: created.stored,
750 });750 });
751 yield* persistSourceLocalConfig(definition, {751 yield* persistSourceScopeConfig(definition, {
752 source,752 source,
753 stored: created.stored,753 stored: created.stored,
754 });754 });
@@ -772,7 +772,7 @@ const createExecutorSourcePluginApi = < · 17 unmodified lines
772 sourceId: saved.id,772 sourceId: saved.id,
773 value: updated.stored,773 value: updated.stored,
774 });774 });
775 yield* persistSourceLocalConfig(definition, {775 yield* persistSourceScopeConfig(definition, {
776 source: saved,776 source: saved,
777 stored: updated.stored,777 stored: updated.stored,
778 });778 });
packages/platform/sdk/src/policies/operations.ts+3−3
@@ -8,7 +8,7 @@ import type { · 7 unmodified lines
8} from "./contracts";8} from "./contracts";
9import {9import {
10 PolicyIdSchema,10 PolicyIdSchema,
11 type LocalExecutorConfig,11 type ExecutorScopeConfig,
12 type LocalScopePolicy,12 type LocalScopePolicy,
13 type PolicyId,13 type PolicyId,
14 type ScopeId,14 type ScopeId,
@@ -64,7 +64,7 @@ const toLocalScopePolicy = (input: { · 49 unmodified lines
64 scopeId: ScopeId;64 scopeId: ScopeId;
65 scopeStableKey: string;65 scopeStableKey: string;
66 key: string;66 key: string;
67 policyConfig: NonNullable<LocalExecutorConfig["policies"]>[string];67 policyConfig: NonNullable<ExecutorScopeConfig["policies"]>[string];
68 state: LocalScopeState["policies"][string] | undefined;68 state: LocalScopeState["policies"][string] | undefined;
69}): LocalScopePolicy => ({69}): LocalScopePolicy => ({
70 id:70 id:
@@ -119,7 +119,7 @@ const writeLocalPolicyFiles = (input: { · 48 unmodified lines
119 operation: OperationErrors;119 operation: OperationErrors;
120 scopeConfigStore: ScopeConfigStoreShape;120 scopeConfigStore: ScopeConfigStoreShape;
121 scopeStateStore: ScopeStateStoreShape;121 scopeStateStore: ScopeStateStoreShape;
122 projectConfig: LocalExecutorConfig;122 projectConfig: ExecutorScopeConfig;
123 scopeState: LocalScopeState;123 scopeState: LocalScopeState;
124}) =>124}) =>
125 Effect.all(125 Effect.all(
packages/platform/sdk/src/runtime/execution/runtime.ts+6−6
@@ -11,18 +11,18 @@ import { · 10 unmodified lines
11 makeSesExecutor,11 makeSesExecutor,
12} from "@executor/runtime-ses";12} from "@executor/runtime-ses";
13import type {13import type {
14 LocalExecutorConfig,14 ExecutorScopeConfig,
15 LocalExecutorRuntime,15 ExecutorRuntimeConfig,
16} from "#schema";16} from "#schema";
17 17
18const DEFAULT_EXECUTION_RUNTIME: LocalExecutorRuntime = "quickjs";18const DEFAULT_EXECUTION_RUNTIME: ExecutorRuntimeConfig = "quickjs";
19 19
20export const resolveConfiguredExecutionRuntime = (20export const resolveConfiguredExecutionRuntime = (
21 config: LocalExecutorConfig | null | undefined,21 config: ExecutorScopeConfig | null | undefined,
22): LocalExecutorRuntime => config?.runtime ?? DEFAULT_EXECUTION_RUNTIME;22): ExecutorRuntimeConfig => config?.runtime ?? DEFAULT_EXECUTION_RUNTIME;
23 23
24export const createCodeExecutorForRuntime = (24export const createCodeExecutorForRuntime = (
25 runtime: LocalExecutorRuntime,25 runtime: ExecutorRuntimeConfig,
26): CodeExecutor => {26): CodeExecutor => {
27 switch (runtime) {27 switch (runtime) {
28 case "deno":28 case "deno":
packages/platform/sdk/src/runtime/index.ts+5−5
@@ -32,10 +32,10 @@ import { · 31 unmodified lines
32 RuntimeExecutionResolverLive,32 RuntimeExecutionResolverLive,
33} from "./execution/scope/environment";33} from "./execution/scope/environment";
34import type {34import type {
35 LoadedLocalExecutorConfig,35 LoadedExecutorScopeConfig,
36} from "./scope-config";36} from "./scope-config";
37import type {37import type {
38 LocalExecutorConfig,38 ExecutorScopeConfig,
39} from "#schema";39} from "#schema";
40import type {40import type {
41 InstanceConfig,41 InstanceConfig,
@@ -113,7 +113,7 @@ export * from "./source-artifacts"; · 71 unmodified lines
113export * from "./scope-config";113export * from "./scope-config";
114export * from "./scope-state";114export * from "./scope-state";
115export * from "./sources/source-store/config";115export * from "./sources/source-store/config";
116export * from "./sources/source-store/plugin-local-config";116export * from "./sources/source-store/plugin-scope-config";
117export {117export {
118 LocalInstanceConfigService,118 LocalInstanceConfigService,
119 SecretMaterialDeleterService,119 SecretMaterialDeleterService,
@@ -184,9 +184,9 @@ export type BoundInstallationStore = { · 64 unmodified lines
184};184};
185 185
186export type BoundScopeConfigStore = {186export type BoundScopeConfigStore = {
187 load: () => Effect.Effect<LoadedLocalExecutorConfig, Error, never>;187 load: () => Effect.Effect<LoadedExecutorScopeConfig, Error, never>;
188 writeProject: (188 writeProject: (
189 config: LocalExecutorConfig,189 config: ExecutorScopeConfig,
190 ) => Effect.Effect<void, Error, never>;190 ) => Effect.Effect<void, Error, never>;
191 resolveRelativePath: ScopeConfigStoreShape["resolveRelativePath"];191 resolveRelativePath: ScopeConfigStoreShape["resolveRelativePath"];
192};192};
packages/platform/sdk/src/runtime/scope-config.ts+5−5
@@ -1,11 +1,11 @@
1import type {1import type {
2 LocalExecutorConfig,2 ExecutorScopeConfig,
3} from "#schema";3} from "#schema";
4 4
5export type LoadedExecutorConfig = {5export type LoadedExecutorConfig = {
6 config: LocalExecutorConfig | null;6 config: ExecutorScopeConfig | null;
7 homeConfig: LocalExecutorConfig | null;7 homeConfig: ExecutorScopeConfig | null;
8 projectConfig: LocalExecutorConfig | null;8 projectConfig: ExecutorScopeConfig | null;
9};9};
10 10
11export type LoadedLocalExecutorConfig = LoadedExecutorConfig;11export type LoadedExecutorScopeConfig = LoadedExecutorConfig;
packages/platform/sdk/src/runtime/scope/runtime-context.ts+2−2
@@ -10,7 +10,7 @@ import type { · 9 unmodified lines
10 ExecutorScopeContext,10 ExecutorScopeContext,
11} from "../../scope";11} from "../../scope";
12import type {12import type {
13 LoadedLocalExecutorConfig,13 LoadedExecutorScopeConfig,
14} from "../scope-config";14} from "../scope-config";
15import {15import {
16 RuntimeLocalScopeMismatchError,16 RuntimeLocalScopeMismatchError,
@@ -24,7 +24,7 @@ export type RuntimeLocalScopeState = { · 7 unmodified lines
24 actorScopeId: ScopeId;24 actorScopeId: ScopeId;
25 resolutionScopeIds: ReadonlyArray<ScopeId>;25 resolutionScopeIds: ReadonlyArray<ScopeId>;
26 };26 };
27 loadedConfig: LoadedLocalExecutorConfig;27 loadedConfig: LoadedExecutorScopeConfig;
28};28};
29 29
30export class RuntimeLocalScopeService extends Context.Tag(30export class RuntimeLocalScopeService extends Context.Tag(
packages/platform/sdk/src/runtime/scope/scope-sync.ts+4−4
@@ -1,7 +1,7 @@
1import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
2 2
3import {3import {
4 type LoadedLocalExecutorConfig,4 type LoadedExecutorScopeConfig,
5} from "../scope-config";5} from "../scope-config";
6import {6import {
7 ScopeStateStore,7 ScopeStateStore,
@@ -40,7 +40,7 @@ export const derivePolicyConfigKey = ( · 32 unmodified lines
40};40};
41 41
42const pruneLocalWorkspaceState = (input: {42const pruneLocalWorkspaceState = (input: {
43 loadedConfig: LoadedLocalExecutorConfig;43 loadedConfig: LoadedExecutorScopeConfig;
44}): Effect.Effect<LocalScopeState, Error, ScopeStateStore> =>44}): Effect.Effect<LocalScopeState, Error, ScopeStateStore> =>
45 Effect.gen(function* () {45 Effect.gen(function* () {
46 const scopeStateStore = yield* ScopeStateStore;46 const scopeStateStore = yield* ScopeStateStore;
@@ -79,8 +79,8 @@ const pruneLocalWorkspaceState = (input: { · 32 unmodified lines
79 });79 });
80 80
81export const synchronizeLocalScopeState = (input: {81export const synchronizeLocalScopeState = (input: {
82 loadedConfig: LoadedLocalExecutorConfig;82 loadedConfig: LoadedExecutorScopeConfig;
83}): Effect.Effect<LoadedLocalExecutorConfig["config"], Error, ScopeStateStore> =>83}): Effect.Effect<LoadedExecutorScopeConfig["config"], Error, ScopeStateStore> =>
84 Effect.gen(function* () {84 Effect.gen(function* () {
85 yield* pruneLocalWorkspaceState({85 yield* pruneLocalWorkspaceState({
86 loadedConfig: input.loadedConfig,86 loadedConfig: input.loadedConfig,
packages/platform/sdk/src/runtime/scope/storage.ts+4−4
@@ -1,12 +1,12 @@
1import type {1import type {
2 LocalInstallation,2 LocalInstallation,
3 LocalExecutorConfig,3 ExecutorScopeConfig,
4} from "#schema";4} from "#schema";
5import * as Context from "effect/Context";5import * as Context from "effect/Context";
6import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
7 7
8import type {8import type {
9 LoadedLocalExecutorConfig,9 LoadedExecutorScopeConfig,
10} from "../scope-config";10} from "../scope-config";
11import type {11import type {
12 LocalSourceArtifact,12 LocalSourceArtifact,
@@ -31,9 +31,9 @@ export class InstallationStore extends Context.Tag( · 18 unmodified lines
31)<InstallationStore, InstallationStoreShape>() {}31)<InstallationStore, InstallationStoreShape>() {}
32 32
33export type ScopeConfigStoreShape = {33export type ScopeConfigStoreShape = {
34 load: () => import("effect/Effect").Effect<LoadedLocalExecutorConfig, Error, never>;34 load: () => import("effect/Effect").Effect<LoadedExecutorScopeConfig, Error, never>;
35 writeProject: (input: {35 writeProject: (input: {
36 config: LocalExecutorConfig;36 config: ExecutorScopeConfig;
37 }) => import("effect/Effect").Effect<void, Error, never>;37 }) => import("effect/Effect").Effect<void, Error, never>;
38 resolveRelativePath: (input: { path: string; scopeRoot: string }) => string;38 resolveRelativePath: (input: { path: string; scopeRoot: string }) => string;
39};39};
packages/platform/sdk/src/runtime/sources/source-store/config.ts+10−20
@@ -1,5 +1,5 @@
1import type {1import type {
2 LocalConfigSource,2 ExecutorScopeConfigSource,
3 Source,3 Source,
4 SourceId,4 SourceId,
5} from "#schema";5} from "#schema";
@@ -23,7 +23,7 @@ export const trimOrNull = (value: string | null | undefined): string | null => { · 17 unmodified lines
23export const cloneJson = <T>(value: T): T =>23export const cloneJson = <T>(value: T): T =>
24 JSON.parse(JSON.stringify(value)) as T;24 JSON.parse(JSON.stringify(value)) as T;
25 25
26export const deriveLocalSourceId = (26export const deriveScopeConfigSourceId = (
27 source: Pick<Source, "namespace" | "name">,27 source: Pick<Source, "namespace" | "name">,
28 used: ReadonlySet<string>,28 used: ReadonlySet<string>,
29): SourceId => {29): SourceId => {
@@ -38,9 +38,9 @@ export const deriveLocalSourceId = ( · 8 unmodified lines
38 return SourceIdSchema.make(candidate);38 return SourceIdSchema.make(candidate);
39};39};
40 40
41export const configSourceBaseFromLocalSource = (input: {41export const scopeConfigSourceBaseFromSource = (input: {
42 source: Source;42 source: Source;
43}): Omit<LocalConfigSource, "kind" | "config" | "connection" | "binding"> => ({43}): Omit<ExecutorScopeConfigSource, "kind" | "config"> => ({
44 ...(trimOrNull(input.source.name) !== trimOrNull(input.source.id)44 ...(trimOrNull(input.source.name) !== trimOrNull(input.source.id)
45 ? { name: input.source.name }45 ? { name: input.source.name }
46 : {}),46 : {}),
@@ -50,29 +50,19 @@ export const configSourceBaseFromLocalSource = (input: { · 3 unmodified lines
50 ...(input.source.enabled === false ? { enabled: false } : {}),50 ...(input.source.enabled === false ? { enabled: false } : {}),
51});51});
52 52
53export const configSourceFromLocalSource = (input: {53export const scopeConfigSourceFromSource = (input: {
54 source: Source;54 source: Source;
55 existingConfig?: LocalConfigSource | null;55 existingConfig?: ExecutorScopeConfigSource | null;
56}): LocalConfigSource => {56}): ExecutorScopeConfigSource => {
57 return {57 return {
58 ...configSourceBaseFromLocalSource({58 ...scopeConfigSourceBaseFromSource({
59 source: input.source,59 source: input.source,
60 }),60 }),
61 kind: input.source.kind as LocalConfigSource["kind"],61 kind: input.source.kind as ExecutorScopeConfigSource["kind"],
62 ...(input.existingConfig?.config !== undefined62 ...(input.existingConfig?.config !== undefined
63 ? {63 ? {
64 config: cloneJson(input.existingConfig.config),64 config: cloneJson(input.existingConfig.config),
65 }65 }
66 : {}),66 : {}),
67 ...(input.existingConfig?.connection !== undefined67 } as ExecutorScopeConfigSource;
68 ? {
69 connection: cloneJson(input.existingConfig.connection),
70 }
71 : {}),
72 ...(input.existingConfig?.binding !== undefined
73 ? {
74 binding: cloneJson(input.existingConfig.binding),
75 }
76 : {}),
77 } as LocalConfigSource;
78};68};
packages/platform/sdk/src/runtime/sources/source-store/deps.ts+2−2
@@ -4,7 +4,7 @@ import type { · 3 unmodified lines
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5 5
6import type {6import type {
7 LoadedLocalExecutorConfig,7 LoadedExecutorScopeConfig,
8} from "../../scope-config";8} from "../../scope-config";
9import {9import {
10 SourceTypeDeclarationsRefresherService,10 SourceTypeDeclarationsRefresherService,
@@ -53,7 +53,7 @@ export type ResolvedSourceStoreScope = { · 42 unmodified lines
53 scopeConfigStore: ScopeConfigStoreShape;53 scopeConfigStore: ScopeConfigStoreShape;
54 scopeStateStore: ScopeStateStoreShape;54 scopeStateStore: ScopeStateStoreShape;
55 sourceArtifactStore: SourceArtifactStoreShape;55 sourceArtifactStore: SourceArtifactStoreShape;
56 loadedConfig: LoadedLocalExecutorConfig;56 loadedConfig: LoadedExecutorScopeConfig;
57 scopeState: LocalScopeState;57 scopeState: LocalScopeState;
58};58};
59 59
packages/platform/sdk/src/runtime/sources/source-store/lifecycle.ts+4−4
@@ -6,9 +6,9 @@ import * as Effect from "effect/Effect"; · 5 unmodified lines
6 6
7import type { LocalScopeState } from "../../scope-state";7import type { LocalScopeState } from "../../scope-state";
8import {8import {
9 configSourceFromLocalSource,9 scopeConfigSourceFromSource,
10 cloneJson,10 cloneJson,
11 deriveLocalSourceId,11 deriveScopeConfigSourceId,
12} from "./config";12} from "./config";
13import {13import {
14 type RuntimeSourceStoreDeps,14 type RuntimeSourceStoreDeps,
@@ -84,7 +84,7 @@ export const persistSourceWithDeps = ( · 69 unmodified lines
84 localScope.loadedConfig.config?.sources?.[source.id] ||84 localScope.loadedConfig.config?.sources?.[source.id] ||
85 localScope.scopeState.sources[source.id]85 localScope.scopeState.sources[source.id]
86 ? source.id86 ? source.id
87 : deriveLocalSourceId(87 : deriveScopeConfigSourceId(
88 source,88 source,
89 new Set(Object.keys(localScope.loadedConfig.config?.sources ?? {})),89 new Set(Object.keys(localScope.loadedConfig.config?.sources ?? {})),
90 ),90 ),
@@ -94,7 +94,7 @@ export const persistSourceWithDeps = ( · 3 unmodified lines
94 const sources = {94 const sources = {
95 ...projectConfig.sources,95 ...projectConfig.sources,
96 };96 };
97 sources[nextSource.id] = configSourceFromLocalSource({97 sources[nextSource.id] = scopeConfigSourceFromSource({
98 source: nextSource,98 source: nextSource,
99 existingConfig: localScope.loadedConfig.config?.sources?.[nextSource.id] ?? null,99 existingConfig: localScope.loadedConfig.config?.sources?.[nextSource.id] ?? null,
100 });100 });
packages/platform/sdk/src/runtime/sources/source-store/plugin-scope-config.tsrenamed from packages/platform/sdk/src/runtime/sources/source-store/plugin-local-config.ts+7−9
@@ -1,14 +1,14 @@
1import type {1import type {
2 LocalConfigSource,2 ExecutorScopeConfigSource,
3 Source,3 Source,
4} from "#schema";4} from "#schema";
5import * as Schema from "effect/Schema";5import * as Schema from "effect/Schema";
6import {6import {
7 cloneJson,7 cloneJson,
8 configSourceBaseFromLocalSource,8 scopeConfigSourceBaseFromSource,
9} from "./config";9} from "./config";
10 10
11export const createPluginLocalConfigEntrySchema = <11export const createPluginScopeConfigEntrySchema = <
12 TKind extends string,12 TKind extends string,
13 TConfig,13 TConfig,
14>(14>(
@@ -23,17 +23,15 @@ export const createPluginLocalConfigEntrySchema = < · 8 unmodified lines
23 namespace: Schema.optional(Schema.String),23 namespace: Schema.optional(Schema.String),
24 enabled: Schema.optional(Schema.Boolean),24 enabled: Schema.optional(Schema.Boolean),
25 config: input.config,25 config: input.config,
26 connection: Schema.optional(Schema.Unknown),
27 binding: Schema.optional(Schema.Unknown),
28 });26 });
29 27
30export const pluginLocalConfigSourceFromConfig = <TConfig>(input: {28export const pluginScopeConfigSourceFromConfig = <TConfig>(input: {
31 source: Source;29 source: Source;
32 config: TConfig;30 config: TConfig;
33}): LocalConfigSource => ({31}): ExecutorScopeConfigSource => ({
34 ...configSourceBaseFromLocalSource({32 ...scopeConfigSourceBaseFromSource({
35 source: input.source,33 source: input.source,
36 }),34 }),
37 kind: input.source.kind as LocalConfigSource["kind"],35 kind: input.source.kind as ExecutorScopeConfigSource["kind"],
38 config: cloneJson(input.config),36 config: cloneJson(input.config),
39});37});
packages/platform/sdk/src/runtime/sources/source-store/records.ts+2−2
@@ -6,7 +6,7 @@ import type { · 5 unmodified lines
6import { SourceIdSchema } from "#schema";6import { SourceIdSchema } from "#schema";
7import * as Effect from "effect/Effect";7import * as Effect from "effect/Effect";
8 8
9import type { LoadedLocalExecutorConfig } from "../../scope-config";9import type { LoadedExecutorScopeConfig } from "../../scope-config";
10import {10import {
11 LocalConfiguredSourceNotFoundError,11 LocalConfiguredSourceNotFoundError,
12 RuntimeLocalScopeMismatchError,12 RuntimeLocalScopeMismatchError,
@@ -21,7 +21,7 @@ import { trimOrNull } from "./config"; · 8 unmodified lines
21 21
22export const buildLocalSourceRecord = (input: {22export const buildLocalSourceRecord = (input: {
23 scopeId: ScopeId;23 scopeId: ScopeId;
24 loadedConfig: LoadedLocalExecutorConfig;24 loadedConfig: LoadedExecutorScopeConfig;
25 scopeState: LocalScopeState;25 scopeState: LocalScopeState;
26 sourceId: SourceId;26 sourceId: SourceId;
27}): Effect.Effect<27}): Effect.Effect<
packages/platform/sdk/src/schema/index.ts+1−1
@@ -9,7 +9,7 @@ export * from "./models/secret-material"; · 8 unmodified lines
9export * from "./models/secret-store";9export * from "./models/secret-store";
10export * from "./models/policy";10export * from "./models/policy";
11export * from "./models/local-installation";11export * from "./models/local-installation";
12export * from "./models/local-config";12export * from "./models/scope-config";
13export * from "./models/execution";13export * from "./models/execution";
14export * from "./models/source-inspection";14export * from "./models/source-inspection";
15export * from "./models/code-migration";15export * from "./models/code-migration";
packages/platform/sdk/src/schema/models/local-config.ts+0−68
@@ -1,68 +0,0 @@
1import {
2 Schema,
3} from "effect";
4
5export const LocalExecutorRuntimeSchema = Schema.Literal(
6 "quickjs",
7 "ses",
8 "deno",
9);
10
11export const LocalConfigSourceConnectionSchema = Schema.Struct({
12 endpoint: Schema.optional(Schema.NullOr(Schema.String)),
13 auth: Schema.optional(Schema.Unknown),
14});
15
16const LocalConfigSourceEntryBaseSchema = Schema.Struct({
17 name: Schema.optional(Schema.String),
18 namespace: Schema.optional(Schema.String),
19 enabled: Schema.optional(Schema.Boolean),
20 config: Schema.optional(Schema.Unknown),
21 connection: Schema.optional(LocalConfigSourceConnectionSchema),
22 binding: Schema.optional(Schema.Unknown),
23});
24
25export const LocalConfigSourceSchema = Schema.extend(
26 LocalConfigSourceEntryBaseSchema,
27 Schema.Struct({
28 kind: Schema.String,
29 }),
30);
31
32export type LocalConfigSource = typeof LocalConfigSourceSchema.Type;
33
34export const LocalConfigPolicyActionSchema = Schema.Literal("allow", "deny");
35export const LocalConfigPolicyApprovalSchema = Schema.Literal("auto", "manual");
36
37export const LocalConfigPolicySchema = Schema.Struct({
38 match: Schema.String,
39 action: LocalConfigPolicyActionSchema,
40 approval: LocalConfigPolicyApprovalSchema,
41 enabled: Schema.optional(Schema.Boolean),
42 priority: Schema.optional(Schema.Number),
43});
44
45export const LocalConfigWorkspaceSchema = Schema.Struct({
46 name: Schema.optional(Schema.String),
47});
48
49export const LocalExecutorConfigSchema = Schema.Struct({
50 runtime: Schema.optional(LocalExecutorRuntimeSchema),
51 workspace: Schema.optional(LocalConfigWorkspaceSchema),
52 sources: Schema.optional(
53 Schema.Record({
54 key: Schema.String,
55 value: LocalConfigSourceSchema,
56 }),
57 ),
58 policies: Schema.optional(
59 Schema.Record({
60 key: Schema.String,
61 value: LocalConfigPolicySchema,
62 }),
63 ),
64});
65
66export type LocalConfigPolicy = typeof LocalConfigPolicySchema.Type;
67export type LocalExecutorRuntime = typeof LocalExecutorRuntimeSchema.Type;
68export type LocalExecutorConfig = typeof LocalExecutorConfigSchema.Type;
packages/platform/sdk/src/schema/models/scope-config.ts+65−0
@@ -0,0 +1,65 @@
1import {
2 Schema,
3} from "effect";
4
5export const ExecutorRuntimeConfigSchema = Schema.Literal(
6 "quickjs",
7 "ses",
8 "deno",
9);
10
11const ExecutorScopeConfigSourceBaseSchema = Schema.Struct({
12 name: Schema.optional(Schema.String),
13 namespace: Schema.optional(Schema.String),
14 enabled: Schema.optional(Schema.Boolean),
15 config: Schema.optional(Schema.Unknown),
16});
17
18export const ExecutorScopeConfigSourceSchema = Schema.extend(
19 ExecutorScopeConfigSourceBaseSchema,
20 Schema.Struct({
21 kind: Schema.String,
22 }),
23);
24
25export type ExecutorScopeConfigSource = typeof ExecutorScopeConfigSourceSchema.Type;
26
27export const ExecutorScopeConfigPolicyActionSchema = Schema.Literal("allow", "deny");
28export const ExecutorScopeConfigPolicyApprovalSchema = Schema.Literal(
29 "auto",
30 "manual",
31);
32
33export const ExecutorScopeConfigPolicySchema = Schema.Struct({
34 match: Schema.String,
35 action: ExecutorScopeConfigPolicyActionSchema,
36 approval: ExecutorScopeConfigPolicyApprovalSchema,
37 enabled: Schema.optional(Schema.Boolean),
38 priority: Schema.optional(Schema.Number),
39});
40
41export const ExecutorScopeWorkspaceConfigSchema = Schema.Struct({
42 name: Schema.optional(Schema.String),
43});
44
45export const ExecutorScopeConfigSchema = Schema.Struct({
46 runtime: Schema.optional(ExecutorRuntimeConfigSchema),
47 workspace: Schema.optional(ExecutorScopeWorkspaceConfigSchema),
48 sources: Schema.optional(
49 Schema.Record({
50 key: Schema.String,
51 value: ExecutorScopeConfigSourceSchema,
52 }),
53 ),
54 policies: Schema.optional(
55 Schema.Record({
56 key: Schema.String,
57 value: ExecutorScopeConfigPolicySchema,
58 }),
59 ),
60});
61
62export type ExecutorScopeConfigPolicy =
63 typeof ExecutorScopeConfigPolicySchema.Type;
64export type ExecutorRuntimeConfig = typeof ExecutorRuntimeConfigSchema.Type;
65export type ExecutorScopeConfig = typeof ExecutorScopeConfigSchema.Type;
packages/platform/server/src/index.test.ts+5−5
@@ -34,10 +34,10 @@ import { · 33 unmodified lines
34 getOrProvisionLocalInstallation,34 getOrProvisionLocalInstallation,
35 resolveLocalWorkspaceContext,35 resolveLocalWorkspaceContext,
36 writeLocalSourceArtifact,36 writeLocalSourceArtifact,
37 writeProjectLocalExecutorConfig,37 writeProjectExecutorScopeConfig,
38} from "@executor/platform-sdk-file/effect";38} from "@executor/platform-sdk-file/effect";
39import {39import {
40 type LocalConfigSource,40 type ExecutorScopeConfigSource,
41 SourceCatalogIdSchema,41 SourceCatalogIdSchema,
42 SourceCatalogRevisionIdSchema,42 SourceCatalogRevisionIdSchema,
43 SourceIdSchema,43 SourceIdSchema,
@@ -136,7 +136,7 @@ const writeConfiguredLocalMcpSource = (input: { · 92 unmodified lines
136 });136 });
137 const installation = yield* getOrProvisionLocalInstallation({ context });137 const installation = yield* getOrProvisionLocalInstallation({ context });
138 138
139 yield* writeProjectLocalExecutorConfig({139 yield* writeProjectExecutorScopeConfig({
140 context,140 context,
141 config: {141 config: {
142 sources: {142 sources: {
@@ -274,7 +274,7 @@ const writeConfiguredLocalMcpSource = (input: { · 131 unmodified lines
274 274
275const writeConfiguredWorkspaceSources = (input: {275const writeConfiguredWorkspaceSources = (input: {
276 workspaceRoot: string;276 workspaceRoot: string;
277 sources: Record<string, LocalConfigSource>;277 sources: Record<string, ExecutorScopeConfigSource>;
278}) =>278}) =>
279 Effect.gen(function* () {279 Effect.gen(function* () {
280 const context = yield* resolveLocalWorkspaceContext({280 const context = yield* resolveLocalWorkspaceContext({
@@ -282,7 +282,7 @@ const writeConfiguredWorkspaceSources = (input: { · 1 unmodified line
282 });282 });
283 const installation = yield* getOrProvisionLocalInstallation({ context });283 const installation = yield* getOrProvisionLocalInstallation({ context });
284 284
285 yield* writeProjectLocalExecutorConfig({285 yield* writeProjectExecutorScopeConfig({
286 context,286 context,
287 config: {287 config: {
288 sources: input.sources,288 sources: input.sources,
plugins/google-discovery/sdk/index.ts+7−7
@@ -7,7 +7,7 @@ import { · 6 unmodified lines
7 createSourceCatalogSyncResult,7 createSourceCatalogSyncResult,
8} from "@executor/source-core";8} from "@executor/source-core";
9import type {9import type {
10 LocalExecutorConfig,10 ExecutorScopeConfig,
11 Source,11 Source,
12} from "@executor/platform-sdk/schema";12} from "@executor/platform-sdk/schema";
13import {13import {
@@ -17,8 +17,8 @@ import { · 3 unmodified lines
17 defineExecutorSourcePlugin,17 defineExecutorSourcePlugin,
18} from "@executor/platform-sdk/plugins";18} from "@executor/platform-sdk/plugins";
19import {19import {
20 createPluginLocalConfigEntrySchema,20 createPluginScopeConfigEntrySchema,
21 pluginLocalConfigSourceFromConfig,21 pluginScopeConfigSourceFromConfig,
22 SecretMaterialDeleterService,22 SecretMaterialDeleterService,
23 SecretMaterialResolverService,23 SecretMaterialResolverService,
24 SecretMaterialStorerService,24 SecretMaterialStorerService,
@@ -97,7 +97,7 @@ const decodeBatchSourceInput = Schema.decodeUnknownSync( · 72 unmodified lines
97const decodeSession = Schema.decodeUnknownSync(GoogleDiscoveryOAuthSessionSchema);97const decodeSession = Schema.decodeUnknownSync(GoogleDiscoveryOAuthSessionSchema);
98const decodeProviderData = Schema.decodeUnknownSync(GoogleDiscoveryToolProviderDataSchema);98const decodeProviderData = Schema.decodeUnknownSync(GoogleDiscoveryToolProviderDataSchema);
99const decodeGoogleDiscoveryCurrentLocalConfigOption = Schema.decodeUnknownOption(99const decodeGoogleDiscoveryCurrentLocalConfigOption = Schema.decodeUnknownOption(
100 createPluginLocalConfigEntrySchema({100 createPluginScopeConfigEntrySchema({
101 kind: GOOGLE_DISCOVERY_SOURCE_KIND,101 kind: GOOGLE_DISCOVERY_SOURCE_KIND,
102 config: GoogleDiscoveryStoredSourceDataSchema,102 config: GoogleDiscoveryStoredSourceDataSchema,
103 }),103 }),
@@ -563,7 +563,7 @@ const googleDiscoveryConnectInputFromAddInput = ( · 459 unmodified lines
563 563
564const googleDiscoveryStoredSourceDataFromLocalConfig = (input: {564const googleDiscoveryStoredSourceDataFromLocalConfig = (input: {
565 config: unknown;565 config: unknown;
566 loadedConfig: LocalExecutorConfig | null;566 loadedConfig: ExecutorScopeConfig | null;
567}): GoogleDiscoveryStoredSourceData => {567}): GoogleDiscoveryStoredSourceData => {
568 const current = decodeGoogleDiscoveryCurrentLocalConfigOption(input.config);568 const current = decodeGoogleDiscoveryCurrentLocalConfigOption(input.config);
569 if (Option.isSome(current)) {569 if (Option.isSome(current)) {
@@ -629,9 +629,9 @@ export const googleDiscoverySdkPlugin = (options: { · 59 unmodified lines
629 }629 }
630 }),630 }),
631 },631 },
632 localConfig: {632 scopeConfig: {
633 toConfigSource: ({ source, stored }) =>633 toConfigSource: ({ source, stored }) =>
634 pluginLocalConfigSourceFromConfig({634 pluginScopeConfigSourceFromConfig({
635 source,635 source,
636 config: stored,636 config: stored,
637 }),637 }),
plugins/graphql/sdk/index.ts+7−7
@@ -12,7 +12,7 @@ import { · 11 unmodified lines
12 createSourceCatalogSyncResult,12 createSourceCatalogSyncResult,
13} from "@executor/source-core";13} from "@executor/source-core";
14import type {14import type {
15 LocalExecutorConfig,15 ExecutorScopeConfig,
16 Source,16 Source,
17} from "@executor/platform-sdk/schema";17} from "@executor/platform-sdk/schema";
18import {18import {
@@ -23,8 +23,8 @@ import { · 4 unmodified lines
23} from "@executor/platform-sdk/plugins";23} from "@executor/platform-sdk/plugins";
24import {24import {
25 SecretMaterialResolverService,25 SecretMaterialResolverService,
26 createPluginLocalConfigEntrySchema,26 createPluginScopeConfigEntrySchema,
27 pluginLocalConfigSourceFromConfig,27 pluginScopeConfigSourceFromConfig,
28 provideExecutorRuntime,28 provideExecutorRuntime,
29 runtimeEffectError,29 runtimeEffectError,
30} from "@executor/platform-sdk/runtime";30} from "@executor/platform-sdk/runtime";
@@ -151,7 +151,7 @@ const asStringRecord = (value: unknown): Record<string, string> => · 120 unmodified lines
151 );151 );
152 152
153const decodeGraphqlCurrentLocalConfigOption = Schema.decodeUnknownOption(153const decodeGraphqlCurrentLocalConfigOption = Schema.decodeUnknownOption(
154 createPluginLocalConfigEntrySchema({154 createPluginScopeConfigEntrySchema({
155 kind: "graphql",155 kind: "graphql",
156 config: GraphqlStoredSourceDataSchema,156 config: GraphqlStoredSourceDataSchema,
157 }),157 }),
@@ -336,7 +336,7 @@ const graphqlConnectInputFromAddInput = ( · 178 unmodified lines
336 336
337const graphqlStoredSourceDataFromLocalConfig = (input: {337const graphqlStoredSourceDataFromLocalConfig = (input: {
338 config: unknown;338 config: unknown;
339 loadedConfig: LocalExecutorConfig | null;339 loadedConfig: ExecutorScopeConfig | null;
340}): GraphqlStoredSourceData => {340}): GraphqlStoredSourceData => {
341 const current = decodeGraphqlCurrentLocalConfigOption(input.config);341 const current = decodeGraphqlCurrentLocalConfigOption(input.config);
342 if (Option.isSome(current)) {342 if (Option.isSome(current)) {
@@ -397,9 +397,9 @@ export const graphqlSdkPlugin = (options: { · 54 unmodified lines
397 toConfig: ({ source, stored }) =>397 toConfig: ({ source, stored }) =>
398 sourceConfigFromStored(source, normalizeStoredSourceData(stored)),398 sourceConfigFromStored(source, normalizeStoredSourceData(stored)),
399 },399 },
400 localConfig: {400 scopeConfig: {
401 toConfigSource: ({ source, stored }) =>401 toConfigSource: ({ source, stored }) =>
402 pluginLocalConfigSourceFromConfig({402 pluginScopeConfigSourceFromConfig({
403 source,403 source,
404 config: normalizeStoredSourceData(stored),404 config: normalizeStoredSourceData(stored),
405 }),405 }),
plugins/mcp/sdk/index.ts+5−5
@@ -31,8 +31,8 @@ import { · 30 unmodified lines
31 defineExecutorSourcePlugin,31 defineExecutorSourcePlugin,
32} from "@executor/platform-sdk/plugins";32} from "@executor/platform-sdk/plugins";
33import {33import {
34 createPluginLocalConfigEntrySchema,34 createPluginScopeConfigEntrySchema,
35 pluginLocalConfigSourceFromConfig,35 pluginScopeConfigSourceFromConfig,
36 SecretMaterialDeleterService,36 SecretMaterialDeleterService,
37 SecretMaterialResolverService,37 SecretMaterialResolverService,
38 SecretMaterialStorerService,38 SecretMaterialStorerService,
@@ -250,7 +250,7 @@ const normalizeStringArray = ( · 211 unmodified lines
250};250};
251 251
252const decodeMcpCurrentLocalConfigOption = Schema.decodeUnknownOption(252const decodeMcpCurrentLocalConfigOption = Schema.decodeUnknownOption(
253 createPluginLocalConfigEntrySchema({253 createPluginScopeConfigEntrySchema({
254 kind: "mcp",254 kind: "mcp",
255 config: McpStoredSourceDataSchema,255 config: McpStoredSourceDataSchema,
256 }),256 }),
@@ -720,9 +720,9 @@ export const mcpSdkPlugin = ( · 463 unmodified lines
720 }720 }
721 }),721 }),
722 },722 },
723 localConfig: {723 scopeConfig: {
724 toConfigSource: ({ source, stored }) =>724 toConfigSource: ({ source, stored }) =>
725 pluginLocalConfigSourceFromConfig({725 pluginScopeConfigSourceFromConfig({
726 source,726 source,
727 config: stored,727 config: stored,
728 }),728 }),
plugins/openapi/sdk/index.ts+7−7
@@ -8,7 +8,7 @@ import { · 7 unmodified lines
8 createSourceCatalogSyncResult,8 createSourceCatalogSyncResult,
9} from "@executor/source-core";9} from "@executor/source-core";
10import type {10import type {
11 LocalExecutorConfig,11 ExecutorScopeConfig,
12 Source,12 Source,
13} from "@executor/platform-sdk/schema";13} from "@executor/platform-sdk/schema";
14import {14import {
@@ -18,9 +18,9 @@ import { · 3 unmodified lines
18 defineExecutorSourcePlugin,18 defineExecutorSourcePlugin,
19} from "@executor/platform-sdk/plugins";19} from "@executor/platform-sdk/plugins";
20import {20import {
21 createPluginLocalConfigEntrySchema,21 createPluginScopeConfigEntrySchema,
22 provideExecutorRuntime,22 provideExecutorRuntime,
23 pluginLocalConfigSourceFromConfig,23 pluginScopeConfigSourceFromConfig,
24 runtimeEffectError,24 runtimeEffectError,
25 SecretMaterialResolverService,25 SecretMaterialResolverService,
26} from "@executor/platform-sdk/runtime";26} from "@executor/platform-sdk/runtime";
@@ -157,7 +157,7 @@ const OpenApiLocalSourceConfigSchema = Schema.Struct({ · 130 unmodified lines
157});157});
158 158
159const decodeOpenApiCurrentLocalConfigOption = Schema.decodeUnknownOption(159const decodeOpenApiCurrentLocalConfigOption = Schema.decodeUnknownOption(
160 createPluginLocalConfigEntrySchema({160 createPluginScopeConfigEntrySchema({
161 kind: "openapi",161 kind: "openapi",
162 config: OpenApiLocalSourceConfigSchema,162 config: OpenApiLocalSourceConfigSchema,
163 }),163 }),
@@ -193,7 +193,7 @@ const configFromStoredSourceData = ( · 29 unmodified lines
193 193
194const openApiStoredSourceDataFromLocalConfig = (input: {194const openApiStoredSourceDataFromLocalConfig = (input: {
195 config: unknown;195 config: unknown;
196 loadedConfig: LocalExecutorConfig | null;196 loadedConfig: ExecutorScopeConfig | null;
197}): OpenApiStoredSourceData => {197}): OpenApiStoredSourceData => {
198 const current = decodeOpenApiCurrentLocalConfigOption(input.config);198 const current = decodeOpenApiCurrentLocalConfigOption(input.config);
199 if (Option.isSome(current)) {199 if (Option.isSome(current)) {
@@ -539,9 +539,9 @@ export const openApiSdkPlugin = ( · 339 unmodified lines
539 toConfig: ({ source, stored }) =>539 toConfig: ({ source, stored }) =>
540 configFromStoredSourceData(source, normalizeStoredSourceData(stored)),540 configFromStoredSourceData(source, normalizeStoredSourceData(stored)),
541 },541 },
542 localConfig: {542 scopeConfig: {
543 toConfigSource: ({ source, stored }) =>543 toConfigSource: ({ source, stored }) =>
544 pluginLocalConfigSourceFromConfig({544 pluginScopeConfigSourceFromConfig({
545 source,545 source,
546 config: {546 config: {
547 specUrl: stored.specUrl,547 specUrl: stored.specUrl,