apps 1.0.0-beta.0 → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,7 @@
1
- import { Crypto, type Effect, FileSystem } from "effect";
1
+ import { type Effect, FileSystem } from "effect";
2
2
  import type { AppManifestResult, PublishError } from "../manifest/contract";
3
- /** The capability exposed to one tool requirement role. */
4
- export interface ToolCtxEntry {
5
- readonly grant: string;
6
- readonly token?: string;
7
- readonly secrets?: Readonly<Record<string, string>>;
8
- fetch(path: string, init?: RequestInit): Promise<Response>;
9
- }
10
- /** Tool capabilities keyed by requirement role. */
11
- export type ToolCtx = Readonly<Record<string, ToolCtxEntry>>;
3
+ import type { ToolCtx } from "../runtime/index";
4
+ export type { ToolCtx, ToolCtxEntry } from "../runtime/index";
12
5
  declare const CallError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
13
6
  readonly _tag: "CallError";
14
7
  } & Readonly<A>;
@@ -21,8 +14,7 @@ export declare class CallError extends CallError_base<{
21
14
  }
22
15
  /** The platform protocol implemented by an app execution environment. */
23
16
  export interface AppRunner {
24
- manifest(): Effect.Effect<AppManifestResult, PublishError, Crypto.Crypto | FileSystem.FileSystem>;
25
- call(name: string, input: unknown, ctx: ToolCtx): Effect.Effect<unknown, CallError, Crypto.Crypto | FileSystem.FileSystem>;
26
- discover(name: string, binding: unknown): Effect.Effect<unknown, CallError, Crypto.Crypto | FileSystem.FileSystem>;
17
+ manifest(): Effect.Effect<AppManifestResult, PublishError, FileSystem.FileSystem>;
18
+ call(name: string, input: unknown, ctx: ToolCtx): Effect.Effect<unknown, CallError, FileSystem.FileSystem>;
19
+ discover(name: string, binding: unknown): Effect.Effect<unknown, CallError, FileSystem.FileSystem>;
27
20
  }
28
- export {};
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  CallError
3
- } from "../index-dh4z81es.js";
3
+ } from "../chunk-NFLNA2SC.js";
4
4
  export {
5
5
  CallError
6
6
  };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The execution core: everything that happens after authored modules
3
+ * load, in any environment. Project a registry into its deterministic
4
+ * manifest, validate a call's input, run the handler, shape the
5
+ * outcome. There is exactly one implementation of this logic; the
6
+ * local runner and the sandboxed workerd guest are both thin doors
7
+ * onto this module.
8
+ *
9
+ * Deliberately dependency-free (no effect, no node builtins): this
10
+ * module bundles into sandboxed guests, where every dependency is
11
+ * paid on each cold start. Digesting uses WebCrypto, which node, bun,
12
+ * and workerd all provide.
13
+ */
14
+ import type { Registry } from "../authoring/index";
15
+ import type { AppManifestResult } from "../manifest/contract";
16
+ /** The capability exposed to one tool requirement role. */
17
+ export interface ToolCtxEntry {
18
+ readonly grant: string;
19
+ readonly token?: string;
20
+ readonly secrets?: Readonly<Record<string, string>>;
21
+ fetch(path: string, init?: RequestInit): Promise<Response>;
22
+ }
23
+ /** Tool capabilities keyed by requirement role. */
24
+ export type ToolCtx = Readonly<Record<string, ToolCtxEntry>>;
25
+ /** A manifest projection: the app's deterministic description, or why not. */
26
+ export type ManifestOutcome = {
27
+ readonly ok: true;
28
+ readonly result: AppManifestResult;
29
+ } | {
30
+ readonly ok: false;
31
+ readonly problems: readonly string[];
32
+ };
33
+ /** One tool invocation's result envelope. */
34
+ export type CallOutcome = {
35
+ readonly ok: true;
36
+ readonly value: unknown;
37
+ } | {
38
+ readonly ok: false;
39
+ readonly reason: string;
40
+ };
41
+ /** Project a loaded registry into the app's deterministic manifest. */
42
+ export declare const manifestOf: (registry: Registry) => Promise<ManifestOutcome>;
43
+ /** Validate input and run one registered tool's handler. */
44
+ export declare const callTool: (registry: Registry, name: string, input: unknown, ctx: ToolCtx) => Promise<CallOutcome>;
45
+ /** Discover one dynamic tool set's current segment. Unbuilt: always fails. */
46
+ export declare const discoverTool: (_registry: Registry, _name: string, _binding: unknown) => Promise<CallOutcome>;
@@ -0,0 +1,10 @@
1
+ import {
2
+ callTool,
3
+ discoverTool,
4
+ manifestOf
5
+ } from "../chunk-OGCPVWAZ.js";
6
+ export {
7
+ callTool,
8
+ discoverTool,
9
+ manifestOf
10
+ };
@@ -1,2 +1,7 @@
1
- /** Worker-safe registry projection injected into sandboxed app workers. @internal */
2
- export { collectRegistry } from "../manifest/collect";
1
+ /**
2
+ * The execution core re-exported for sandboxed guests. A generated
3
+ * guest module imports the authored files (filling the registry) and
4
+ * exposes these verbs over RPC; the logic itself lives in ../runtime
5
+ * and is identical to what the local runner executes. @internal
6
+ */
7
+ export { callTool, discoverTool, manifestOf, type CallOutcome, type ManifestOutcome, type ToolCtx, type ToolCtxEntry, } from "../runtime/index";
@@ -1,3 +1,10 @@
1
+ import {
2
+ callTool,
3
+ discoverTool,
4
+ manifestOf
5
+ } from "../chunk-OGCPVWAZ.js";
1
6
  export {
2
- collectRegistry
7
+ callTool,
8
+ discoverTool,
9
+ manifestOf
3
10
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apps",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.2",
4
4
  "description": "Author executor apps: declare services, auth, and tools in plain TypeScript; the executor platform runs, validates, and serves them.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,6 +24,10 @@
24
24
  "types": "./dist/authoring/index.d.ts",
25
25
  "default": "./dist/authoring/index.js"
26
26
  },
