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
66 import * as Effect from "effect/Effect";
77 import * as Exit from "effect/Exit";
88
9−import type { LocalExecutorConfig } from "@executor/platform-sdk/schema";
9+import type { ExecutorScopeConfig } from "@executor/platform-sdk/schema";
1010
1111 import {
12− readOptionalLocalExecutorConfig,
12+ readOptionalExecutorScopeConfig,
1313 resolveLocalWorkspaceContext,
1414 } from "./config";
1515 import {
@@ -186,8 +186,8 @@ const copyRecursive = ( · 170 unmodified lines
186186 });
187187
188188 const sanitizeSourceConfig = (
189− sourceConfig: NonNullable<LocalExecutorConfig["sources"]>[string],
190−): NonNullable<LocalExecutorConfig["sources"]>[string] => sourceConfig;
189+ sourceConfig: NonNullable<ExecutorScopeConfig["sources"]>[string],
190+): NonNullable<ExecutorScopeConfig["sources"]>[string] => sourceConfig;
191191
192192 const main = Effect.gen(function* () {
193193 const args = yield* Effect.try({
@@ -218,7 +218,7 @@ const main = Effect.gen(function* () { · 24 unmodified lines
218218 ),
219219 );
220220
221− const projectConfig = yield* readOptionalLocalExecutorConfig(
221+ const projectConfig = yield* readOptionalExecutorScopeConfig(
222222 context.projectConfigPath,
223223 ).pipe(
224224 Effect.mapError((cause) =>
@@ -293,7 +293,7 @@ const main = Effect.gen(function* () { · 68 unmodified lines
293293 sources: {
294294 [args.sourceId]: sanitizeSourceConfig(sourceConfig),
295295 },
296− } satisfies LocalExecutorConfig;
296+ } satisfies ExecutorScopeConfig;
297297
298298 const fixtureState = {
299299 version: 1 as const,
packages/platform/sdk-file/src/config-codec.ts+93−0
@@ -0,0 +1,93 @@
1+import {
2+ type ParseError as JsoncParseError,
3+ parse as parseJsoncDocument,
4+ printParseErrorCode,
5+} from "jsonc-parser/lib/esm/main.js";
6+
7+import {
8+ ExecutorScopeConfigSchema,
9+ type ExecutorScopeConfig,
10+} from "@executor/platform-sdk/schema";
11+import * as Schema from "effect/Schema";
12+import {
13+ ExecutorScopeConfigDecodeError,
14+ unknownLocalErrorDetails,
15+} from "./errors";
16+
17+export const decodeExecutorScopeConfig = Schema.decodeUnknownSync(
18+ ExecutorScopeConfigSchema,
19+ {
20+ onExcessProperty: "error",
21+ },
22+);
23+
24+export const encodeExecutorScopeConfig = (
25+ config: ExecutorScopeConfig,
26+): string => `${JSON.stringify(config, null, 2)}\n`;
27+
28+const 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+
52+export 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+
72+export 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 @@
1+import { tmpdir } from "node:os";
2+import { join } from "node:path";
3+import { FileSystem } from "@effect/platform";
4+import { NodeFileSystem } from "@effect/platform-node";
5+import { describe, expect, it } from "@effect/vitest";
6+import * as Effect from "effect/Effect";
7+
8+import {
9+ loadExecutorScopeConfig,
10+ resolveLocalWorkspaceContext,
11+} from "./config";
12+import {
13+ migrateLegacyExecutorScopeConfigs,
14+} from "./config-migrations";
15+import {
16+ createLocalExecutorRepositoriesEffect,
17+} from "./index";
18+
19+const 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+
28+const makeWorkspaceRoot = () =>
29+ FileSystem.FileSystem.pipe(
30+ Effect.flatMap((fs) =>
31+ fs.makeTempDirectory({
32+ directory: tmpdir(),
33+ prefix: "executor-config-migrations-",
34+ })
35+ ),
36+ );
37+
38+describe("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 @@
1+import { FileSystem } from "@effect/platform";
2+import * as Effect from "effect/Effect";
3+import type {
4+ ExecutorScopeConfigDecodeError,
5+ LocalFileSystemError,
6+} from "../errors";
7+import {
8+ migrateLegacyExecutorScopeConfigs,
9+ type StartupConfigMigrationContext,
10+} from "./legacy-local-config";
11+
12+type 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.
22+const startupConfigMigrations: readonly StartupConfigMigration[] = [
23+ migrateLegacyExecutorScopeConfigs,
24+];
25+
26+export 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+
45+export {
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 @@
1+import { FileSystem } from "@effect/platform";
2+import {
3+ type ExecutorScopeConfigSource,
4+ type ExecutorScopeConfig,
5+} from "@executor/platform-sdk/schema";
6+import * as Effect from "effect/Effect";
7+import {
8+ decodeExecutorScopeConfig,
9+ encodeExecutorScopeConfig,
10+ parseJsoncValue,
11+} from "../config-codec";
12+import {
13+ ExecutorScopeConfigDecodeError,
14+ LocalFileSystemError,
15+ unknownLocalErrorDetails,
16+} from "../errors";
17+
18+const LEGACY_CONFIG_BACKUP_SUFFIX = ".legacy-backup";
19+
20+const 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+
28+const 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+
33+const asString = (value: unknown): string | null =>
34+ typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
35+
36+const 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+
48+const 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+
67+const asFiniteNumber = (value: unknown): number | null =>
68+ typeof value === "number" && Number.isFinite(value) ? value : null;
69+
70+const asBoolean = (value: unknown): boolean | null =>
71+ typeof value === "boolean" ? value : null;
72+
73+const asSecretRef = (value: unknown): { secretId: string } | null => {
74+ const record = asRecord(value);
75+ const secretId = asString(record?.secretId);
76+ return secretId ? { secretId } : null;
77+};
78+
79+const normalizeLegacyHeaderName = (value: unknown): string | null =>
80+ asString(value);
81+
82+const 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+
94+const defaultGoogleDiscoveryUrl = (
95+ service: string,
96+ version: string,
97+): string =>
98+ `https://www.googleapis.com/discovery/v1/apis/${encodeURIComponent(service)}/${encodeURIComponent(version)}/rest`;
99+
100+const 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+
111+const 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+
119+const 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+
149+const 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+
195+const 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+
239+const 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+
396+const 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+
443+const 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+
514+export type StartupConfigMigrationContext = {
515+ homeConfigPath: string;
516+ projectConfigPath: string;
517+};
518+
519+export 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
66 import * as Effect from "effect/Effect";
77
88 import {
9− loadLocalExecutorConfig,
10− mergeLocalExecutorConfigs,
9+ loadExecutorScopeConfig,
10+ mergeExecutorScopeConfigs,
1111 resolveDefaultHomeConfigCandidates,
1212 resolveDefaultHomeStateDirectory,
1313 resolveLocalWorkspaceContext,
@@ -53,7 +53,7 @@ describe("local-config", () => { · 39 unmodified lines
5353 );
5454
5555 const context = yield* resolveLocalWorkspaceContext({ workspaceRoot });
56− const loaded = yield* loadLocalExecutorConfig(context);
56+ const loaded = yield* loadExecutorScopeConfig(context);
5757
5858 expect(loaded.config?.sources?.github?.kind).toBe("openapi");
5959 expect(loaded.config?.runtime).toBe("ses");
@@ -83,7 +83,7 @@ describe("local-config", () => { · 23 unmodified lines
8383 );
8484
8585 const context = yield* resolveLocalWorkspaceContext({ workspaceRoot });
86− const failure = yield* Effect.flip(loadLocalExecutorConfig(context));
86+ const failure = yield* Effect.flip(loadExecutorScopeConfig(context));
8787
8888 expect(failure.message).toContain("Invalid executor config");
8989 expect(failure.message).toContain("line 5, column 7");
@@ -129,7 +129,7 @@ describe("local-config", () => { · 39 unmodified lines
129129 });
130130
131131 it("lets project config override the merged runtime", () => {
132− const merged = mergeLocalExecutorConfigs(
132+ const merged = mergeExecutorScopeConfigs(
133133 {
134134 runtime: "quickjs",
135135 sources: {},
packages/platform/sdk-file/src/config.ts+38−93
@@ -1,37 +1,30 @@
11 import { homedir } from "node:os";
22 import { basename, dirname, isAbsolute, join, resolve } from "node:path";
33 import { FileSystem } from "@effect/platform";
4−import {
5− type ParseError as JsoncParseError,
6− parse as parseJsoncDocument,
7− printParseErrorCode,
8−} from "jsonc-parser/lib/esm/main.js";
94
105 import {
11− LocalExecutorConfigSchema,
12− type LocalExecutorConfig,
13− type LocalConfigPolicy,
14− type LocalConfigSource,
6+ type ExecutorScopeConfig,
7+ type ExecutorScopeConfigPolicy,
8+ type ExecutorScopeConfigSource,
159 } from "@executor/platform-sdk/schema";
16−import type { LoadedLocalExecutorConfig } from "@executor/platform-sdk/runtime";
10+import type { LoadedExecutorScopeConfig } from "@executor/platform-sdk/runtime";
1711 import * as Effect from "effect/Effect";
18−import * as Schema from "effect/Schema";
1912 import {
20− LocalExecutorConfigDecodeError,
13+ decodeExecutorScopeConfig,
14+ encodeExecutorScopeConfig,
15+ parseExecutorScopeConfig,
16+} from "./config-codec";
17+import {
18+ ExecutorScopeConfigDecodeError,
2119 LocalFileSystemError,
2220 unknownLocalErrorDetails,
2321 } from "./errors";
2422
25−const decodeLocalExecutorConfig = Schema.decodeUnknownSync(LocalExecutorConfigSchema);
26−
27−export type FileLoadedExecutorConfig = LoadedLocalExecutorConfig & {
23+export type FileLoadedExecutorConfig = LoadedExecutorScopeConfig & {
2824 homeConfigPath: string;
2925 projectConfigPath: string;
3026 };
3127
32−export const encodeLocalExecutorConfig = (config: LocalExecutorConfig): string =>
33− `${JSON.stringify(config, null, 2)}\n`;
34−
3528 export const resolveConfigRelativePath = (input: {
3629 path: string;
3730 scopeRoot: string;
@@ -49,7 +42,8 @@ export const resolveConfigRelativePath = (input: { · 11 unmodified lines
4942 return resolve(input.scopeRoot, trimmed);
5043 };
5144
52−export type { LoadedLocalExecutorConfig } from "@executor/platform-sdk/runtime";
45+export type { LoadedExecutorScopeConfig } from "@executor/platform-sdk/runtime";
46+export { encodeExecutorScopeConfig } from "./config-codec";
5347
5448 const PROJECT_CONFIG_BASENAME = "executor.jsonc";
5549 const PROJECT_CONFIG_DIRECTORY = ".executor";
@@ -173,59 +167,10 @@ export const resolveDefaultHomeStateDirectory = (input: { · 117 unmodified lines
173167 } = {}): string =>
174168 defaultExecutorStateDirectory(input);
175169
176−const 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−
197−const 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−
225170 const mergeSourceMaps = (
226− base: Record<string, LocalConfigSource> | undefined,
227− extra: Record<string, LocalConfigSource> | undefined,
228−): Record<string, LocalConfigSource> | undefined => {
171+ base: Record<string, ExecutorScopeConfigSource> | undefined,
172+ extra: Record<string, ExecutorScopeConfigSource> | undefined,
173+): Record<string, ExecutorScopeConfigSource> | undefined => {
229174 if (!base && !extra) {
230175 return undefined;
231176 }
@@ -236,9 +181,9 @@ const mergeSourceMaps = ( · 4 unmodified lines
236181 };
237182
238183 const mergePolicyMaps = (
239− base: Record<string, LocalConfigPolicy> | undefined,
240− extra: Record<string, LocalConfigPolicy> | undefined,
241−): Record<string, LocalConfigPolicy> | undefined => {
184+ base: Record<string, ExecutorScopeConfigPolicy> | undefined,
185+ extra: Record<string, ExecutorScopeConfigPolicy> | undefined,
186+): Record<string, ExecutorScopeConfigPolicy> | undefined => {
242187 if (!base && !extra) {
243188 return undefined;
244189 }
@@ -248,15 +193,15 @@ const mergePolicyMaps = ( · 3 unmodified lines
248193 };
249194 };
250195
251−export const mergeLocalExecutorConfigs = (
252− base: LocalExecutorConfig | null,
253− extra: LocalExecutorConfig | null,
254−): LocalExecutorConfig | null => {
196+export const mergeExecutorScopeConfigs = (
197+ base: ExecutorScopeConfig | null,
198+ extra: ExecutorScopeConfig | null,
199+): ExecutorScopeConfig | null => {
255200 if (!base && !extra) {
256201 return null;
257202 }
258203
259− return decodeLocalExecutorConfig({
204+ return decodeExecutorScopeConfig({
260205 runtime: extra?.runtime ?? base?.runtime,
261206 workspace: {
262207 ...base?.workspace,
@@ -370,11 +315,11 @@ export const resolveLocalWorkspaceContext = (input: { · 107 unmodified lines
370315 };
371316 });
372317
373−export const readOptionalLocalExecutorConfig = (
318+export const readOptionalExecutorScopeConfig = (
374319 path: string,
375320 ): Effect.Effect<
376− LocalExecutorConfig | null,
377− LocalFileSystemError | LocalExecutorConfigDecodeError,
321+ ExecutorScopeConfig | null,
322+ LocalFileSystemError | ExecutorScopeConfigDecodeError,
378323 FileSystem.FileSystem
379324 > =>
380325 Effect.gen(function* () {
@@ -390,11 +335,11 @@ export const readOptionalLocalExecutorConfig = ( · 9 unmodified lines
390335 Effect.mapError(mapFileSystemError(path, "read config")),
391336 );
392337 return yield* Effect.try({
393− try: () => parseJsonc({ path, content }),
338+ try: () => parseExecutorScopeConfig({ path, content }),
394339 catch: (cause) =>
395− cause instanceof LocalExecutorConfigDecodeError
340+ cause instanceof ExecutorScopeConfigDecodeError
396341 ? cause
397− : new LocalExecutorConfigDecodeError({
342+ : new ExecutorScopeConfigDecodeError({
398343 message: `Invalid executor config at ${path}: ${unknownLocalErrorDetails(cause)}`,
399344 path,
400345 details: unknownLocalErrorDetails(cause),
@@ -402,21 +347,21 @@ export const readOptionalLocalExecutorConfig = ( · 1 unmodified line
402347 });
403348 });
404349
405−export const loadLocalExecutorConfig = (
350+export const loadExecutorScopeConfig = (
406351 context: ResolvedLocalWorkspaceContext,
407352 ): Effect.Effect<
408353 FileLoadedExecutorConfig,
409− LocalFileSystemError | LocalExecutorConfigDecodeError,
354+ LocalFileSystemError | ExecutorScopeConfigDecodeError,
410355 FileSystem.FileSystem
411356 > =>
412357 Effect.gen(function* () {
413358 const [homeConfig, projectConfig] = yield* Effect.all([
414− readOptionalLocalExecutorConfig(context.homeConfigPath),
415− readOptionalLocalExecutorConfig(context.projectConfigPath),
359+ readOptionalExecutorScopeConfig(context.homeConfigPath),
360+ readOptionalExecutorScopeConfig(context.projectConfigPath),
416361 ]);
417362
418363 return {
419− config: mergeLocalExecutorConfigs(homeConfig, projectConfig),
364+ config: mergeExecutorScopeConfigs(homeConfig, projectConfig),
420365 homeConfig,
421366 projectConfig,
422367 homeConfigPath: context.homeConfigPath,
@@ -424,9 +369,9 @@ export const loadLocalExecutorConfig = ( · 1 unmodified line
424369 };
425370 });
426371
427−export const writeProjectLocalExecutorConfig = (input: {
372+export const writeProjectExecutorScopeConfig = (input: {
428373 context: ResolvedLocalWorkspaceContext;
429− config: LocalExecutorConfig;
374+ config: ExecutorScopeConfig;
430375 }): Effect.Effect<void, LocalFileSystemError, FileSystem.FileSystem> =>
431376 Effect.gen(function* () {
432377 const fs = yield* FileSystem.FileSystem;
@@ -435,7 +380,7 @@ export const writeProjectLocalExecutorConfig = (input: { · 2 unmodified lines
435380 );
436381 yield* fs.writeFileString(
437382 input.context.projectConfigPath,
438− encodeLocalExecutorConfig(input.config),
383+ encodeExecutorScopeConfig(input.config),
439384 ).pipe(
440385 Effect.mapError(mapFileSystemError(input.context.projectConfigPath, "write config")),
441386 );
packages/platform/sdk-file/src/effect.ts+3−3
@@ -5,7 +5,7 @@ export { · 4 unmodified lines
55 createLocalExecutorRepositoriesEffect,
66 deriveLocalInstallation,
77 getOrProvisionLocalInstallation,
8− loadLocalExecutorConfig,
8+ loadExecutorScopeConfig,
99 loadLocalExecutorStateSnapshot,
1010 loadLocalInstallation,
1111 loadLocalWorkspaceState,
@@ -20,9 +20,9 @@ export { · 8 unmodified lines
2020 writeLocalExecutorStateSnapshot,
2121 writeLocalSourceArtifact,
2222 writeLocalWorkspaceState,
23− writeProjectLocalExecutorConfig,
23+ writeProjectExecutorScopeConfig,
2424 } from "./index";
2525 export type {
26− LoadedLocalExecutorConfig,
26+ LoadedExecutorScopeConfig,
2727 ResolvedLocalWorkspaceContext,
2828 } from "./index";
packages/platform/sdk-file/src/errors.ts+2−2
@@ -12,8 +12,8 @@ export class LocalFileSystemError extends Data.TaggedError( · 11 unmodified lines
1212 readonly details: string;
1313 }> {}
1414
15−export class LocalExecutorConfigDecodeError extends Data.TaggedError(
16− "LocalExecutorConfigDecodeError",
15+export class ExecutorScopeConfigDecodeError extends Data.TaggedError(
16+ "ExecutorScopeConfigDecodeError",
1717 )<{
1818 readonly message: string;
1919 readonly path: string;
packages/platform/sdk-file/src/executor-state-codec.ts+82−0
@@ -0,0 +1,82 @@
1+import { join } from "node:path";
2+
3+import {
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";
15+import * as Schema from "effect/Schema";
16+
17+import type { ResolvedLocalWorkspaceContext } from "./config";
18+import { deriveLocalInstallation } from "./installation";
19+
20+export const LOCAL_EXECUTOR_STATE_VERSION = 2 as const;
21+export const LOCAL_EXECUTOR_STATE_BASENAME = "executor-state.json";
22+
23+export const SecretMaterialStoredDataRecordSchema = Schema.Struct({
24+ secretId: Schema.String,
25+ data: Schema.Unknown,
26+});
27+
28+export type SecretMaterialStoredDataRecord =
29+ typeof SecretMaterialStoredDataRecordSchema.Type;
30+
31+export 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+
41+export type LocalExecutorStateSnapshot =
42+ typeof LocalExecutorStateSnapshotSchema.Type;
43+
44+export const decodeLocalExecutorStateSnapshot = Schema.decodeUnknownSync(
45+ LocalExecutorStateSnapshotSchema,
46+ {
47+ onExcessProperty: "error",
48+ },
49+);
50+
51+export const encodeLocalExecutorStateSnapshot = (
52+ state: LocalExecutorStateSnapshot,
53+): string => `${JSON.stringify(state, null, 2)}\n`;
54+
55+export const defaultLocalExecutorStateSnapshot =
56+ (): LocalExecutorStateSnapshot => ({
57+ version: LOCAL_EXECUTOR_STATE_VERSION,
58+ secretStores: [],
59+ secretMaterials: [],
60+ secretMaterialStoredData: [],
61+ executions: [],
62+ executionInteractions: [],
63+ executionSteps: [],
64+ });
65+
66+export 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+
76+export 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 @@
1+import { dirname, join } from "node:path";
2+
3+import { FileSystem } from "@effect/platform";
4+import { NodeFileSystem } from "@effect/platform-node";
5+import { describe, expect, it } from "@effect/vitest";
6+import * as Effect from "effect/Effect";
7+
8+import { resolveLocalWorkspaceContext } from "./config";
9+import {
10+ createLocalExecutorRepositoriesEffect,
11+ loadLocalExecutorStateSnapshot,
12+} from "./index";
13+import { migrateLegacyLocalExecutorState } from "./executor-state-migrations";
14+import { localExecutorStatePath } from "./executor-state-store";
15+import { deriveLocalInstallation } from "./installation";
16+
17+const makeWorkspaceRoot = () =>
18+ FileSystem.FileSystem.pipe(
19+ Effect.flatMap((fs) =>
20+ fs.makeTempDirectory({
21+ prefix: "executor-state-migrations-",
22+ })
23+ ),
24+ );
25+
26+describe("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 @@
1+import { FileSystem } from "@effect/platform";
2+import * as Effect from "effect/Effect";
3+
4+import type { ResolvedLocalWorkspaceContext } from "../config";
5+import type { LocalFileSystemError } from "../errors";
6+import { migrateLegacyLocalExecutorState } from "./legacy-local-executor-state";
7+
8+type 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.
13+const startupExecutorStateMigrations: readonly StartupExecutorStateMigration[] = [
14+ migrateLegacyLocalExecutorState,
15+];
16+
17+export 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+
32+export { 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 @@
1+import { FileSystem } from "@effect/platform";
2+import * as Effect from "effect/Effect";
3+
4+import type { ResolvedLocalWorkspaceContext } from "../config";
5+import {
6+ decodeLocalExecutorStateSnapshot,
7+ encodeLocalExecutorStateSnapshot,
8+ localExecutorStatePath,
9+ LOCAL_EXECUTOR_STATE_VERSION,
10+ type LocalExecutorStateSnapshot,
11+} from "../executor-state-codec";
12+import {
13+ LocalFileSystemError,
14+ unknownLocalErrorDetails,
15+} from "../errors";
16+import { deriveLocalInstallation } from "../installation";
17+
18+const LEGACY_EXECUTOR_STATE_BACKUP_SUFFIX = ".legacy-backup";
19+const LEGACY_LOCAL_SECRET_STORE_ID = "sts_builtin_local";
20+const LEGACY_KEYCHAIN_SECRET_STORE_ID = "sts_builtin_keychain";
21+
22+const 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+
30+const 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+
35+const asString = (value: unknown): string | null =>
36+ typeof value === "string" && value.length > 0 ? value : null;
37+
38+const asNullableString = (value: unknown): string | null =>
39+ value === null
40+ ? null
41+ : typeof value === "string"
42+ ? value
43+ : null;
44+
45+const asBoolean = (value: unknown): boolean | null =>
46+ typeof value === "boolean" ? value : null;
47+
48+const asFiniteNumber = (value: unknown): number | null =>
49+ typeof value === "number" && Number.isFinite(value) ? value : null;
50+
51+const 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+
58+const 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+
73+const 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+
121+const 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+
151+const 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+
173+const canDecodeCurrentExecutorStateSnapshot = (value: unknown): boolean => {
174+ try {
175+ decodeLocalExecutorStateSnapshot(value);
176+ return true;
177+ } catch {
178+ return false;
179+ }
180+};
181+
182+const 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+
294+const 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+
350+export 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 @@
11 import { randomUUID } from "node:crypto";
2−import { dirname, join } from "node:path";
2+import { dirname } from "node:path";
33 import { FileSystem } from "@effect/platform";
44 import { NodeFileSystem } from "@effect/platform-node";
55
@@ -7,66 +7,32 @@ import { · 1 unmodified line
77 type Execution,
88 type ExecutionInteraction,
99 type ExecutionStep,
10− ExecutionInteractionSchema,
11− ExecutionSchema,
12− ExecutionStepSchema,
13− SecretMaterialSchema,
1410 type SecretMaterial,
15− SecretStoreSchema,
1611 type SecretStore,
1712 } from "@executor/platform-sdk/schema";
1813 import * as Effect from "effect/Effect";
1914 import * as Option from "effect/Option";
20−import * as Schema from "effect/Schema";
2115
2216 import type { ResolvedLocalWorkspaceContext } from "./config";
23−import { deriveLocalInstallation } from "./installation";
17+import {
18+ decodeLocalExecutorStateSnapshot,
19+ defaultLocalExecutorStateSnapshot,
20+ encodeLocalExecutorStateSnapshot,
21+ localExecutorStatePath,
22+ LOCAL_EXECUTOR_STATE_VERSION,
23+ type LocalExecutorStateSnapshot,
24+ type SecretMaterialStoredDataRecord,
25+} from "./executor-state-codec";
2426 import {
2527 LocalFileSystemError,
2628 unknownLocalErrorDetails,
2729 } from "./errors";
2830
29−const LOCAL_EXECUTOR_STATE_VERSION = 2 as const;
30−const LOCAL_EXECUTOR_STATE_BASENAME = "executor-state.json";
31−
32−const SecretMaterialStoredDataRecordSchema = Schema.Struct({
33− secretId: Schema.String,
34− data: Schema.Unknown,
35−});
36−
37−type SecretMaterialStoredDataRecord = typeof SecretMaterialStoredDataRecordSchema.Type;
38−
39−const 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−
49−export type LocalExecutorStateSnapshot = typeof LocalExecutorStateSnapshotSchema.Type;
50−
5131 export type LocalExecutorStatePersistence = {
5232 executorState: LocalExecutorStateStore;
5333 close: () => Promise<void>;
5434 };
5535
56−const decodeLocalExecutorStateSnapshot = Schema.decodeUnknown(
57− LocalExecutorStateSnapshotSchema,
58−);
59−
60−const defaultLocalExecutorStateSnapshot = (): LocalExecutorStateSnapshot => ({
61− version: LOCAL_EXECUTOR_STATE_VERSION,
62− secretStores: [],
63− secretMaterials: [],
64− secretMaterialStoredData: [],
65− executions: [],
66− executionInteractions: [],
67− executionSteps: [],
68−});
69−
7036 const cloneValue = <T>(value: T): T =>
7137 JSON.parse(JSON.stringify(value)) as T;
7238 const mapFileSystemError = (path: string, action: string) => (cause: unknown) =>
@@ -84,16 +50,6 @@ const sortByUpdatedAtAndIdDesc = <T extends { updatedAt: number; id: string }>( · 11 unmodified lines
8450 right.updatedAt - left.updatedAt || right.id.localeCompare(left.id),
8551 );
8652
87−const localExecutorStatePath = (
88− context: ResolvedLocalWorkspaceContext,
89−): string =>
90− join(
91− context.homeStateDirectory,
92− "workspaces",
93− deriveLocalInstallation(context).scopeId,
94− LOCAL_EXECUTOR_STATE_BASENAME,
95− );
96−
9753 const bindFileSystem = <A, E>(
9854 fileSystem: FileSystem.FileSystem,
9955 effect: Effect.Effect<A, E, FileSystem.FileSystem>,
@@ -125,10 +81,10 @@ const readStateFromDisk = ( · 25 unmodified lines
12581 try: () => JSON.parse(content) as unknown,
12682 catch: mapFileSystemError(path, "parse executor state"),
12783 });
128− const decoded = yield* decodeLocalExecutorStateSnapshot(parsed).pipe(
129− Effect.mapError(mapFileSystemError(path, "decode executor state")),
130− );
131− return decoded;
84+ return yield* Effect.try({
85+ try: () => decodeLocalExecutorStateSnapshot(parsed),
86+ catch: mapFileSystemError(path, "decode executor state"),
87+ });
13288 });
13389
13490 const writeStateToDisk = (
@@ -143,7 +99,7 @@ const writeStateToDisk = ( · 8 unmodified lines
14399 yield* fs.makeDirectory(dirname(path), { recursive: true }).pipe(
144100 Effect.mapError(mapFileSystemError(dirname(path), "create executor state directory")),
145101 );
146− yield* fs.writeFileString(tempPath, `${JSON.stringify(state, null, 2)}\n`, {
102+ yield* fs.writeFileString(tempPath, encodeLocalExecutorStateSnapshot(state), {
147103 mode: 0o600,
148104 }).pipe(
149105 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
1919 ExecutorSdkPluginExtensions,
2020 } from "@executor/platform-sdk/plugins";
2121 import type {
22− LocalExecutorConfig,
22+ ExecutorScopeConfig,
2323 LocalInstallation,
2424 } from "@executor/platform-sdk/schema";
2525 import {
@@ -30,14 +30,18 @@ import { · 4 unmodified lines
3030 } from "@executor/platform-sdk/runtime";
3131 import * as Effect from "effect/Effect";
3232 import {
33− type LoadedLocalExecutorConfig,
33+ type LoadedExecutorScopeConfig,
3434 type ResolvedLocalWorkspaceContext,
35− loadLocalExecutorConfig,
35+ loadExecutorScopeConfig,
3636 resolveConfigRelativePath,
3737 resolveLocalWorkspaceContext,
38− writeProjectLocalExecutorConfig,
38+ writeProjectExecutorScopeConfig,
3939 } from "./config";
40+import {
41+ runStartupConfigMigrations,
42+} from "./config-migrations";
4043 import { createLocalExecutorStatePersistence } from "./executor-state-store";
44+import { runStartupExecutorStateMigrations } from "./executor-state-migrations";
4145 import {
4246 createDefaultSecretMaterialDeleter,
4347 createLocalInstanceConfigResolver,
@@ -114,18 +118,18 @@ const createBoundWorkspaceConfigStore = ( · 70 unmodified lines
114118 context: ResolvedLocalWorkspaceContext,
115119 fileSystem: FileSystem.FileSystem,
116120 ): {
117− load: () => Effect.Effect<LoadedLocalExecutorConfig, Error, never>;
118− writeProject: (config: LocalExecutorConfig) => Effect.Effect<void, Error, never>;
121+ load: () => Effect.Effect<LoadedExecutorScopeConfig, Error, never>;
122+ writeProject: (config: ExecutorScopeConfig) => Effect.Effect<void, Error, never>;
119123 resolveRelativePath: typeof resolveConfigRelativePath;
120124 } => ({
121125 load: () =>
122− bindFileSystem(fileSystem, loadLocalExecutorConfig(context)).pipe(
126+ bindFileSystem(fileSystem, loadExecutorScopeConfig(context)).pipe(
123127 Effect.mapError(toError),
124128 ),
125129 writeProject: (config) =>
126130 bindFileSystem(
127131 fileSystem,
128− writeProjectLocalExecutorConfig({ context, config }),
132+ writeProjectExecutorScopeConfig({ context, config }),
129133 ).pipe(Effect.mapError(toError)),
130134 resolveRelativePath: resolveConfigRelativePath,
131135 });
@@ -237,6 +241,14 @@ export const createLocalExecutorRepositoriesEffect = ( · 105 unmodified lines
237241 homeStateDirectory: options.homeStateDirectory,
238242 }),
239243 ).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));
240252
241253 const executorStateStorage = createLocalExecutorStatePersistence(
242254 workspaceContext,
@@ -384,11 +396,19 @@ export { · 141 unmodified lines
384396 writeLocalExecutorStateSnapshot,
385397 } from "./executor-state-store";
386398 export {
387− loadLocalExecutorConfig,
399+ migrateLegacyLocalExecutorState,
400+ runStartupExecutorStateMigrations,
401+} from "./executor-state-migrations";
402+export {
403+ loadExecutorScopeConfig,
388404 resolveConfigRelativePath,
389405 resolveLocalWorkspaceContext,
390− writeProjectLocalExecutorConfig,
406+ writeProjectExecutorScopeConfig,
391407 } from "./config";
408+export {
409+ migrateLegacyExecutorScopeConfigs,
410+ runStartupConfigMigrations,
411+} from "./config-migrations";
392412 export {
393413 loadLocalWorkspaceState,
394414 writeLocalWorkspaceState,
@@ -406,6 +426,6 @@ export { · 11 unmodified lines
406426 syncWorkspaceSourceTypeDeclarationsNode,
407427 } from "./source-type-declarations";
408428 export type {
409− LoadedLocalExecutorConfig,
429+ LoadedExecutorScopeConfig,
410430 ResolvedLocalWorkspaceContext,
411431 } from "./config";
packages/platform/sdk/src/plugins.ts+15−15
@@ -8,8 +8,8 @@ import type { · 7 unmodified lines
88 import type { ExecutorEffect } from "./executor-effect";
99 import type { ExecutorScopeContext } from "./scope";
1010 import {
11− type LocalConfigSource,
12− type LocalExecutorConfig,
11+ type ExecutorScopeConfigSource,
12+ type ExecutorScopeConfig,
1313 type Source as ExecutorSource,
1414 type SecretMaterial,
1515 type SecretMaterialPurpose,
@@ -300,15 +300,15 @@ export type ExecutorSourcePluginDefinition< · 284 unmodified lines
300300 stored: TStored | null;
301301 }) => Effect.Effect<void, Error, any>;
302302 };
303− localConfig?: {
303+ scopeConfig?: {
304304 toConfigSource: (input: {
305305 source: ExecutorSource;
306306 stored: TStored;
307− }) => LocalConfigSource;
307+ }) => ExecutorScopeConfigSource;
308308 recoverStored: (input: {
309309 source: ExecutorSource;
310− config: LocalConfigSource;
311− loadedConfig: LocalExecutorConfig | null;
310+ config: ExecutorScopeConfigSource;
311+ loadedConfig: ExecutorScopeConfig | null;
312312 }) => Effect.Effect<TStored, Error, any> | TStored;
313313 };
314314 catalog: {
@@ -588,7 +588,7 @@ const fromMaybeEffect = <A>( · 273 unmodified lines
588588 ? value
589589 : Effect.succeed(value);
590590
591−const persistSourceLocalConfig = <
591+const persistSourceScopeConfig = <
592592 TAddInput,
593593 TConnectInput,
594594 TSourceConfig,
@@ -610,15 +610,15 @@ const persistSourceLocalConfig = < · 15 unmodified lines
610610 stored: TStored;
611611 },
612612 ): Effect.Effect<void, Error, ScopeConfigStore> =>
613− definition.localConfig
613+ definition.scopeConfig
614614 ? Effect.gen(function* () {
615− const localConfig = definition.localConfig!;
615+ const scopeConfig = definition.scopeConfig!;
616616 const scopeConfigStore = yield* ScopeConfigStore;
617617 const loadedConfig = yield* scopeConfigStore.load();
618618 const projectConfig = cloneJson(loadedConfig.projectConfig ?? {});
619619 const sources = {
620620 ...projectConfig.sources,
621− [input.source.id]: localConfig.toConfigSource({
621+ [input.source.id]: scopeConfig.toConfigSource({
622622 source: input.source,
623623 stored: input.stored,
624624 }),
@@ -653,12 +653,12 @@ const loadStoredOrRecover = < · 28 unmodified lines
653653 source: ExecutorSource,
654654 ): Effect.Effect<TStored | null, Error, ScopeConfigStore> =>
655655 Effect.gen(function* () {
656− const localConfig = definition.localConfig;
656+ const scopeConfig = definition.scopeConfig;
657657 const stored = yield* definition.storage.get({
658658 scopeId: source.scopeId,
659659 sourceId: source.id,
660660 });
661− if (stored !== null || localConfig === undefined) {
661+ if (stored !== null || scopeConfig === undefined) {
662662 return stored;
663663 }
664664
@@ -677,7 +677,7 @@ const loadStoredOrRecover = < · 12 unmodified lines
677677 }
678678
679679 const recovered = yield* fromMaybeEffect(
680− localConfig.recoverStored({
680+ scopeConfig.recoverStored({
681681 source,
682682 config,
683683 loadedConfig: loadedConfig.config,
@@ -748,7 +748,7 @@ const createExecutorSourcePluginApi = < · 64 unmodified lines
748748 sourceId: source.id,
749749 value: created.stored,
750750 });
751− yield* persistSourceLocalConfig(definition, {
751+ yield* persistSourceScopeConfig(definition, {
752752 source,
753753 stored: created.stored,
754754 });
@@ -772,7 +772,7 @@ const createExecutorSourcePluginApi = < · 17 unmodified lines
772772 sourceId: saved.id,
773773 value: updated.stored,
774774 });
775− yield* persistSourceLocalConfig(definition, {
775+ yield* persistSourceScopeConfig(definition, {
776776 source: saved,
777777 stored: updated.stored,
778778 });
packages/platform/sdk/src/policies/operations.ts+3−3
@@ -8,7 +8,7 @@ import type { · 7 unmodified lines
88 } from "./contracts";
99 import {
1010 PolicyIdSchema,
11− type LocalExecutorConfig,
11+ type ExecutorScopeConfig,
1212 type LocalScopePolicy,
1313 type PolicyId,
1414 type ScopeId,
@@ -64,7 +64,7 @@ const toLocalScopePolicy = (input: { · 49 unmodified lines
6464 scopeId: ScopeId;
6565 scopeStableKey: string;
6666 key: string;
67− policyConfig: NonNullable<LocalExecutorConfig["policies"]>[string];
67+ policyConfig: NonNullable<ExecutorScopeConfig["policies"]>[string];
6868 state: LocalScopeState["policies"][string] | undefined;
6969 }): LocalScopePolicy => ({
7070 id:
@@ -119,7 +119,7 @@ const writeLocalPolicyFiles = (input: { · 48 unmodified lines
119119 operation: OperationErrors;
120120 scopeConfigStore: ScopeConfigStoreShape;
121121 scopeStateStore: ScopeStateStoreShape;
122− projectConfig: LocalExecutorConfig;
122+ projectConfig: ExecutorScopeConfig;
123123 scopeState: LocalScopeState;
124124 }) =>
125125 Effect.all(
packages/platform/sdk/src/runtime/execution/runtime.ts+6−6
@@ -11,18 +11,18 @@ import { · 10 unmodified lines
1111 makeSesExecutor,
1212 } from "@executor/runtime-ses";
1313 import type {
14− LocalExecutorConfig,
15− LocalExecutorRuntime,
14+ ExecutorScopeConfig,
15+ ExecutorRuntimeConfig,
1616 } from "#schema";
1717
18−const DEFAULT_EXECUTION_RUNTIME: LocalExecutorRuntime = "quickjs";
18+const DEFAULT_EXECUTION_RUNTIME: ExecutorRuntimeConfig = "quickjs";
1919
2020 export const resolveConfiguredExecutionRuntime = (
21− config: LocalExecutorConfig | null | undefined,
22−): LocalExecutorRuntime => config?.runtime ?? DEFAULT_EXECUTION_RUNTIME;
21+ config: ExecutorScopeConfig | null | undefined,
22+): ExecutorRuntimeConfig => config?.runtime ?? DEFAULT_EXECUTION_RUNTIME;
2323
2424 export const createCodeExecutorForRuntime = (
25− runtime: LocalExecutorRuntime,
25+ runtime: ExecutorRuntimeConfig,
2626 ): CodeExecutor => {
2727 switch (runtime) {
2828 case "deno":
packages/platform/sdk/src/runtime/index.ts+5−5
@@ -32,10 +32,10 @@ import { · 31 unmodified lines
3232 RuntimeExecutionResolverLive,
3333 } from "./execution/scope/environment";
3434 import type {
35− LoadedLocalExecutorConfig,
35+ LoadedExecutorScopeConfig,
3636 } from "./scope-config";
3737 import type {
38− LocalExecutorConfig,
38+ ExecutorScopeConfig,
3939 } from "#schema";
4040 import type {
4141 InstanceConfig,
@@ -113,7 +113,7 @@ export * from "./source-artifacts"; · 71 unmodified lines
113113 export * from "./scope-config";
114114 export * from "./scope-state";
115115 export * from "./sources/source-store/config";
116−export * from "./sources/source-store/plugin-local-config";
116+export * from "./sources/source-store/plugin-scope-config";
117117 export {
118118 LocalInstanceConfigService,
119119 SecretMaterialDeleterService,
@@ -184,9 +184,9 @@ export type BoundInstallationStore = { · 64 unmodified lines
184184 };
185185
186186 export type BoundScopeConfigStore = {
187− load: () => Effect.Effect<LoadedLocalExecutorConfig, Error, never>;
187+ load: () => Effect.Effect<LoadedExecutorScopeConfig, Error, never>;
188188 writeProject: (
189− config: LocalExecutorConfig,
189+ config: ExecutorScopeConfig,
190190 ) => Effect.Effect<void, Error, never>;
191191 resolveRelativePath: ScopeConfigStoreShape["resolveRelativePath"];
192192 };
packages/platform/sdk/src/runtime/scope-config.ts+5−5
@@ -1,11 +1,11 @@
11 import type {
2− LocalExecutorConfig,
2+ ExecutorScopeConfig,
33 } from "#schema";
44
55 export type LoadedExecutorConfig = {
6− config: LocalExecutorConfig | null;
7− homeConfig: LocalExecutorConfig | null;
8− projectConfig: LocalExecutorConfig | null;
6+ config: ExecutorScopeConfig | null;
7+ homeConfig: ExecutorScopeConfig | null;
8+ projectConfig: ExecutorScopeConfig | null;
99 };
1010
11−export type LoadedLocalExecutorConfig = LoadedExecutorConfig;
11+export type LoadedExecutorScopeConfig = LoadedExecutorConfig;
packages/platform/sdk/src/runtime/scope/runtime-context.ts+2−2
@@ -10,7 +10,7 @@ import type { · 9 unmodified lines
1010 ExecutorScopeContext,
1111 } from "../../scope";
1212 import type {
13− LoadedLocalExecutorConfig,
13+ LoadedExecutorScopeConfig,
1414 } from "../scope-config";
1515 import {
1616 RuntimeLocalScopeMismatchError,
@@ -24,7 +24,7 @@ export type RuntimeLocalScopeState = { · 7 unmodified lines
2424 actorScopeId: ScopeId;
2525 resolutionScopeIds: ReadonlyArray<ScopeId>;
2626 };
27− loadedConfig: LoadedLocalExecutorConfig;
27+ loadedConfig: LoadedExecutorScopeConfig;
2828 };
2929
3030 export class RuntimeLocalScopeService extends Context.Tag(
packages/platform/sdk/src/runtime/scope/scope-sync.ts+4−4
@@ -1,7 +1,7 @@
11 import * as Effect from "effect/Effect";
22
33 import {
4− type LoadedLocalExecutorConfig,
4+ type LoadedExecutorScopeConfig,
55 } from "../scope-config";
66 import {
77 ScopeStateStore,
@@ -40,7 +40,7 @@ export const derivePolicyConfigKey = ( · 32 unmodified lines
4040 };
4141
4242 const pruneLocalWorkspaceState = (input: {
43− loadedConfig: LoadedLocalExecutorConfig;
43+ loadedConfig: LoadedExecutorScopeConfig;
4444 }): Effect.Effect<LocalScopeState, Error, ScopeStateStore> =>
4545 Effect.gen(function* () {
4646 const scopeStateStore = yield* ScopeStateStore;
@@ -79,8 +79,8 @@ const pruneLocalWorkspaceState = (input: { · 32 unmodified lines
7979 });
8080
8181 export const synchronizeLocalScopeState = (input: {
82− loadedConfig: LoadedLocalExecutorConfig;
83−}): Effect.Effect<LoadedLocalExecutorConfig["config"], Error, ScopeStateStore> =>
82+ loadedConfig: LoadedExecutorScopeConfig;
83+}): Effect.Effect<LoadedExecutorScopeConfig["config"], Error, ScopeStateStore> =>
8484 Effect.gen(function* () {
8585 yield* pruneLocalWorkspaceState({
8686 loadedConfig: input.loadedConfig,
packages/platform/sdk/src/runtime/scope/storage.ts+4−4
@@ -1,12 +1,12 @@
11 import type {
22 LocalInstallation,
3− LocalExecutorConfig,
3+ ExecutorScopeConfig,
44 } from "#schema";
55 import * as Context from "effect/Context";
66 import * as Layer from "effect/Layer";
77
88 import type {
9− LoadedLocalExecutorConfig,
9+ LoadedExecutorScopeConfig,
1010 } from "../scope-config";
1111 import type {
1212 LocalSourceArtifact,
@@ -31,9 +31,9 @@ export class InstallationStore extends Context.Tag( · 18 unmodified lines
3131 )<InstallationStore, InstallationStoreShape>() {}
3232
3333 export type ScopeConfigStoreShape = {
34− load: () => import("effect/Effect").Effect<LoadedLocalExecutorConfig, Error, never>;
34+ load: () => import("effect/Effect").Effect<LoadedExecutorScopeConfig, Error, never>;
3535 writeProject: (input: {
36− config: LocalExecutorConfig;
36+ config: ExecutorScopeConfig;
3737 }) => import("effect/Effect").Effect<void, Error, never>;
3838 resolveRelativePath: (input: { path: string; scopeRoot: string }) => string;
3939 };
packages/platform/sdk/src/runtime/sources/source-store/config.ts+10−20
@@ -1,5 +1,5 @@
11 import type {
2− LocalConfigSource,
2+ ExecutorScopeConfigSource,
33 Source,
44 SourceId,
55 } from "#schema";
@@ -23,7 +23,7 @@ export const trimOrNull = (value: string | null | undefined): string | null => { · 17 unmodified lines
2323 export const cloneJson = <T>(value: T): T =>
2424 JSON.parse(JSON.stringify(value)) as T;
2525
26−export const deriveLocalSourceId = (
26+export const deriveScopeConfigSourceId = (
2727 source: Pick<Source, "namespace" | "name">,
2828 used: ReadonlySet<string>,
2929 ): SourceId => {
@@ -38,9 +38,9 @@ export const deriveLocalSourceId = ( · 8 unmodified lines
3838 return SourceIdSchema.make(candidate);
3939 };
4040
41−export const configSourceBaseFromLocalSource = (input: {
41+export const scopeConfigSourceBaseFromSource = (input: {
4242 source: Source;
43−}): Omit<LocalConfigSource, "kind" | "config" | "connection" | "binding"> => ({
43+}): Omit<ExecutorScopeConfigSource, "kind" | "config"> => ({
4444 ...(trimOrNull(input.source.name) !== trimOrNull(input.source.id)
4545 ? { name: input.source.name }
4646 : {}),
@@ -50,29 +50,19 @@ export const configSourceBaseFromLocalSource = (input: { · 3 unmodified lines
5050 ...(input.source.enabled === false ? { enabled: false } : {}),
5151 });
5252
53−export const configSourceFromLocalSource = (input: {
53+export const scopeConfigSourceFromSource = (input: {
5454 source: Source;
55− existingConfig?: LocalConfigSource | null;
56−}): LocalConfigSource => {
55+ existingConfig?: ExecutorScopeConfigSource | null;
56+}): ExecutorScopeConfigSource => {
5757 return {
58− ...configSourceBaseFromLocalSource({
58+ ...scopeConfigSourceBaseFromSource({
5959 source: input.source,
6060 }),
61− kind: input.source.kind as LocalConfigSource["kind"],
61+ kind: input.source.kind as ExecutorScopeConfigSource["kind"],
6262 ...(input.existingConfig?.config !== undefined
6363 ? {
6464 config: cloneJson(input.existingConfig.config),
6565 }
6666 : {}),
67− ...(input.existingConfig?.connection !== undefined
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;
67+ } as ExecutorScopeConfigSource;
7868 };
packages/platform/sdk/src/runtime/sources/source-store/deps.ts+2−2
@@ -4,7 +4,7 @@ import type { · 3 unmodified lines
44 import * as Effect from "effect/Effect";
55
66 import type {
7− LoadedLocalExecutorConfig,
7+ LoadedExecutorScopeConfig,
88 } from "../../scope-config";
99 import {
1010 SourceTypeDeclarationsRefresherService,
@@ -53,7 +53,7 @@ export type ResolvedSourceStoreScope = { · 42 unmodified lines
5353 scopeConfigStore: ScopeConfigStoreShape;
5454 scopeStateStore: ScopeStateStoreShape;
5555 sourceArtifactStore: SourceArtifactStoreShape;
56− loadedConfig: LoadedLocalExecutorConfig;
56+ loadedConfig: LoadedExecutorScopeConfig;
5757 scopeState: LocalScopeState;
5858 };
5959
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
66
77 import type { LocalScopeState } from "../../scope-state";
88 import {
9− configSourceFromLocalSource,
9+ scopeConfigSourceFromSource,
1010 cloneJson,
11− deriveLocalSourceId,
11+ deriveScopeConfigSourceId,
1212 } from "./config";
1313 import {
1414 type RuntimeSourceStoreDeps,
@@ -84,7 +84,7 @@ export const persistSourceWithDeps = ( · 69 unmodified lines
8484 localScope.loadedConfig.config?.sources?.[source.id] ||
8585 localScope.scopeState.sources[source.id]
8686 ? source.id
87− : deriveLocalSourceId(
87+ : deriveScopeConfigSourceId(
8888 source,
8989 new Set(Object.keys(localScope.loadedConfig.config?.sources ?? {})),
9090 ),
@@ -94,7 +94,7 @@ export const persistSourceWithDeps = ( · 3 unmodified lines
9494 const sources = {
9595 ...projectConfig.sources,
9696 };
97− sources[nextSource.id] = configSourceFromLocalSource({
97+ sources[nextSource.id] = scopeConfigSourceFromSource({
9898 source: nextSource,
9999 existingConfig: localScope.loadedConfig.config?.sources?.[nextSource.id] ?? null,
100100 });
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 @@
11 import type {
2− LocalConfigSource,
2+ ExecutorScopeConfigSource,
33 Source,
44 } from "#schema";
55 import * as Schema from "effect/Schema";
66 import {
77 cloneJson,
8− configSourceBaseFromLocalSource,
8+ scopeConfigSourceBaseFromSource,
99 } from "./config";
1010
11−export const createPluginLocalConfigEntrySchema = <
11+export const createPluginScopeConfigEntrySchema = <
1212 TKind extends string,
1313 TConfig,
1414 >(
@@ -23,17 +23,15 @@ export const createPluginLocalConfigEntrySchema = < · 8 unmodified lines
2323 namespace: Schema.optional(Schema.String),
2424 enabled: Schema.optional(Schema.Boolean),
2525 config: input.config,
26− connection: Schema.optional(Schema.Unknown),
27− binding: Schema.optional(Schema.Unknown),
2826 });
2927
30−export const pluginLocalConfigSourceFromConfig = <TConfig>(input: {
28+export const pluginScopeConfigSourceFromConfig = <TConfig>(input: {
3129 source: Source;
3230 config: TConfig;
33−}): LocalConfigSource => ({
34− ...configSourceBaseFromLocalSource({
31+}): ExecutorScopeConfigSource => ({
32+ ...scopeConfigSourceBaseFromSource({
3533 source: input.source,
3634 }),
37− kind: input.source.kind as LocalConfigSource["kind"],
35+ kind: input.source.kind as ExecutorScopeConfigSource["kind"],
3836 config: cloneJson(input.config),
3937 });
packages/platform/sdk/src/runtime/sources/source-store/records.ts+2−2
@@ -6,7 +6,7 @@ import type { · 5 unmodified lines
66 import { SourceIdSchema } from "#schema";
77 import * as Effect from "effect/Effect";
88
9−import type { LoadedLocalExecutorConfig } from "../../scope-config";
9+import type { LoadedExecutorScopeConfig } from "../../scope-config";
1010 import {
1111 LocalConfiguredSourceNotFoundError,
1212 RuntimeLocalScopeMismatchError,
@@ -21,7 +21,7 @@ import { trimOrNull } from "./config"; · 8 unmodified lines
2121
2222 export const buildLocalSourceRecord = (input: {
2323 scopeId: ScopeId;
24− loadedConfig: LoadedLocalExecutorConfig;
24+ loadedConfig: LoadedExecutorScopeConfig;
2525 scopeState: LocalScopeState;
2626 sourceId: SourceId;
2727 }): Effect.Effect<
packages/platform/sdk/src/schema/index.ts+1−1
@@ -9,7 +9,7 @@ export * from "./models/secret-material"; · 8 unmodified lines
99 export * from "./models/secret-store";
1010 export * from "./models/policy";
1111 export * from "./models/local-installation";
12−export * from "./models/local-config";
12+export * from "./models/scope-config";
1313 export * from "./models/execution";
1414 export * from "./models/source-inspection";
1515 export * from "./models/code-migration";
packages/platform/sdk/src/schema/models/local-config.ts+0−68
@@ -1,68 +0,0 @@
1−import {
2− Schema,
3−} from "effect";
4−
5−export const LocalExecutorRuntimeSchema = Schema.Literal(
6− "quickjs",
7− "ses",
8− "deno",
9−);
10−
11−export const LocalConfigSourceConnectionSchema = Schema.Struct({
12− endpoint: Schema.optional(Schema.NullOr(Schema.String)),
13− auth: Schema.optional(Schema.Unknown),
14−});
15−
16−const 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−
25−export const LocalConfigSourceSchema = Schema.extend(
26− LocalConfigSourceEntryBaseSchema,
27− Schema.Struct({
28− kind: Schema.String,
29− }),
30−);
31−
32−export type LocalConfigSource = typeof LocalConfigSourceSchema.Type;
33−
34−export const LocalConfigPolicyActionSchema = Schema.Literal("allow", "deny");
35−export const LocalConfigPolicyApprovalSchema = Schema.Literal("auto", "manual");
36−
37−export 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−
45−export const LocalConfigWorkspaceSchema = Schema.Struct({
46− name: Schema.optional(Schema.String),
47−});
48−
49−export 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−
66−export type LocalConfigPolicy = typeof LocalConfigPolicySchema.Type;
67−export type LocalExecutorRuntime = typeof LocalExecutorRuntimeSchema.Type;
68−export type LocalExecutorConfig = typeof LocalExecutorConfigSchema.Type;
packages/platform/sdk/src/schema/models/scope-config.ts+65−0
@@ -0,0 +1,65 @@
1+import {
2+ Schema,
3+} from "effect";
4+
5+export const ExecutorRuntimeConfigSchema = Schema.Literal(
6+ "quickjs",
7+ "ses",
8+ "deno",
9+);
10+
11+const 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+
18+export const ExecutorScopeConfigSourceSchema = Schema.extend(
19+ ExecutorScopeConfigSourceBaseSchema,
20+ Schema.Struct({
21+ kind: Schema.String,
22+ }),
23+);
24+
25+export type ExecutorScopeConfigSource = typeof ExecutorScopeConfigSourceSchema.Type;
26+
27+export const ExecutorScopeConfigPolicyActionSchema = Schema.Literal("allow", "deny");
28+export const ExecutorScopeConfigPolicyApprovalSchema = Schema.Literal(
29+ "auto",
30+ "manual",
31+);
32+
33+export 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+
41+export const ExecutorScopeWorkspaceConfigSchema = Schema.Struct({
42+ name: Schema.optional(Schema.String),
43+});
44+
45+export 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+
62+export type ExecutorScopeConfigPolicy =
63+ typeof ExecutorScopeConfigPolicySchema.Type;
64+export type ExecutorRuntimeConfig = typeof ExecutorRuntimeConfigSchema.Type;
65+export type ExecutorScopeConfig = typeof ExecutorScopeConfigSchema.Type;
packages/platform/server/src/index.test.ts+5−5
@@ -34,10 +34,10 @@ import { · 33 unmodified lines
3434 getOrProvisionLocalInstallation,
3535 resolveLocalWorkspaceContext,
3636 writeLocalSourceArtifact,
37− writeProjectLocalExecutorConfig,
37+ writeProjectExecutorScopeConfig,
3838 } from "@executor/platform-sdk-file/effect";
3939 import {
40− type LocalConfigSource,
40+ type ExecutorScopeConfigSource,
4141 SourceCatalogIdSchema,
4242 SourceCatalogRevisionIdSchema,
4343 SourceIdSchema,
@@ -136,7 +136,7 @@ const writeConfiguredLocalMcpSource = (input: { · 92 unmodified lines
136136 });
137137 const installation = yield* getOrProvisionLocalInstallation({ context });
138138
139− yield* writeProjectLocalExecutorConfig({
139+ yield* writeProjectExecutorScopeConfig({
140140 context,
141141 config: {
142142 sources: {
@@ -274,7 +274,7 @@ const writeConfiguredLocalMcpSource = (input: { · 131 unmodified lines
274274
275275 const writeConfiguredWorkspaceSources = (input: {
276276 workspaceRoot: string;
277− sources: Record<string, LocalConfigSource>;
277+ sources: Record<string, ExecutorScopeConfigSource>;
278278 }) =>
279279 Effect.gen(function* () {
280280 const context = yield* resolveLocalWorkspaceContext({
@@ -282,7 +282,7 @@ const writeConfiguredWorkspaceSources = (input: { · 1 unmodified line
282282 });
283283 const installation = yield* getOrProvisionLocalInstallation({ context });
284284
285− yield* writeProjectLocalExecutorConfig({
285+ yield* writeProjectExecutorScopeConfig({
286286 context,
287287 config: {
288288 sources: input.sources,
plugins/google-discovery/sdk/index.ts+7−7
@@ -7,7 +7,7 @@ import { · 6 unmodified lines
77 createSourceCatalogSyncResult,
88 } from "@executor/source-core";
99 import type {
10− LocalExecutorConfig,
10+ ExecutorScopeConfig,
1111 Source,
1212 } from "@executor/platform-sdk/schema";
1313 import {
@@ -17,8 +17,8 @@ import { · 3 unmodified lines
1717 defineExecutorSourcePlugin,
1818 } from "@executor/platform-sdk/plugins";
1919 import {
20− createPluginLocalConfigEntrySchema,
21− pluginLocalConfigSourceFromConfig,
20+ createPluginScopeConfigEntrySchema,
21+ pluginScopeConfigSourceFromConfig,
2222 SecretMaterialDeleterService,
2323 SecretMaterialResolverService,
2424 SecretMaterialStorerService,
@@ -97,7 +97,7 @@ const decodeBatchSourceInput = Schema.decodeUnknownSync( · 72 unmodified lines
9797 const decodeSession = Schema.decodeUnknownSync(GoogleDiscoveryOAuthSessionSchema);
9898 const decodeProviderData = Schema.decodeUnknownSync(GoogleDiscoveryToolProviderDataSchema);
9999 const decodeGoogleDiscoveryCurrentLocalConfigOption = Schema.decodeUnknownOption(
100− createPluginLocalConfigEntrySchema({
100+ createPluginScopeConfigEntrySchema({
101101 kind: GOOGLE_DISCOVERY_SOURCE_KIND,
102102 config: GoogleDiscoveryStoredSourceDataSchema,
103103 }),
@@ -563,7 +563,7 @@ const googleDiscoveryConnectInputFromAddInput = ( · 459 unmodified lines
563563
564564 const googleDiscoveryStoredSourceDataFromLocalConfig = (input: {
565565 config: unknown;
566− loadedConfig: LocalExecutorConfig | null;
566+ loadedConfig: ExecutorScopeConfig | null;
567567 }): GoogleDiscoveryStoredSourceData => {
568568 const current = decodeGoogleDiscoveryCurrentLocalConfigOption(input.config);
569569 if (Option.isSome(current)) {
@@ -629,9 +629,9 @@ export const googleDiscoverySdkPlugin = (options: { · 59 unmodified lines
629629 }
630630 }),
631631 },
632− localConfig: {
632+ scopeConfig: {
633633 toConfigSource: ({ source, stored }) =>
634− pluginLocalConfigSourceFromConfig({
634+ pluginScopeConfigSourceFromConfig({
635635 source,
636636 config: stored,
637637 }),
plugins/graphql/sdk/index.ts+7−7
@@ -12,7 +12,7 @@ import { · 11 unmodified lines
1212 createSourceCatalogSyncResult,
1313 } from "@executor/source-core";
1414 import type {
15− LocalExecutorConfig,
15+ ExecutorScopeConfig,
1616 Source,
1717 } from "@executor/platform-sdk/schema";
1818 import {
@@ -23,8 +23,8 @@ import { · 4 unmodified lines
2323 } from "@executor/platform-sdk/plugins";
2424 import {
2525 SecretMaterialResolverService,
26− createPluginLocalConfigEntrySchema,
27− pluginLocalConfigSourceFromConfig,
26+ createPluginScopeConfigEntrySchema,
27+ pluginScopeConfigSourceFromConfig,
2828 provideExecutorRuntime,
2929 runtimeEffectError,
3030 } from "@executor/platform-sdk/runtime";
@@ -151,7 +151,7 @@ const asStringRecord = (value: unknown): Record<string, string> => · 120 unmodified lines
151151 );
152152
153153 const decodeGraphqlCurrentLocalConfigOption = Schema.decodeUnknownOption(
154− createPluginLocalConfigEntrySchema({
154+ createPluginScopeConfigEntrySchema({
155155 kind: "graphql",
156156 config: GraphqlStoredSourceDataSchema,
157157 }),
@@ -336,7 +336,7 @@ const graphqlConnectInputFromAddInput = ( · 178 unmodified lines
336336
337337 const graphqlStoredSourceDataFromLocalConfig = (input: {
338338 config: unknown;
339− loadedConfig: LocalExecutorConfig | null;
339+ loadedConfig: ExecutorScopeConfig | null;
340340 }): GraphqlStoredSourceData => {
341341 const current = decodeGraphqlCurrentLocalConfigOption(input.config);
342342 if (Option.isSome(current)) {
@@ -397,9 +397,9 @@ export const graphqlSdkPlugin = (options: { · 54 unmodified lines
397397 toConfig: ({ source, stored }) =>
398398 sourceConfigFromStored(source, normalizeStoredSourceData(stored)),
399399 },
400− localConfig: {
400+ scopeConfig: {
401401 toConfigSource: ({ source, stored }) =>
402− pluginLocalConfigSourceFromConfig({
402+ pluginScopeConfigSourceFromConfig({
403403 source,
404404 config: normalizeStoredSourceData(stored),
405405 }),
plugins/mcp/sdk/index.ts+5−5
@@ -31,8 +31,8 @@ import { · 30 unmodified lines
3131 defineExecutorSourcePlugin,
3232 } from "@executor/platform-sdk/plugins";
3333 import {
34− createPluginLocalConfigEntrySchema,
35− pluginLocalConfigSourceFromConfig,
34+ createPluginScopeConfigEntrySchema,
35+ pluginScopeConfigSourceFromConfig,
3636 SecretMaterialDeleterService,
3737 SecretMaterialResolverService,
3838 SecretMaterialStorerService,
@@ -250,7 +250,7 @@ const normalizeStringArray = ( · 211 unmodified lines
250250 };
251251
252252 const decodeMcpCurrentLocalConfigOption = Schema.decodeUnknownOption(
253− createPluginLocalConfigEntrySchema({
253+ createPluginScopeConfigEntrySchema({
254254 kind: "mcp",
255255 config: McpStoredSourceDataSchema,
256256 }),
@@ -720,9 +720,9 @@ export const mcpSdkPlugin = ( · 463 unmodified lines
720720 }
721721 }),
722722 },
723− localConfig: {
723+ scopeConfig: {
724724 toConfigSource: ({ source, stored }) =>
725− pluginLocalConfigSourceFromConfig({
725+ pluginScopeConfigSourceFromConfig({
726726 source,
727727 config: stored,
728728 }),
plugins/openapi/sdk/index.ts+7−7
@@ -8,7 +8,7 @@ import { · 7 unmodified lines
88 createSourceCatalogSyncResult,
99 } from "@executor/source-core";
1010 import type {
11− LocalExecutorConfig,
11+ ExecutorScopeConfig,
1212 Source,
1313 } from "@executor/platform-sdk/schema";
1414 import {
@@ -18,9 +18,9 @@ import { · 3 unmodified lines
1818 defineExecutorSourcePlugin,
1919 } from "@executor/platform-sdk/plugins";
2020 import {
21− createPluginLocalConfigEntrySchema,
21+ createPluginScopeConfigEntrySchema,
2222 provideExecutorRuntime,
23− pluginLocalConfigSourceFromConfig,
23+ pluginScopeConfigSourceFromConfig,
2424 runtimeEffectError,
2525 SecretMaterialResolverService,
2626 } from "@executor/platform-sdk/runtime";
@@ -157,7 +157,7 @@ const OpenApiLocalSourceConfigSchema = Schema.Struct({ · 130 unmodified lines
157157 });
158158
159159 const decodeOpenApiCurrentLocalConfigOption = Schema.decodeUnknownOption(
160− createPluginLocalConfigEntrySchema({
160+ createPluginScopeConfigEntrySchema({
161161 kind: "openapi",
162162 config: OpenApiLocalSourceConfigSchema,
163163 }),
@@ -193,7 +193,7 @@ const configFromStoredSourceData = ( · 29 unmodified lines
193193
194194 const openApiStoredSourceDataFromLocalConfig = (input: {
195195 config: unknown;
196− loadedConfig: LocalExecutorConfig | null;
196+ loadedConfig: ExecutorScopeConfig | null;
197197 }): OpenApiStoredSourceData => {
198198 const current = decodeOpenApiCurrentLocalConfigOption(input.config);
199199 if (Option.isSome(current)) {
@@ -539,9 +539,9 @@ export const openApiSdkPlugin = ( · 339 unmodified lines
539539 toConfig: ({ source, stored }) =>
540540 configFromStoredSourceData(source, normalizeStoredSourceData(stored)),
541541 },
542− localConfig: {
542+ scopeConfig: {
543543 toConfigSource: ({ source, stored }) =>
544− pluginLocalConfigSourceFromConfig({
544+ pluginScopeConfigSourceFromConfig({
545545 source,
546546 config: {
547547 specUrl: stored.specUrl,