History

Add GitHub sync and self-host custom tools wiring

Rhys Sullivan committed 4 months ago

Commit 275446e1 parent 41eb3ae19 files · +1665 −40Browse files

Showing the changes to packages/plugins/apps/src/plugin (2 of 19 files). Show all

Unified · Split

2 files
  1. packages/plugins/apps/src/plugin/apps-plugin.ts +136 −37
  2. packages/plugins/apps/src/plugin/runtime.ts +5 −1
packages/plugins/apps/src/plugin/apps-plugin.ts+136−37
@@ -4,10 +4,12 @@ import { · 3 unmodified lines
44 definePlugin,
55 tool,
66 ToolName,
7− ConnectionName,
87 IntegrationSlug,
98 AuthTemplateSlug,
109 connectionIdentifier,
10+ type ConnectionRef,
11+ type Owner,
12+ type PluginCtx,
1113 type ResolveToolsInput,
1214 type ResolveToolsResult,
1315 type InvokeToolInput,
@@ -18,6 +20,7 @@ import type { AppsRuntime } from "./runtime"; · 4 unmodified lines
1820 import { makeAppsStore } from "./store";
1921 import type { ClientResolver, ConnectionCandidate } from "./bindings";
2022 import type { IntegrationDecl, ToolDescriptor } from "../pipeline/descriptor";
23+import { syncGitHubSource, type GitHubSyncResult } from "../source/github-source";
2124
2225 export const APPS_INTEGRATION_SLUG = "apps";
2326 export const APPS_PLUGIN_ID = "apps";
@@ -42,6 +45,40 @@ const isRecord = (value: unknown): value is Record<string, unknown> => · 18 unmodified lines
4245
4346 const unique = (values: readonly string[]): readonly string[] => [...new Set(values)];
4447
48+const syncFailure = (message: string, path?: string): GitHubSyncResult => ({
49+ status: "failed",
50+ tools: [],
51+ skipped: [],
52+ errors: [
53+ {
54+ stage: "source",
55+ message,
56+ ...(path ? { diagnostics: [{ path, message }] } : {}),
57+ },
58+ ],
59+});
60+
61+const sourceScopeFor = (input: { repo: string; connection: string }): string =>
62+ String(connectionIdentifier(`github ${input.connection} ${input.repo}`, "githubSource"));
63+
64+const parseConnectionAddress = (address: string): ConnectionRef | null => {
65+ const parts = address.split(".");
66+ if (parts.length !== 4 || parts[0] !== "tools") return null;
67+ const [, integration, owner, name] = parts;
68+ if (!integration || !name) return null;
69+ if (owner !== "org" && owner !== "user") return null;
70+ return {
71+ owner: owner as Owner,
72+ integration: IntegrationSlug.make(integration),
73+ name: connectionIdentifier(name),
74+ };
75+};
76+
77+const configBaseUrl = (config: unknown): string | undefined =>
78+ isRecord(config) && typeof config.baseUrl === "string" && config.baseUrl.length > 0
79+ ? config.baseUrl
80+ : undefined;
81+
4582 const projectInputSchema = (
4683 schema: unknown,
4784 integrations: Readonly<Record<string, IntegrationDecl>>,
@@ -94,6 +131,49 @@ export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { · 46 unmodified lines
94131 Effect.map((mapped) => mapped ?? scopeFromConnection(connectionName)),
95132 );
96133
134+ const ensureCatalogConnection = (scope: string, ctx: PluginCtx<AppsStoreShape>) =>
135+ Effect.gen(function* () {
136+ const slug = IntegrationSlug.make(APPS_INTEGRATION_SLUG);
137+ const existing = yield* ctx.core.integrations
138+ .get(slug)
139+ .pipe(Effect.orElseSucceed(() => null));
140+ if (!existing) {
141+ yield* ctx.core.integrations.register({
142+ slug,
143+ name: "Apps",
144+ description: "User-authored, published custom tools.",
145+ config: {},
146+ canRemove: false,
147+ canRefresh: true,
148+ });
149+ }
150+ const rawName = connectionNameForScope(scope);
151+ const connName = connectionIdentifier(rawName);
152+ const conns = yield* ctx.connections
153+ .list({ integration: slug })
154+ .pipe(Effect.orElseSucceed(() => []));
155+ if (!conns.some((c) => String(c.name) === String(connName))) {
156+ yield* ctx.connections.create({
157+ owner: "user",
158+ name: connName,
159+ integration: slug,
160+ template: AuthTemplateSlug.make("none"),
161+ value: "",
162+ });
163+ }
164+ yield* runtime.deps.store
165+ .putScopeForConnection(String(rawName), scope)
166+ .pipe(Effect.orElseSucceed(() => undefined));
167+ yield* runtime.deps.store
168+ .putScopeForConnection(String(connName), scope)
169+ .pipe(Effect.orElseSucceed(() => undefined));
170+ return {
171+ scope,
172+ connection: String(connName),
173+ ref: { owner: "user" as const, integration: slug, name: connName },
174+ };
175+ });
176+
97177 return {
98178 id: APPS_PLUGIN_ID as "apps",
99179 packageName: "@executor-js/plugin-apps",
@@ -135,59 +215,78 @@ export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { · 35 unmodified lines
135215 Effect.gen(function* () {
136216 const scope =
137217 (args as { scope?: string } | undefined)?.scope ?? DEFAULT_CATALOG_SCOPE;
138− const slug = IntegrationSlug.make(APPS_INTEGRATION_SLUG);
139− const existing = yield* ctx.core.integrations
140− .get(slug)
141− .pipe(Effect.orElseSucceed(() => null));
142− if (!existing) {
143− yield* ctx.core.integrations.register({
144− slug,
145− name: "Apps",
146− description: "User-authored, published custom tools.",
147− config: {},
148− canRemove: false,
149− canRefresh: true,
150− });
218+ const connected = yield* ensureCatalogConnection(scope, ctx);
219+ return { scope, connection: connected.connection };
220+ }),
221+ }),
222+ tool<AppsStoreShape>({
223+ name: "sync_github_source",
224+ description: "Sync a GitHub repository containing custom tool source files.",
225+ execute: (args, { ctx }) =>
226+ Effect.gen(function* () {
227+ const payload = isRecord(args) ? args : {};
228+ const repo = typeof payload.repo === "string" ? payload.repo : "";
229+ const connection = typeof payload.connection === "string" ? payload.connection : "";
230+ const ref = typeof payload.ref === "string" ? payload.ref : undefined;
231+ const explicitScope = typeof payload.scope === "string" ? payload.scope : undefined;
232+ if (!repo) return syncFailure('sync_github_source requires "repo"');
233+ if (!connection) return syncFailure('sync_github_source requires "connection"');
234+
235+ const sourceConnection = parseConnectionAddress(connection);
236+ if (!sourceConnection) {
237+ return syncFailure(
238+ `GitHub connection must be a connection address like tools.github.user.main; got "${connection}"`,
239+ connection,
240+ );
151241 }
152− const connName = ConnectionName.make(connectionNameForScope(scope));
153− const conns = yield* ctx.connections
154− .list({ integration: slug })
155− .pipe(Effect.orElseSucceed(() => []));
156− if (!conns.some((c) => String(c.name) === String(connName))) {
157− yield* ctx.connections.create({
158− owner: "user",
159− name: connName,
160− integration: slug,
161− template: AuthTemplateSlug.make("none"),
162− value: "",
163− });
242+ if (String(sourceConnection.integration) !== "github") {
243+ return syncFailure(
244+ `GitHub source sync requires a github connection; got "${sourceConnection.integration}"`,
245+ connection,
246+ );
164247 }
165− yield* runtime.deps.store
166− .putScopeForConnection(String(connName), scope)
167− .pipe(Effect.orElseSucceed(() => undefined));
168− const normalized = connectionIdentifier(String(connName));
169− if (normalized !== String(connName)) {
170− yield* runtime.deps.store
171− .putScopeForConnection(normalized, scope)
172− .pipe(Effect.orElseSucceed(() => undefined));
248+ const existing = yield* ctx.connections
249+ .get(sourceConnection)
250+ .pipe(Effect.orElseSucceed(() => null));
251+ if (!existing) return syncFailure(`GitHub connection not found: ${connection}`);
252+ const token = yield* ctx.connections
253+ .resolveValue(sourceConnection)
254+ .pipe(Effect.orElseSucceed(() => null));
255+ const integration = yield* ctx.core.integrations
256+ .get(sourceConnection.integration)
257+ .pipe(Effect.orElseSucceed(() => null));
258+ const scope = explicitScope ?? sourceScopeFor({ repo, connection });
259+ const result = yield* syncGitHubSource({
260+ runtime,
261+ scope,
262+ repo,
263+ ref,
264+ token,
265+ baseUrl: configBaseUrl(integration?.config),
266+ });
267+ if (result.status === "published") {
268+ const catalog = yield* ensureCatalogConnection(scope, ctx);
269+ yield* ctx.connections.refresh(catalog.ref).pipe(Effect.orElseSucceed(() => []));
173270 }
174− return { scope, connection: String(connName) };
271+ return result;
175272 }),
176273 }),
177274 ],
178275 },
179276 ],
180277
181− resolveTools: ({ connection }: ResolveToolsInput<AppsStoreShape>) =>
278+ resolveTools: ({ ctx, connection }: ResolveToolsInput<AppsStoreShape>) =>
182279 Effect.gen(function* () {
183280 const scope = yield* scopeFor(String(connection.name));
184281 const descriptor = yield* runtime.getDescriptor(scope);
185282 if (!descriptor) return { tools: [] } satisfies ResolveToolsResult;
283+ const resolver =
284+ makeResolver && ctx ? makeResolver({ ctx, scope, tool: "*" }) : runtime.deps.resolver;
186285 const tools: ToolDef[] = [];
187286 for (const t of descriptor.tools) {
188287 const byRole: Record<string, readonly ConnectionCandidate[]> = {};
189288 for (const [role, decl] of Object.entries(t.integrations)) {
190− byRole[role] = yield* runtime.deps.resolver
289+ byRole[role] = yield* resolver
191290 .listConnections({ integration: decl.integration })
192291 .pipe(Effect.orElseSucceed(() => []));
193292 }
packages/plugins/apps/src/plugin/runtime.ts+5−1
@@ -13,7 +13,7 @@ import { · 12 unmodified lines
1313 type PublishOutput,
1414 } from "../pipeline/publish";
1515 import { PublishError } from "../pipeline/discover";
16−import type { AppDescriptor, ToolDescriptor } from "../pipeline/descriptor";
16+import type { AppDescriptor, AppSourceRef, ToolDescriptor } from "../pipeline/descriptor";
1717 import { bundleEntry } from "../pipeline/bundle";
1818 import {
1919 buildBridge,
@@ -42,6 +42,8 @@ export interface AppsRuntime { · 22 unmodified lines
4242 readonly scope: string;
4343 readonly files: ReadonlyMap<string, string>;
4444 readonly message?: string;
45+ readonly description?: string;
46+ readonly source?: AppSourceRef;
4547 }) => Effect.Effect<PublishOutput, PublishError>;
4648 readonly getDescriptor: (scope: string) => Effect.Effect<AppDescriptor | null>;
4749 /** Re-derive the published-descriptor pointer from the latest committed
@@ -264,6 +266,8 @@ export const makeAppsRuntime = (deps: AppsRuntimeDeps): AppsRuntime => { · 216 unmodified lines
264266 scope: input.scope,
265267 files: input.files,
266268 commitMessage: input.message,
269+ description: input.description,
270+ source: input.source,
267271 },
268272 );
269273 yield* putDescriptorPointer(out.descriptor);