27
+ "./runtime": {
28
+ "types": "./dist/runtime/index.d.ts",
29
+ "default": "./dist/runtime/index.js"
30
+ },
27
31
  "./manifest": {
28
32
  "types": "./dist/manifest/index.d.ts",
29
33
  "default": "./dist/manifest/index.js"
@@ -55,6 +59,7 @@
55
59
  },
56
60
  "devDependencies": {
57
61
  "@types/node": "22",
62
+ "esbuild": "0.25",
58
63
  "typescript": "5.9.3"
59
64
  }
60
65
  }
package/src/index.ts CHANGED
@@ -1,9 +1 @@
1
- /**
2
- * The authoring surface: what app code imports. Pure and synchronous —
3
- * no effect dependency, no node builtins — so authored files run
4
- * unmodified in any execution environment, including sandboxed workers.
5
- *
6
- * Host-side machinery lives behind subpaths: "apps/manifest" loads and
7
- * validates authored sources, "apps/runner" executes them.
8
- */
9
1
  export * from "./authoring/index";
@@ -1,11 +1,13 @@
1
1
  /**
2
- * Manifest: execute an authored app folder, read its registry, validate
3
- * every claim, and emit the deterministic publish manifest. Defines run
4
- * without user context; platform services begin at this boundary.
2
+ * Manifest: execute an authored app folder and project its registry.
3
+ * This module owns exactly what differs on a host with a filesystem —
4
+ * finding and importing authored modules. Everything after the modules
5
+ * load (validation, canonicalization, digesting) is the shared
6
+ * execution core in ../runtime; sandboxed guests run that same core.
5
7
  */
6
8
 
7
9
  import { fileURLToPath, pathToFileURL } from "node:url";
