History

Fix apps inner failures and descriptor blobs

Rhys Sullivan committed 4 months ago

Commit f5d9c971 parent 1aa1ab410 files · +296 −70Browse files

Unified · Split

10 files
  1. packages/plugins/apps/src/executor/app-tool-executor.ts +13 −2
  2. packages/plugins/apps/src/pipeline/descriptor.ts +0 −1
  3. packages/plugins/apps/src/pipeline/publish.test.ts +5 −4
  4. packages/plugins/apps/src/pipeline/publish.ts +20 −32
  5. packages/plugins/apps/src/plugin/apps-plugin.test.ts +132 −1
  6. packages/plugins/apps/src/plugin/apps-plugin.ts +61 −10
  7. packages/plugins/apps/src/plugin/bindings.ts +10 −1
  8. packages/plugins/apps/src/plugin/resolver.ts +44 −13
  9. packages/plugins/apps/src/plugin/store.test.ts +7 −3
  10. packages/plugins/apps/src/plugin/store.ts +4 −3
packages/plugins/apps/src/executor/app-tool-executor.ts+13−2
@@ -1,6 +1,6 @@
11 /* oxlint-disable executor/no-try-catch-or-throw, executor/no-double-cast, executor/no-promise-reject, executor/no-instanceof-tagged-error -- boundary: in-process dynamic-import backing converts JavaScript exceptions into typed AppExecutorError */
22 import { Buffer } from "node:buffer";
3−import { Data, Effect } from "effect";
3+import { Data, Effect, Predicate } from "effect";
44 import { z } from "zod";
55
66 import { validToolKey } from "../pipeline/discover";
@@ -126,6 +126,13 @@ const jsonSchemaFor = (schema: unknown): unknown => { · 119 unmodified lines
126126 return schema;
127127 };
128128
129+const innerToolFailureMessage = (cause: unknown): string | null => {
130+ if (!Predicate.isTagged("AppInnerToolError")(cause)) return null;
131+ const error = cause as { readonly address?: unknown; readonly innerMessage?: unknown };
132+ if (typeof error.address !== "string" || typeof error.innerMessage !== "string") return null;
133+ return `Inner tool ${error.address} failed: "${error.innerMessage}"`;
134+};
135+
129136 type CollectedIntegrationDecl = CollectedTool["integrations"][string];
130137
131138 const integrationDeclaration = (value: unknown): CollectedIntegrationDecl | null => {
@@ -452,6 +459,10 @@ export const makeInProcessAppToolExecutor = (): AppToolExecutor => ({ · 320 unmodified lines
452459 catch: (cause) =>
453460 cause instanceof AppExecutorError
454461 ? cause
455− : new AppExecutorError({ kind: "invoke", message: "app tool invocation failed", cause }),
462+ : new AppExecutorError({
463+ kind: "invoke",
464+ message: innerToolFailureMessage(cause) ?? "app tool invocation failed",
465+ cause,
466+ }),
456467 }),
457468 });
packages/plugins/apps/src/pipeline/descriptor.ts+0−1
@@ -48,7 +48,6 @@ export interface AppDescriptor { · 47 unmodified lines
4848 readonly version: typeof DESCRIPTOR_VERSION;
4949 readonly app: string;
5050 readonly sourceRef: string;
51− readonly descriptorKey: string;
5251 readonly publishedAt: number;
5352 readonly toolchain: ToolchainRef;
5453 readonly tools: readonly ToolDescriptor[];
packages/plugins/apps/src/pipeline/publish.test.ts+5−4
@@ -28,6 +28,7 @@ const makeMemoryStore = (): AppsStore & { · 27 unmodified lines
2828 const blobs = new Map<string, string>();
2929 const rows = new Map<string, unknown>();
3030 let descriptor: AppDescriptor | null = null;
31+ let descriptorKey: string | null = null;
3132 return {
3233 rows,
3334 blobs,
@@ -40,11 +41,9 @@ const makeMemoryStore = (): AppsStore & { · 6 unmodified lines
4041 getBlob: (key) => Effect.sync(() => blobs.get(key) ?? null),
4142 getDescriptorRecord: () =>
4243 Effect.sync(() =>
43− descriptor
44− ? { sourceRef: descriptor.sourceRef, descriptorKey: descriptor.descriptorKey }
45− : null,
44+ descriptor ? { sourceRef: descriptor.sourceRef, descriptorKey: descriptorKey ?? "" } : null,
4645 ),
47− putPublished: (next, _owner, expectedSourceRef) =>
46+ putPublished: (next, nextDescriptorKey, _owner, expectedSourceRef) =>
4847 Effect.gen(function* () {
4948 const actualSourceRef = descriptor?.sourceRef ?? null;
5049 if (actualSourceRef !== expectedSourceRef) {
@@ -55,6 +54,7 @@ const makeMemoryStore = (): AppsStore & { · 4 unmodified lines
5554 });
5655 }
5756 descriptor = next;
57+ descriptorKey = nextDescriptorKey;
5858 for (const key of rows.keys()) rows.delete(key);
5959 for (const item of next.tools) rows.set(item.name, item);
6060 }),
@@ -132,6 +132,7 @@ describe("publish", () => { · 71 unmodified lines
132132 expect(result.publishedTools).toEqual(["sync"]);
133133 expect(result.skipped).toEqual([{ path: "workflows/later.ts", reason: "not supported yet" }]);
134134 expect(result.descriptor.toolchain.executor.name).toBe("in-process-data-url");
135+ expect(store.blobs.size).toBe(2);
135136 }),
136137 );
137138
packages/plugins/apps/src/pipeline/publish.ts+20−32
@@ -180,11 +180,10 @@ export const publish = ( · 179 unmodified lines
180180 }
181181 }
182182
183− const descriptorSeed = {
183+ const descriptor: AppDescriptor = {
184184 version: DESCRIPTOR_VERSION,
185185 app: input.app,
186186 sourceRef: input.sourceRef,
187− descriptorKey: "",
188187 publishedAt: deps.now?.() ?? Date.now(),
189188 toolchain: toolchainRef(),
190189 tools,
@@ -193,49 +192,38 @@ export const publish = ( · 2 unmodified lines
193192 skills: discovered.skipped.filter((item) => item.path.startsWith("skills/")),
194193 skipped: discovered.skipped,
195194 };
196− const descriptorBodyWithoutKey = stableStringify(descriptorSeed);
197− const descriptorKey = yield* deps.store.putBlob(descriptorBodyWithoutKey, owner).pipe(
198− Effect.mapError(
199− () =>
200− new PublishError({
201− stage: "project",
202− message: "failed to write descriptor blob",
203− diagnostics: [],
204− }),
205− ),
206− );
207− const descriptor: AppDescriptor = { ...descriptorSeed, descriptorKey };
208195 const descriptorBody = stableStringify(descriptor);
209− const finalDescriptorKey = yield* deps.store.putBlob(descriptorBody, owner).pipe(
196+ const descriptorKey = yield* deps.store.putBlob(descriptorBody, owner).pipe(
210197 Effect.mapError(
211198 () =>
212199 new PublishError({
213200 stage: "project",
214− message: "failed to write final descriptor blob",
201+ message: "failed to write descriptor blob",
215202 diagnostics: [],
216203 }),
217204 ),
218205 );
219− const finalDescriptor: AppDescriptor = { ...descriptor, descriptorKey: finalDescriptorKey };
220− yield* deps.store.putPublished(finalDescriptor, owner, existing?.sourceRef ?? null).pipe(
221− Effect.mapError((cause) => {
222− if (Predicate.isTagged("AppPublishConflictError")(cause)) {
206+ yield* deps.store
207+ .putPublished(descriptor, descriptorKey, owner, existing?.sourceRef ?? null)
208+ .pipe(
209+ Effect.mapError((cause) => {
210+ if (Predicate.isTagged("AppPublishConflictError")(cause)) {
211+ return new PublishError({
212+ stage: "project",
213+ message: `app "${input.app}" changed during publish`,
214+ diagnostics: [{ path: "", message: "publish sourceRef changed during publish" }],
215+ });
216+ }
223217 return new PublishError({
224218 stage: "project",
225− message: `app "${input.app}" changed during publish`,
226− diagnostics: [{ path: "", message: "publish sourceRef changed during publish" }],
219+ message: "failed to persist app publication",
220+ diagnostics: [],
227221 });
228− }
229− return new PublishError({
230− stage: "project",
231− message: "failed to persist app publication",
232− diagnostics: [],
233− });
234− }),
235− );
222+ }),
223+ );
236224 return {
237− descriptor: finalDescriptor,
238− publishedTools: finalDescriptor.tools.map((tool) => tool.name),
225+ descriptor,
226+ publishedTools: descriptor.tools.map((tool) => tool.name),
239227 skipped: discovered.skipped,
240228 noop: false,
241229 };
packages/plugins/apps/src/plugin/apps-plugin.test.ts+132−1
@@ -1,9 +1,60 @@
11 import { describe, expect, it } from "@effect/vitest";
22 import { Effect } from "effect";
3+import { ToolResult } from "@executor-js/sdk";
34
4−import { projectAppsToolSchema } from "./apps-plugin";
5+import { bundleEntry } from "../pipeline/bundle";
6+import { makeInProcessAppToolExecutor } from "../executor/app-tool-executor";
7+import type { AppDescriptor } from "../pipeline/descriptor";
8+import { makeAppsPlugin, projectAppsToolSchema } from "./apps-plugin";
59 import type { AppsStore } from "./store";
610
11+const bundle = (source: string) =>
12+ bundleEntry({
13+ files: new Map([["tools/sync.ts", source]]),
14+ entry: "tools/sync.ts",
15+ });
16+
17+const makeInvokeStore = (input: {
18+ readonly bundle: string;
19+ readonly integrations: AppDescriptor["tools"][number]["integrations"];
20+}): AppsStore => ({
21+ putBlob: () => Effect.succeed("bundle"),
22+ getBlob: () => Effect.succeed(input.bundle),
23+ getDescriptorRecord: () => Effect.succeed(null),
24+ putPublished: () => Effect.void,
25+ listActiveTools: () => Effect.succeed([]),
26+ getTool: () =>
27+ Effect.succeed({
28+ app: "crm",
29+ name: "sync",
30+ bundleKey: "bundle",
31+ description: "Sync",
32+ integrations: input.integrations,
33+ }),
34+});
35+
36+const invokeCtx = (input: {
37+ readonly bundle: string;
38+ readonly execute: (address: string, args: unknown) => Effect.Effect<unknown, unknown>;
39+}) =>
40+ ({
41+ storage: makeInvokeStore({
42+ bundle: input.bundle,
43+ integrations: { crm: { slug: "dealcloud", mode: "one" } },
44+ }),
45+ connections: {
46+ list: () => Effect.succeed([]),
47+ get: () =>
48+ Effect.succeed({
49+ address: "tools.dealcloud.org.main",
50+ integration: "dealcloud",
51+ owner: "org",
52+ name: "main",
53+ }),
54+ },
55+ execute: (address: string, args: unknown) => input.execute(String(address), args),
56+ }) as never;
57+
758 describe("apps plugin schema projection", () => {
859 it.effect("narrows synthesized integration fields to connection address enums", () =>
960 Effect.gen(function* () {
@@ -63,3 +114,83 @@ describe("apps plugin schema projection", () => { · 53 unmodified lines
63114 }),
64115 );
65116 });
117+
118+describe("apps plugin invocation", () => {
119+ it.effect("surfaces uncaught inner tool failures without binding_error", () =>
120+ Effect.gen(function* () {
121+ const bundled = yield* bundle(`
122+ import { z } from "zod";
123+ import { defineTool, integration } from "executor:app";
124+ export default defineTool({
125+ description: "Sync",
126+ integrations: { crm: integration("dealcloud") },
127+ input: z.object({}),
128+ async handler(_input, { crm }) {
129+ await crm.deals.list({});
130+ return { ok: true };
131+ },
132+ });
133+ `);
134+ const plugin = makeAppsPlugin({ executor: makeInProcessAppToolExecutor() });
135+ const result = yield* plugin.invokeTool!({
136+ ctx: invokeCtx({
137+ bundle: bundled.code,
138+ execute: () =>
139+ Effect.succeed(
140+ ToolResult.fail({ code: "upstream_failed", message: "CRM unavailable" }),
141+ ),
142+ }),
143+ toolRow: { name: "sync" },
144+ args: { crm: "tools.dealcloud.org.main" },
145+ } as never);
146+ expect(result).toMatchObject({
147+ ok: false,
148+ error: {
149+ code: "upstream_failed",
150+ message: expect.stringContaining("tools.dealcloud.org.main.deals.list"),
151+ },
152+ });
153+ expect(result).toMatchObject({
154+ error: { message: expect.stringContaining('"CRM unavailable"') },
155+ });
156+ expect((result as { readonly error: { readonly code: string } }).error.code).not.toBe(
157+ "binding_error",
158+ );
159+ }),
160+ );
161+
162+ it.effect("lets handlers catch inner tool failures and return a fallback", () =>
163+ Effect.gen(function* () {
164+ const bundled = yield* bundle(`
165+ import { z } from "zod";
166+ import { defineTool, integration } from "executor:app";
167+ export default defineTool({
168+ description: "Sync",
169+ integrations: { crm: integration("dealcloud") },
170+ input: z.object({}),
171+ async handler(_input, { crm }) {
172+ try {
173+ await crm.deals.list({});
174+ return { fallback: false };
175+ } catch {
176+ return { fallback: true };
177+ }
178+ },
179+ });
180+ `);
181+ const plugin = makeAppsPlugin({ executor: makeInProcessAppToolExecutor() });
182+ const result = yield* plugin.invokeTool!({
183+ ctx: invokeCtx({
184+ bundle: bundled.code,
185+ execute: () =>
186+ Effect.succeed(
187+ ToolResult.fail({ code: "upstream_failed", message: "CRM unavailable" }),
188+ ),
189+ }),
190+ toolRow: { name: "sync" },
191+ args: { crm: "tools.dealcloud.org.main" },
192+ } as never);
193+ expect(result).toEqual({ fallback: true });
194+ }),
195+ );
196+});
packages/plugins/apps/src/plugin/apps-plugin.ts+61−10
@@ -1,5 +1,5 @@
1−import { Data, Effect } from "effect";
2−import { ToolName, definePlugin, type PluginCtx, type ToolDef } from "@executor-js/sdk";
1+import { Data, Effect, Predicate } from "effect";
2+import { ToolName, ToolResult, definePlugin, type PluginCtx, type ToolDef } from "@executor-js/sdk";
33
44 import { makeInProcessAppToolExecutor, type AppToolExecutor } from "../executor/app-tool-executor";
55 import { publish } from "../pipeline/publish";
@@ -19,6 +19,43 @@ interface ProjectedToolSchema { · 13 unmodified lines
1919 readonly outputSchema?: unknown;
2020 }
2121
22+const innerToolError = (
23+ cause: unknown,
24+): { readonly address: string; readonly innerMessage: string; readonly code?: string } | null => {
25+ const direct =
26+ Predicate.isTagged("AppInnerToolError")(cause) && typeof cause === "object" && cause !== null
27+ ? (cause as {
28+ readonly address?: unknown;
29+ readonly innerMessage?: unknown;
30+ readonly code?: unknown;
31+ })
32+ : null;
33+ const nested =
34+ direct === null &&
35+ cause !== null &&
36+ typeof cause === "object" &&
37+ "cause" in cause &&
38+ Predicate.isTagged("AppInnerToolError")(cause.cause)
39+ ? (cause.cause as {
40+ readonly address?: unknown;
41+ readonly innerMessage?: unknown;
42+ readonly code?: unknown;
43+ })
44+ : direct;
45+ if (
46+ nested === null ||
47+ typeof nested.address !== "string" ||
48+ typeof nested.innerMessage !== "string"
49+ ) {
50+ return null;
51+ }
52+ return {
53+ address: nested.address,
54+ innerMessage: nested.innerMessage,
55+ ...(typeof nested.code === "string" ? { code: nested.code } : {}),
56+ };
57+};
58+
2259 const makeAppsExtension = (ctx: PluginCtx<AppsStore>, executor?: AppToolExecutor) => ({
2360 publish: (input: Parameters<typeof publish>[1]) =>
2461 publish({ store: ctx.storage, executor: executor ?? makeInProcessAppToolExecutor() }, input),
@@ -92,14 +129,28 @@ export const makeAppsPlugin = (options?: { readonly executor?: AppToolExecutor } · 67 unmodified lines
92129 resolver,
93130 invokeOptions,
94131 });
95− const result = yield* (options?.executor ?? makeInProcessAppToolExecutor()).invoke(
96− bundle,
97− { toolName: tool.name },
98− { ...bindings.input, ...bindings.bindings },
99− bridge,
100− { timeoutMs: 30_000 },
101− );
102− return result.output;
132+ const result = yield* (options?.executor ?? makeInProcessAppToolExecutor())
133+ .invoke(
134+ bundle,
135+ { toolName: tool.name },
136+ { ...bindings.input, ...bindings.bindings },
137+ bridge,
138+ { timeoutMs: 30_000 },
139+ )
140+ .pipe(
141+ Effect.catch((cause: unknown) => {
142+ const inner = innerToolError(cause);
143+ return inner
144+ ? Effect.succeed(
145+ ToolResult.fail({
146+ code: inner.code ?? "inner_tool_error",
147+ message: `Inner tool ${inner.address} failed: "${inner.innerMessage}"`,
148+ }),
149+ )
150+ : Effect.fail(cause);
151+ }),
152+ );
153+ return "output" in result ? result.output : result;
103154 }),
104155 }))();
105156
packages/plugins/apps/src/plugin/bindings.ts+10−1
@@ -11,6 +11,15 @@ export class BindingError extends Data.TaggedError("BindingError")<{ · 10 unmodified lines
1111 readonly requestedConnection?: string;
1212 }> {}
1313
14+export class AppInnerToolError extends Data.TaggedError("AppInnerToolError")<{
15+ readonly address: string;
16+ readonly innerMessage: string;
17+ readonly code?: string;
18+}> {}
19+
20+export const appInnerToolFailureMessage = (error: AppInnerToolError): string =>
21+ `Inner tool ${error.address} failed: "${error.innerMessage}"`;
22+
1423 export interface ConnectionCandidate {
1524 readonly address: string;
1625 readonly integration: string;
@@ -29,7 +38,7 @@ export interface ClientResolver { · 12 unmodified lines
2938 readonly path: readonly string[];
3039 readonly args: unknown;
3140 readonly invokeOptions?: InvokeOptions;
32− }) => Effect.Effect<unknown, BindingError>;
41+ }) => Effect.Effect<unknown, BindingError | AppInnerToolError>;
3342 }
3443
3544 export interface ResolvedBindings {
packages/plugins/apps/src/plugin/resolver.ts+44−13
@@ -1,15 +1,21 @@
1−import { Effect } from "effect";
1+import { Effect, Predicate } from "effect";
22 import {
33 ConnectionName,
44 IntegrationSlug,
55 ToolAddress,
66 isToolResult,
7+ type ExecuteError,
78 type ConnectionRef,
89 type Owner,
910 type PluginCtx,
1011 } from "@executor-js/sdk";
1112
12−import { BindingError, type ClientResolver, type ConnectionCandidate } from "./bindings";
13+import {
14+ AppInnerToolError,
15+ BindingError,
16+ type ClientResolver,
17+ type ConnectionCandidate,
18+} from "./bindings";
1319
1420 export interface AppsResolverPluginCtx {
1521 readonly connections: Pick<PluginCtx["connections"], "list" | "get">;
@@ -44,6 +50,34 @@ const toCandidate = (connection: { · 28 unmodified lines
4450 integration: String(connection.integration),
4551 });
4652
53+const innerMessageFromCause = (cause: ExecuteError): string => {
54+ if (
55+ Predicate.isTagged("ToolInvocationError")(cause) ||
56+ Predicate.isTagged("CredentialResolutionError")(cause) ||
57+ Predicate.isTagged("StorageError")(cause)
58+ ) {
59+ // oxlint-disable-next-line executor/no-unknown-error-message -- typed ExecuteError variants expose message as caller-facing failure text
60+ return cause.message;
61+ }
62+ if (Predicate.isTagged("ToolNotFoundError")(cause)) return `Tool not found: ${cause.address}`;
63+ if (Predicate.isTagged("ToolBlockedError")(cause)) return `Tool blocked: ${cause.address}`;
64+ if (Predicate.isTagged("PluginNotLoadedError")(cause)) {
65+ return `Plugin not loaded for tool: ${cause.address}`;
66+ }
67+ if (Predicate.isTagged("NoHandlerError")(cause)) return `No handler for tool: ${cause.address}`;
68+ if (Predicate.isTagged("ConnectionNotFoundError")(cause)) {
69+ return `Connection not found: ${cause.integration}.${cause.owner}.${cause.name}`;
70+ }
71+ if (Predicate.isTagged("CredentialProviderNotRegisteredError")(cause)) {
72+ return `Credential provider not registered: ${cause.provider}`;
73+ }
74+ if (Predicate.isTagged("ElicitationDeclinedError")(cause)) {
75+ return `Tool approval ${cause.action === "cancel" ? "cancelled" : "declined"}: ${cause.address}`;
76+ }
77+ if (Predicate.isTagged("UniqueViolationError")(cause)) return "storage unique violation";
78+ return "inner tool failed";
79+};
80+
4781 export const makePluginCtxAppsResolver = (input: {
4882 readonly ctx: AppsResolverPluginCtx;
4983 }): ClientResolver => ({
@@ -98,22 +132,19 @@ export const makePluginCtxAppsResolver = (input: { · 48 unmodified lines
98132 const address = ToolAddress.make(`${connection}.${path.join(".")}`);
99133 const result = yield* input.ctx.execute(address, args, invokeOptions).pipe(
100134 Effect.mapError(
101− () =>
102− new BindingError({
103− role: integration,
104− integration,
105− requestedConnection: connection,
106− message: `failed to execute ${address}`,
135+ (cause) =>
136+ new AppInnerToolError({
137+ address,
138+ innerMessage: innerMessageFromCause(cause),
107139 }),
108140 ),
109141 );
110142 if (isToolResult(result)) {
111143 if (result.ok) return result.data;
112− return yield* new BindingError({
113− role: integration,
114− integration,
115− requestedConnection: connection,
116− message: result.error.message,
144+ return yield* new AppInnerToolError({
145+ address,
146+ code: result.error.code,
147+ innerMessage: result.error.message,
117148 });
118149 }
119150 return result;
packages/plugins/apps/src/plugin/store.test.ts+7−3
@@ -139,7 +139,6 @@ const descriptor = (tools: readonly AppDescriptor["tools"][number][]): AppDescri · 138 unmodified lines
139139 version: 6,
140140 app: "crm",
141141 sourceRef: "sha-1",
142− descriptorKey: "descriptor",
143142 publishedAt: 1,
144143 toolchain: {
145144 bundler: { name: "esbuild", version: "0" },
@@ -169,8 +168,13 @@ describe("apps store", () => { · 23 unmodified lines
169168 blobs: pluginBlobStore(makeInMemoryBlobStore(), { org: "tenant", user: null }, "apps"),
170169 pluginStorage: makeMemoryPluginStorage(),
171170 });
172− yield* store.putPublished(descriptor([tool("first"), tool("second")]), "org", null);
173− yield* store.putPublished(descriptor([tool("first")]), "org", "sha-1");
171+ yield* store.putPublished(
172+ descriptor([tool("first"), tool("second")]),
173+ "descriptor",
174+ "org",
175+ null,
176+ );
177+ yield* store.putPublished(descriptor([tool("first")]), "descriptor-2", "org", "sha-1");
174178 const active = yield* store.listActiveTools();
175179 expect(active.map((item) => item.name)).toEqual(["first"]);
176180 }),
packages/plugins/apps/src/plugin/store.ts+4−3
@@ -59,6 +59,7 @@ export interface AppsStore { · 58 unmodified lines
5959 >;
6060 readonly putPublished: (
6161 descriptor: AppDescriptor,
62+ descriptorKey: string,
6263 owner: Owner,
6364 expectedSourceRef: string | null,
6465 ) => Effect.Effect<void, StorageFailure | AppPublishConflictError>;
@@ -113,7 +114,7 @@ export const makeAppsStore = (input: { · 48 unmodified lines
113114 : null,
114115 ),
115116 ),
116− putPublished: (descriptor, owner, expectedSourceRef) =>
117+ putPublished: (descriptor, descriptorKey, owner, expectedSourceRef) =>
117118 Effect.gen(function* () {
118119 const current = yield* descriptors.get({ key: descriptor.app });
119120 const actualSourceRef = current?.data.sourceRef ?? null;
@@ -134,7 +135,7 @@ export const makeAppsStore = (input: { · 14 unmodified lines
134135 app: descriptor.app,
135136 name: tool.name,
136137 sourceRef: descriptor.sourceRef,
137− descriptorKey: descriptor.descriptorKey,
138+ descriptorKey,
138139 bundleKey: tool.bundleKey,
139140 description: tool.description,
140141 inputSchema: tool.inputSchema,
@@ -161,7 +162,7 @@ export const makeAppsStore = (input: { · 20 unmodified lines
161162 data: {
162163 app: descriptor.app,
163164 sourceRef: descriptor.sourceRef,
164− descriptorKey: descriptor.descriptorKey,
165+ descriptorKey,
165166 publishedAt: descriptor.publishedAt,
166167 toolchain: descriptor.toolchain,
167168 },

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t