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.
- package/README.md +3 -2
- package/dist/authoring/index.js +11 -11
- package/dist/chunk-646MS7DD.js +99 -0
- package/dist/{index-mcjwntr2.js → chunk-B7NEUIJL.js} +7 -6
- package/dist/{index-8wmxmfhc.js → chunk-LWKQDDLZ.js} +30 -22
- package/dist/{index-dh4z81es.js → chunk-NFLNA2SC.js} +5 -4
- package/dist/chunk-OGCPVWAZ.js +191 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.js +11 -11
- package/dist/manifest/contract.js +3 -3
- package/dist/manifest/index.d.ts +8 -6
- package/dist/manifest/index.js +7 -6
- package/dist/runner/index.js +35 -44
- package/dist/runner/protocol.d.ts +6 -14
- package/dist/runner/protocol.js +1 -1
- package/dist/runtime/index.d.ts +46 -0
- package/dist/runtime/index.js +10 -0
- package/dist/workerd/index.d.ts +7 -2
- package/dist/workerd/index.js +8 -1
- package/package.json +6 -1
- package/src/index.ts +0 -8
- package/src/manifest/index.ts +89 -157
- package/src/runner/index.ts +25 -85
- package/src/runner/protocol.ts +6 -18
- package/src/runtime/index.ts +363 -0
- package/src/workerd/index.ts +15 -2
- package/dist/index-wfngzvv2.js +0 -228
- package/dist/manifest/collect.d.ts +0 -64
- package/src/manifest/collect.ts +0 -198
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type Effect, FileSystem } from "effect";
|
|
2
2
|
import type { AppManifestResult, PublishError } from "../manifest/contract";
|
|
3
|
-
|
|
4
|
-
export
|
|
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,
|
|
25
|
-
call(name: string, input: unknown, ctx: ToolCtx): Effect.Effect<unknown, CallError,
|
|
26
|
-
discover(name: string, binding: unknown): Effect.Effect<unknown, CallError,
|
|
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 {};
|
package/dist/runner/protocol.js
CHANGED
|
@@ -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>;
|
package/dist/workerd/index.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
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";
|
package/dist/workerd/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apps",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
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";
|
package/src/manifest/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Manifest: execute an authored app folder
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
96
|
-
const
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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)
|
|
120
|
-
catch: (error) =>
|
|
121
|
-
});
|
|
49
|
+
try: () => import(specifier),
|
|
50
|
+
catch: (error) => new PublishError({ problems: [moduleProblem(where, error)] }),
|
|
51
|
+
}).pipe(Effect.asVoid);
|
|
122
52
|
|
|
123
|
-
|
|
53
|
+
/** Import one authored module, converting failure into registry problems. */
|
|
54
|
+
const executeModule = (
|
|
124
55
|
registry: Registry,
|
|
125
|
-
|
|
56
|
+
where: string,
|
|
57
|
+
load: Effect.Effect<void, PublishError, FileSystem.FileSystem>,
|
|
126
58
|
): Effect.Effect<void, never, FileSystem.FileSystem> =>
|
|
127
|
-
|
|
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.
|
|
141
|
-
Effect.
|
|
90
|
+
Effect.acquireUseRelease(
|
|
91
|
+
Effect.sync(() => {
|
|
142
92
|
const registry = newRegistry();
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
yield*
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
164
|
-
yield* recordModuleProblems(
|
|
107
|
+
yield* executeModule(
|
|
165
108
|
registry,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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,
|
|
143
|
+
): Effect.Effect<LoadedApp, PublishError, FileSystem.FileSystem> =>
|
|
197
144
|
Effect.gen(function* () {
|
|
198
|
-
const
|
|
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
|
|
206
|
-
if (
|
|
207
|
-
|
|
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,
|
|
155
|
+
): Effect.Effect<AppManifestResult, PublishError, FileSystem.FileSystem> =>
|
|
224
156
|
Effect.map(loadApp(appDir), ({ result }) => result);
|
package/src/runner/index.ts
CHANGED
|
@@ -1,73 +1,21 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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:
|
|
61
|
-
LoadedApp,
|
|
62
|
-
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
};
|
package/src/runner/protocol.ts
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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,
|
|
24
|
+
): Effect.Effect<unknown, CallError, FileSystem.FileSystem>;
|
|
37
25
|
discover(
|
|
38
26
|
name: string,
|
|
39
27
|
binding: unknown,
|
|
40
|
-
): Effect.Effect<unknown, CallError,
|
|
28
|
+
): Effect.Effect<unknown, CallError, FileSystem.FileSystem>;
|
|
41
29
|
}
|