8
- import { Crypto, Effect, FileSystem, Schema, Semaphore } from "effect";
10
+ import { Effect, FileSystem, MutableRef, Semaphore } from "effect";
9
11
  import {
10
12
  __enterFile,
11
13
  __leaveFile,
@@ -13,118 +15,51 @@ import {
13
15
  setActiveRegistry,
14
16
  type Registry,
15
17
  } from "../authoring/index";
16
- import { collectRegistry, type InputSchemaCompilation } from "./collect";
17
- import {
18
- JsonSchemaObject,
19
- PublishError,
20
- type AppManifestResult,
21
- type JsonSchemaObject as JsonSchemaObjectType,
22
- } from "./contract";
18
+ import { manifestOf } from "../runtime/index";
19
+ import { PublishError, type AppManifestResult } from "./contract";
23
20
 
24
21
  export * from "./contract";
25
22
 
26
- interface StandardJsonSchemaCapable {
27
- readonly "~standard": {
28
- readonly jsonSchema?: {
29
- readonly input: (options: { readonly target: string }) => unknown;
30
- };
31
- };
32
- }
33
-
34
- const hasStandardJsonSchema = (input: object): input is StandardJsonSchemaCapable =>
35
- "~standard" in input &&
36
- typeof (input as StandardJsonSchemaCapable)["~standard"].jsonSchema?.input === "function";
37
-
38
- const compileInputSchema = (
39
- input: object,
40
- where: string,
41
- ): InputSchemaCompilation => {
42
- if ("~standard" in input) {
43
- if (!hasStandardJsonSchema(input))
44
- return {
45
- problem: `${where}: input is a Standard Schema without the jsonSchema extension — use a library that implements it (zod 4+, effect Schema) or pass raw JSON schema`,
46
- };
47
- }
48
-
49
- try {
50
- const compiled = strictify(
51
- hasStandardJsonSchema(input)
52
- ? input["~standard"].jsonSchema!.input({ target: "draft-2020-12" })
53
- : structuredClone(input),
54
- );
55
- return { value: Schema.decodeUnknownSync(JsonSchemaObject)(compiled) as JsonSchemaObjectType };
56
- } catch (error) {
57
- return { problem: schemaPublishProblem(where, error) };
58
- }
59
- };
60
-
61
- const schemaPublishProblem = (where: string, error: unknown): string =>
62
- `${where}: input schema could not compile to JSON schema (${
63
- error instanceof Error ? error.message : String(error)
64
- })`;
65
-
66
- const strictify = (node: unknown): unknown => {
67
- if (typeof node !== "object" || node === null) return node;
68
- if (Array.isArray(node)) return node.map(strictify);
69
- const object = { ...(node as Record<string, unknown>) };
70
- if (object.type === "object" && object.additionalProperties === undefined)
71
- object.additionalProperties = false;
72
- for (const key of ["items", "anyOf", "oneOf", "allOf"])
73
- if (key in object) object[key] = strictify(object[key]);
74
- for (const key of ["properties", "$defs"])
75
- if (object[key] !== undefined && typeof object[key] === "object" && object[key] !== null)
76
- object[key] = Object.fromEntries(
77
- Object.entries(object[key] as Record<string, unknown>).map(([name, value]) => [
78
- name,
79
- strictify(value),
80
- ]),
81
- );
82
- return object;
83
- };
84
-
85
- const digestOf = (content: string): Effect.Effect<string, never, Crypto.Crypto> =>
86
- Effect.gen(function* () {
87
- const crypto = yield* Crypto.Crypto;
88
- const bytes = yield* Effect.orDie(
89
- crypto.digest("SHA-256", new TextEncoder().encode(content)),
90
- );
91
- return Array.from(bytes.slice(0, 8), (byte) => byte.toString(16).padStart(2, "0")).join("");
92
- });
23
+ const moduleProblem = (where: string, error: unknown): string =>
24
+ `${where}: failed to execute (${error instanceof Error ? error.message : String(error)})`;
93
25
 
26
+ /**
27
+ * Module execution is process-global twice over: the authoring
28
+ * registry is a singleton the imported modules write into, and the
29
+ * ESM module cache is shared. One permit serializes loads; the nonce
30
+ * and first-import memo make re-imports observable across runs.
31
+ * Module-scoped by necessity — this state IS process state.
32
+ */
94
33
  const loadPermit = Semaphore.makeUnsafe(1);
95
- let runNonce = 0;
96
- const baseAppImports = new Set<string>();
97
-
98
- const moduleFailure = (where: string, error: unknown): PublishError =>
99
- new PublishError({
100
- problems: [
101
- `${where}: failed to execute (${error instanceof Error ? error.message : String(error)})`,
102
- ],
103
- });
34
+ const runNonce = MutableRef.make(0);
35
+ const importedAppUrls = MutableRef.make<ReadonlySet<string>>(new Set());
104
36
 
105
37
  const canonicalModuleUrl = (
106
38
  path: string,
107
39
  where: string,
108
40
  ): Effect.Effect<URL, PublishError, FileSystem.FileSystem> =>
109
- Effect.gen(function* () {
110
- const fs = yield* FileSystem.FileSystem;
111
- const realPath = yield* fs.realPath(path).pipe(
112
- Effect.mapError((error) => moduleFailure(where, error)),
113
- );
114
- return pathToFileURL(realPath);
115
- });
41
+ FileSystem.FileSystem.pipe(
42
+ Effect.flatMap((fs) => fs.realPath(path)),
43
+ Effect.map(pathToFileURL),
44
+ Effect.mapError((error) => new PublishError({ problems: [moduleProblem(where, error)] })),
45
+ );
116
46
 
117
47
  const importModule = (specifier: string, where: string): Effect.Effect<void, PublishError> =>
118
48
  Effect.tryPromise({
119
- try: () => import(specifier).then(() => undefined),
120
- catch: (error) => moduleFailure(where, error),
121
- });
49
+ try: () => import(specifier),
50
+ catch: (error) => new PublishError({ problems: [moduleProblem(where, error)] }),
51
+ }).pipe(Effect.asVoid);
122
52
 
123
- const recordModuleProblems = (
53
+ /** Import one authored module, converting failure into registry problems. */
54
+ const executeModule = (
124
55
  registry: Registry,
125
- effect: Effect.Effect<void, PublishError, FileSystem.FileSystem>,
56
+ where: string,
57
+ load: Effect.Effect<void, PublishError, FileSystem.FileSystem>,
126
58
  ): Effect.Effect<void, never, FileSystem.FileSystem> =>
127
- effect.pipe(
59
+ Effect.suspend(() => {
60
+ __enterFile(where);
61
+ return load;
62
+ }).pipe(
128
63
  Effect.catchTag("PublishError", (error) =>
129
64
  Effect.sync(() => {
130
65
  registry.problems.push(...error.problems.map((message) => ({ message })));
@@ -132,58 +67,70 @@ const recordModuleProblems = (
132
67
  ),
133
68
  );
134
69
 
70
+ /**
71
+ * The app entry loads through its plain canonical URL the first time
72
+ * so relative imports resolve normally, and through a nonce query on
73
+ * every later run so edits are observed instead of served stale from
74
+ * the module cache.
75
+ */
76
+ const appEntrySpecifier = (appUrl: URL, nonce: number): string => {
77
+ const seen = MutableRef.get(importedAppUrls);
78
+ if (!seen.has(appUrl.href)) {
79
+ MutableRef.set(importedAppUrls, new Set([...seen, appUrl.href]));
80
+ return appUrl.href;
81
+ }
82
+ return `${fileURLToPath(appUrl)}?m=${nonce}`;
83
+ };
84
+
135
85
  const executeModules = (
136
86
  appDir: string,
137
87
  files: readonly string[],
138
88
  ): Effect.Effect<Registry, never, FileSystem.FileSystem> =>
139
89
  loadPermit.withPermit(
140
- Effect.uninterruptibleMask((restore) =>
141
- Effect.gen(function* () {
90
+ Effect.acquireUseRelease(
91
+ Effect.sync(() => {
142
92
  const registry = newRegistry();
143
- const previous = yield* Effect.sync(() => setActiveRegistry(registry));
144
- const nonce = yield* Effect.sync(() => ++runNonce);
145
-
146
- const execute = Effect.gen(function* () {
147
- __enterFile("app.ts");
148
- yield* recordModuleProblems(
93
+ return { registry, previous: setActiveRegistry(registry) };
94
+ }),
95
+ ({ registry }) =>
96
+ Effect.gen(function* () {
97
+ const nonce = MutableRef.incrementAndGet(runNonce);
98
+ yield* executeModule(
149
99
  registry,
150
- Effect.gen(function* () {
151
- const appUrl = yield* canonicalModuleUrl(`${appDir}/app.ts`, "app.ts");
152
- const baseUrl = appUrl.href;
153
- const specifier = baseAppImports.has(baseUrl)
154
- ? `${fileURLToPath(appUrl)}?m=${nonce}`
155
- : baseUrl;
156
- yield* importModule(specifier, "app.ts");
157
- yield* Effect.sync(() => baseAppImports.add(baseUrl));
158
- }),
100
+ "app.ts",
101
+ canonicalModuleUrl(`${appDir}/app.ts`, "app.ts").pipe(
102
+ Effect.flatMap((appUrl) => importModule(appEntrySpecifier(appUrl, nonce), "app.ts")),
103
+ ),
159
104
  );
160
-
161
105
  for (const file of files) {
162
106
  const where = `tools/${file}`;
163
- __enterFile(where);
164
- yield* recordModuleProblems(
107
+ yield* executeModule(
165
108
  registry,
166
- Effect.gen(function* () {
167
- const url = yield* canonicalModuleUrl(`${appDir}/tools/${file}`, where);
168
- yield* importModule(`${fileURLToPath(url)}?m=${nonce}`, where);
169
- }),
109
+ where,
110
+ canonicalModuleUrl(`${appDir}/tools/${file}`, where).pipe(
111
+ Effect.flatMap((url) => importModule(`${fileURLToPath(url)}?m=${nonce}`, where)),
112
+ ),
170
113
  );
171
114
  }
172
- });
173
-
174
- yield* restore(execute).pipe(
175
- Effect.ensuring(
176
- Effect.sync(() => {
177
- __leaveFile();
178
- setActiveRegistry(previous);
179
- }),
180
- ),
181
- );
182
- return registry;
183
- }),
115
+ return registry;
116
+ }),
117
+ ({ previous }) =>
118
+ Effect.sync(() => {
119
+ __leaveFile();
120
+ setActiveRegistry(previous);
121
+ }),
184
122
  ),
185
123
  );
186
124
 
125
+ const toolFiles = (
126
+ appDir: string,
127
+ ): Effect.Effect<readonly string[], PublishError, FileSystem.FileSystem> =>
128
+ FileSystem.FileSystem.pipe(
129
+ Effect.flatMap((fs) => fs.readDirectory(`${appDir}/tools`)),
130
+ Effect.mapError((error) => new PublishError({ problems: [`tools/: ${error.message}`] })),
131
+ Effect.map((entries) => entries.filter((file) => file.endsWith(".ts")).sort()),
132
+ );
133
+
187
134
  /** A registry and its validated public projection. @internal */
188
135
  export interface LoadedApp {
189
136
  readonly registry: Registry;
@@ -193,32 +140,17 @@ export interface LoadedApp {
193
140
  /** Loads an app once and returns both its live registry and manifest. @internal */
194
141
  export const loadApp = (
195
142
  appDir: string,
196
- ): Effect.Effect<LoadedApp, PublishError, Crypto.Crypto | FileSystem.FileSystem> =>
143
+ ): Effect.Effect<LoadedApp, PublishError, FileSystem.FileSystem> =>
197
144
  Effect.gen(function* () {
198
- const fs = yield* FileSystem.FileSystem;
199
- const files = (yield* fs.readDirectory(`${appDir}/tools`).pipe(
200
- Effect.mapError((error) => new PublishError({ problems: [`tools/: ${error.message}`] })),
201
- ))
202
- .filter((file) => file.endsWith(".ts"))
203
- .sort();
145
+ const files = yield* toolFiles(appDir);
204
146
  const registry = yield* executeModules(appDir, files);
205
- const collected = collectRegistry(registry, compileInputSchema);
206
- if (collected.problems.length > 0)
207
- return yield* new PublishError({ problems: collected.problems });
208
- if (!("manifest" in collected))
209
- return yield* Effect.die("validated registry projection unexpectedly missing");
210
-
211
- return {
212
- registry,
213
- result: {
214
- digest: `snap:${yield* digestOf(collected.canonical)}`,
215
- ...collected.manifest,
216
- },
217
- };
147
+ const outcome = yield* Effect.promise(() => manifestOf(registry));
148
+ if (!outcome.ok) return yield* new PublishError({ problems: outcome.problems });
149
+ return { registry, result: outcome.result };
218
150
  });
219
151
 
220
152
  /** Executes an app and returns its deterministic publish manifest. */
221
153
  export const manifest = (
222
154
  appDir: string,
223
- ): Effect.Effect<AppManifestResult, PublishError, Crypto.Crypto | FileSystem.FileSystem> =>
155
+ ): Effect.Effect<AppManifestResult, PublishError, FileSystem.FileSystem> =>
224
156
  Effect.map(loadApp(appDir), ({ result }) => result);
@@ -1,73 +1,21 @@
1
- import { Crypto, Effect, FileSystem } from "effect";
2
- import type { ToolSpec } from "../authoring/index";
1
+ import { Effect, FileSystem } from "effect";
3
2
  import {
4
3
  loadApp,
5
4
  type LoadedApp,
6
5
  type PublishError,
7
6
  } from "../manifest/index";
8
- import { CallError, type AppRunner, type ToolCtx } from "./protocol";
7
+ import { callTool, discoverTool, type CallOutcome } from "../runtime/index";
8
+ import { CallError, type AppRunner } from "./protocol";
9
9
 
10
10
  export * from "./protocol";
11
11
 
12
- interface StandardSchemaCapable {
13
- readonly "~standard": {
14
- readonly validate: (
15
- value: unknown,
16
- ) =>
17
- | { readonly value: unknown; readonly issues?: undefined }
18
- | { readonly issues: ReadonlyArray<{ readonly message: string }> }
19
- | Promise<
20
- | { readonly value: unknown; readonly issues?: undefined }
21
- | { readonly issues: ReadonlyArray<{ readonly message: string }> }
22
- >;
23
- };
24
- }
25
-
26
- const hasStandardValidate = (input: object): input is StandardSchemaCapable =>
27
- "~standard" in input &&
28
- typeof (input as StandardSchemaCapable)["~standard"].validate === "function";
29
-
30
- const parseInput = (name: string, spec: ToolSpec, input: unknown): Effect.Effect<unknown, CallError> => {
31
- if (!hasStandardValidate(spec.input))
32
- return Effect.fail(
33
- new CallError({ target: name, reason: "input schema does not implement Standard Schema validate" }),
34
- );
35
- const standard = spec.input["~standard"];
36
-
37
- return Effect.tryPromise({
38
- try: () => Promise.resolve(standard.validate(input)),
39
- catch: (error) =>
40
- new CallError({
41
- target: name,
42
- reason: `input validation threw (${error instanceof Error ? error.message : String(error)})`,
43
- }),
44
- }).pipe(
45
- Effect.flatMap((result) =>
46
- result.issues === undefined
47
- ? Effect.succeed(result.value)
48
- : Effect.fail(
49
- new CallError({
50
- target: name,
51
- reason: `invalid input (${result.issues.map((issue) => issue.message).join("; ")})`,
52
- }),
53
- ),
54
- ),
55
- );
56
- };
57
-
58
12
  /** Creates a permissive in-process runner for local development and tests. */
59
13
  export const localRunner = (appDir: string): AppRunner => {
60
- let cachedLoad: Effect.Effect<
61
- LoadedApp,
62
- PublishError,
63
- Crypto.Crypto | FileSystem.FileSystem
64
- > | undefined;
14
+ let cachedLoad:
15
+ | Effect.Effect<LoadedApp, PublishError, FileSystem.FileSystem>
16
+ | undefined;
65
17
 
66
- const loaded = (): Effect.Effect<
67
- LoadedApp,
68
- PublishError,
69
- Crypto.Crypto | FileSystem.FileSystem
70
- > =>
18
+ const loaded = (): Effect.Effect<LoadedApp, PublishError, FileSystem.FileSystem> =>
71
19
  Effect.suspend(() => {
72
20
  if (cachedLoad !== undefined) return cachedLoad;
73
21
  return Effect.flatMap(Effect.cached(loadApp(appDir)), (effect) =>
@@ -78,40 +26,32 @@ export const localRunner = (appDir: string): AppRunner => {
78
26
  );
79
27
  });
80
28
 
81
- const loadedFor = (
82
- target: string,
83
- ): Effect.Effect<LoadedApp, CallError, Crypto.Crypto | FileSystem.FileSystem> =>
29
+ const registryFor = (target: string) =>
84
30
  loaded().pipe(
31
+ Effect.map(({ registry }) => registry),
85
32
  Effect.mapError(
86
33
  (error) => new CallError({ target, reason: `app failed to load (${error.message})` }),
87
34
  ),
88
35
  );
89
36
 
37
+ const settle = (target: string, outcome: CallOutcome): Effect.Effect<unknown, CallError> =>
38
+ outcome.ok
39
+ ? Effect.succeed(outcome.value)
40
+ : Effect.fail(new CallError({ target, reason: outcome.reason }));
41
+
90
42
  return {
91
43
  manifest: () => Effect.map(loaded(), ({ result }) => result),
92
44
  call: (name, input, ctx) =>
93
- Effect.gen(function* () {
94
- const { registry } = yield* loadedFor(name);
95
- const registered = registry.tools.get(name);
96
- if (registered === undefined)
97
- return yield* new CallError({ target: name, reason: "unknown tool" });
98
- const parsed = yield* parseInput(name, registered.spec, input);
99
- return yield* Effect.tryPromise({
100
- // SAFETY: ToolSpec intentionally erases authored handler inputs to keep
101
- // authoring generic-free; validation and ToolCtx establish this call boundary.
102
- try: () =>
103
- (registered.spec.handler as (value: unknown, toolCtx: ToolCtx) => Promise<unknown>)(
104
- parsed,
105
- ctx,
106
- ),
107
- catch: (error) =>
108
- new CallError({
109
- target: name,
110
- reason: `handler failed (${error instanceof Error ? error.message : String(error)})`,
111
- }),
112
- });
113
- }),
114
- discover: (name, _binding) =>
115
- Effect.fail(new CallError({ target: name, reason: "no dynamic tools yet" })),
45
+ registryFor(name).pipe(
46
+ Effect.flatMap((registry) => Effect.promise(() => callTool(registry, name, input, ctx))),
47
+ Effect.flatMap((outcome) => settle(name, outcome)),
48
+ ),
49
+ discover: (name, binding) =>
50
+ registryFor(name).pipe(
51
+ Effect.flatMap((registry) =>
52
+ Effect.promise(() => discoverTool(registry, name, binding)),
53
+ ),
54
+ Effect.flatMap((outcome) => settle(name, outcome)),
55
+ ),
116
56
  };
117
57
  };
@@ -1,16 +1,8 @@
1
- import { Crypto, Data, type Effect, FileSystem } from "effect";
1
+ import { Data, type Effect, FileSystem } from "effect";
2
2
  import type { AppManifestResult, PublishError } from "../manifest/contract";
3
+ import type { ToolCtx, ToolCtxEntry } from "../runtime/index";
3
4
 
4
- /** The capability exposed to one tool requirement role. */
5
- export interface ToolCtxEntry {
6
- readonly grant: string;
7
- readonly token?: string;
8
- readonly secrets?: Readonly<Record<string, string>>;
9
- fetch(path: string, init?: RequestInit): Promise<Response>;
10
- }
11
-
12
- /** Tool capabilities keyed by requirement role. */
13
- export type ToolCtx = Readonly<Record<string, ToolCtxEntry>>;
5
+ export type { ToolCtx, ToolCtxEntry } from "../runtime/index";
14
6
 
15
7
  /** A runner operation failure associated with its target. */
16
8
  export class CallError extends Data.TaggedError("CallError")<{
@@ -24,18 +16,14 @@ export class CallError extends Data.TaggedError("CallError")<{
24
16
 
25
17
  /** The platform protocol implemented by an app execution environment. */
26
18
  export interface AppRunner {
27
- manifest(): Effect.Effect<
28
- AppManifestResult,
29
- PublishError,
30
- Crypto.Crypto | FileSystem.FileSystem
31
- >;
19
+ manifest(): Effect.Effect<AppManifestResult, PublishError, FileSystem.FileSystem>;
32
20
  call(
33
21
  name: string,
34
22
  input: unknown,
35
23
  ctx: ToolCtx,
36
- ): Effect.Effect<unknown, CallError, Crypto.Crypto | FileSystem.FileSystem>;
24
+ ): Effect.Effect<unknown, CallError, FileSystem.FileSystem>;
37
25
  discover(
38
26
  name: string,
39
27
  binding: unknown,
40
- ): Effect.Effect<unknown, CallError, Crypto.Crypto | FileSystem.FileSystem>;
28
+ ): Effect.Effect<unknown, CallError, FileSystem.FileSystem>;
41
29
  }