apps 1.0.0-beta.1 → 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/chunk-646MS7DD.js +99 -0
- package/dist/chunk-OGCPVWAZ.js +191 -0
- package/dist/index.d.ts +0 -8
- package/dist/manifest/index.d.ts +8 -6
- package/dist/manifest/index.js +2 -2
- package/dist/runner/index.js +18 -47
- package/dist/runner/protocol.d.ts +6 -14
- 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 +7 -3
- package/package.json +6 -2
- 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/chunk-J5AVXTHA.js +0 -154
- package/dist/chunk-RZA7AXBK.js +0 -107
- package/dist/manifest/collect.d.ts +0 -64
- package/src/manifest/collect.ts +0 -198
package/README.md
CHANGED
|
@@ -49,5 +49,6 @@ export default defineTool({
|
|
|
49
49
|
|
|
50
50
|
The root export is pure and synchronous — no effect dependency, no node
|
|
51
51
|
builtins — so authored files run unmodified anywhere, including sandboxed
|
|
52
|
-
workers.
|
|
53
|
-
`apps/
|
|
52
|
+
workers. `apps/runtime` is the single execution core every environment
|
|
53
|
+
shares; host machinery lives behind `apps/manifest` and `apps/runner` and
|
|
54
|
+
requires `effect`.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__enterFile,
|
|
3
|
+
__leaveFile,
|
|
4
|
+
newRegistry,
|
|
5
|
+
setActiveRegistry
|
|
6
|
+
} from "./chunk-LWKQDDLZ.js";
|
|
7
|
+
import {
|
|
8
|
+
PublishError
|
|
9
|
+
} from "./chunk-B7NEUIJL.js";
|
|
10
|
+
import {
|
|
11
|
+
manifestOf
|
|
12
|
+
} from "./chunk-OGCPVWAZ.js";
|
|
13
|
+
|
|
14
|
+
// src/manifest/index.ts
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
16
|
+
import { Effect, FileSystem, MutableRef, Semaphore } from "effect";
|
|
17
|
+
var moduleProblem = (where, error) => `${where}: failed to execute (${error instanceof Error ? error.message : String(error)})`;
|
|
18
|
+
var loadPermit = Semaphore.makeUnsafe(1);
|
|
19
|
+
var runNonce = MutableRef.make(0);
|
|
20
|
+
var importedAppUrls = MutableRef.make(/* @__PURE__ */ new Set());
|
|
21
|
+
var canonicalModuleUrl = (path, where) => FileSystem.FileSystem.pipe(
|
|
22
|
+
Effect.flatMap((fs) => fs.realPath(path)),
|
|
23
|
+
Effect.map(pathToFileURL),
|
|
24
|
+
Effect.mapError((error) => new PublishError({ problems: [moduleProblem(where, error)] }))
|
|
25
|
+
);
|
|
26
|
+
var importModule = (specifier, where) => Effect.tryPromise({
|
|
27
|
+
try: () => import(specifier),
|
|
28
|
+
catch: (error) => new PublishError({ problems: [moduleProblem(where, error)] })
|
|
29
|
+
}).pipe(Effect.asVoid);
|
|
30
|
+
var executeModule = (registry, where, load) => Effect.suspend(() => {
|
|
31
|
+
__enterFile(where);
|
|
32
|
+
return load;
|
|
33
|
+
}).pipe(
|
|
34
|
+
Effect.catchTag(
|
|
35
|
+
"PublishError",
|
|
36
|
+
(error) => Effect.sync(() => {
|
|
37
|
+
registry.problems.push(...error.problems.map((message) => ({ message })));
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
var appEntrySpecifier = (appUrl, nonce) => {
|
|
42
|
+
const seen = MutableRef.get(importedAppUrls);
|
|
43
|
+
if (!seen.has(appUrl.href)) {
|
|
44
|
+
MutableRef.set(importedAppUrls, /* @__PURE__ */ new Set([...seen, appUrl.href]));
|
|
45
|
+
return appUrl.href;
|
|
46
|
+
}
|
|
47
|
+
return `${fileURLToPath(appUrl)}?m=${nonce}`;
|
|
48
|
+
};
|
|
49
|
+
var executeModules = (appDir, files) => loadPermit.withPermit(
|
|
50
|
+
Effect.acquireUseRelease(
|
|
51
|
+
Effect.sync(() => {
|
|
52
|
+
const registry = newRegistry();
|
|
53
|
+
return { registry, previous: setActiveRegistry(registry) };
|
|
54
|
+
}),
|
|
55
|
+
({ registry }) => Effect.gen(function* () {
|
|
56
|
+
const nonce = MutableRef.incrementAndGet(runNonce);
|
|
57
|
+
yield* executeModule(
|
|
58
|
+
registry,
|
|
59
|
+
"app.ts",
|
|
60
|
+
canonicalModuleUrl(`${appDir}/app.ts`, "app.ts").pipe(
|
|
61
|
+
Effect.flatMap((appUrl) => importModule(appEntrySpecifier(appUrl, nonce), "app.ts"))
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
const where = `tools/${file}`;
|
|
66
|
+
yield* executeModule(
|
|
67
|
+
registry,
|
|
68
|
+
where,
|
|
69
|
+
canonicalModuleUrl(`${appDir}/tools/${file}`, where).pipe(
|
|
70
|
+
Effect.flatMap((url) => importModule(`${fileURLToPath(url)}?m=${nonce}`, where))
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return registry;
|
|
75
|
+
}),
|
|
76
|
+
({ previous }) => Effect.sync(() => {
|
|
77
|
+
__leaveFile();
|
|
78
|
+
setActiveRegistry(previous);
|
|
79
|
+
})
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
var toolFiles = (appDir) => FileSystem.FileSystem.pipe(
|
|
83
|
+
Effect.flatMap((fs) => fs.readDirectory(`${appDir}/tools`)),
|
|
84
|
+
Effect.mapError((error) => new PublishError({ problems: [`tools/: ${error.message}`] })),
|
|
85
|
+
Effect.map((entries) => entries.filter((file) => file.endsWith(".ts")).sort())
|
|
86
|
+
);
|
|
87
|
+
var loadApp = (appDir) => Effect.gen(function* () {
|
|
88
|
+
const files = yield* toolFiles(appDir);
|
|
89
|
+
const registry = yield* executeModules(appDir, files);
|
|
90
|
+
const outcome = yield* Effect.promise(() => manifestOf(registry));
|
|
91
|
+
if (!outcome.ok) return yield* new PublishError({ problems: outcome.problems });
|
|
92
|
+
return { registry, result: outcome.result };
|
|
93
|
+
});
|
|
94
|
+
var manifest = (appDir) => Effect.map(loadApp(appDir), ({ result }) => result);
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
loadApp,
|
|
98
|
+
manifest
|
|
99
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// src/runtime/index.ts
|
|
2
|
+
var failure = (reason) => ({ ok: false, reason });
|
|
3
|
+
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
4
|
+
var isJsonValue = (value) => {
|
|
5
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
6
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
7
|
+
if (Array.isArray(value)) return value.every(isJsonValue);
|
|
8
|
+
if (typeof value !== "object") return false;
|
|
9
|
+
return Object.values(value).every(isJsonValue);
|
|
10
|
+
};
|
|
11
|
+
var standardJsonSchemaOf = (input) => "~standard" in input ? input["~standard"].jsonSchema : void 0;
|
|
12
|
+
var strictify = (node) => {
|
|
13
|
+
if (typeof node !== "object" || node === null) return node;
|
|
14
|
+
if (Array.isArray(node)) return node.map(strictify);
|
|
15
|
+
const object = { ...node };
|
|
16
|
+
if (object.type === "object" && object.additionalProperties === void 0)
|
|
17
|
+
object.additionalProperties = false;
|
|
18
|
+
for (const key of ["items", "anyOf", "oneOf", "allOf"])
|
|
19
|
+
if (key in object) object[key] = strictify(object[key]);
|
|
20
|
+
for (const key of ["properties", "$defs"])
|
|
21
|
+
if (object[key] !== void 0 && typeof object[key] === "object" && object[key] !== null)
|
|
22
|
+
object[key] = Object.fromEntries(
|
|
23
|
+
Object.entries(object[key]).map(([name, value]) => [
|
|
24
|
+
name,
|
|
25
|
+
strictify(value)
|
|
26
|
+
])
|
|
27
|
+
);
|
|
28
|
+
return object;
|
|
29
|
+
};
|
|
30
|
+
var compileInputSchema = (input, where) => {
|
|
31
|
+
const standard = standardJsonSchemaOf(input);
|
|
32
|
+
if ("~standard" in input && typeof standard?.input !== "function")
|
|
33
|
+
return {
|
|
34
|
+
problem: `${where}: input is a Standard Schema without the jsonSchema extension \u2014 use a library that implements it (zod 4+, effect Schema) or pass raw JSON schema`
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
const compiled = strictify(
|
|
38
|
+
typeof standard?.input === "function" ? standard.input({ target: "draft-2020-12" }) : structuredClone(input)
|
|
39
|
+
);
|
|
40
|
+
if (typeof compiled !== "object" || compiled === null || Array.isArray(compiled) || !isJsonValue(compiled))
|
|
41
|
+
throw new Error("expected a JSON object");
|
|
42
|
+
return { value: compiled };
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return {
|
|
45
|
+
problem: `${where}: input schema could not compile to JSON schema (${errorMessage(error)})`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var requirementKey = Symbol.for("executor/apps/requirement");
|
|
50
|
+
var requirementSpecOf = (value) => typeof value === "object" && value !== null && requirementKey in value ? value[requirementKey] : void 0;
|
|
51
|
+
var canonicalJson = (value) => {
|
|
52
|
+
const sortKeys = (node) => {
|
|
53
|
+
if (Array.isArray(node)) return node.map(sortKeys);
|
|
54
|
+
if (typeof node === "object" && node !== null)
|
|
55
|
+
return Object.fromEntries(
|
|
56
|
+
Object.entries(node).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, sortKeys(child)])
|
|
57
|
+
);
|
|
58
|
+
return node;
|
|
59
|
+
};
|
|
60
|
+
return JSON.stringify(sortKeys(value));
|
|
61
|
+
};
|
|
62
|
+
var collectRegistry = (registry) => {
|
|
63
|
+
const problems = registry.problems.map((problem) => problem.message);
|
|
64
|
+
const app = registry.app;
|
|
65
|
+
const claims = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const [origin, registered] of registry.services) {
|
|
67
|
+
if (registered.file !== "app.ts") continue;
|
|
68
|
+
const { decl } = registered;
|
|
69
|
+
claims.set(origin, {
|
|
70
|
+
origin,
|
|
71
|
+
reach: decl.reach ?? [],
|
|
72
|
+
params: decl.params ?? {},
|
|
73
|
+
auth: decl.auth ?? {}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const collectRequirement = (role, requirement, where) => {
|
|
77
|
+
const spec = requirementSpecOf(requirement);
|
|
78
|
+
if (spec === void 0) {
|
|
79
|
+
problems.push(`${where}: requirement '${role}' is not a service/requirement value`);
|
|
80
|
+
return void 0;
|
|
81
|
+
}
|
|
82
|
+
const origin = spec.service.origin;
|
|
83
|
+
if (!claims.has(origin))
|
|
84
|
+
problems.push(`${where}: requirement '${role}' names ${origin}, which app.ts does not declare`);
|
|
85
|
+
if (spec.grant !== void 0 && !(spec.grant in (spec.service.auth ?? {})))
|
|
86
|
+
problems.push(
|
|
87
|
+
`${where}: requirement '${role}' pins grant '${spec.grant}', not a auth method on ${origin}`
|
|
88
|
+
);
|
|
89
|
+
return {
|
|
90
|
+
role,
|
|
91
|
+
service: origin,
|
|
92
|
+
arity: spec.arity,
|
|
93
|
+
...spec.grant === void 0 ? {} : { grant: spec.grant },
|
|
94
|
+
...spec.description === void 0 ? {} : { description: spec.description }
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
if (app === void 0) problems.push("app.ts must call defineApp({...})");
|
|
98
|
+
else if (typeof app.description !== "string" || app.description.trim().length === 0)
|
|
99
|
+
problems.push("app.ts: app description must not be empty");
|
|
100
|
+
for (const [file, summary] of registry.toolFiles) {
|
|
101
|
+
if (summary.definitions > 1 && summary.hasUnnamedTool)
|
|
102
|
+
problems.push(`${file}: multi-tool files must name every tool`);
|
|
103
|
+
}
|
|
104
|
+
const tools = [];
|
|
105
|
+
if (app !== void 0) {
|
|
106
|
+
for (const [name, registered] of registry.tools) {
|
|
107
|
+
const { spec, file } = registered;
|
|
108
|
+
const address = `${app.key}/${name}`;
|
|
109
|
+
if (typeof spec.description !== "string" || spec.description.trim().length === 0)
|
|
110
|
+
problems.push(`${file}: tool '${name}' description must not be empty`);
|
|
111
|
+
const requires = Object.entries(spec.requirements ?? {}).flatMap(([role, requirement]) => {
|
|
112
|
+
const collected = collectRequirement(role, requirement, address);
|
|
113
|
+
return collected === void 0 ? [] : [collected];
|
|
114
|
+
}).sort((left, right) => left.role.localeCompare(right.role));
|
|
115
|
+
const compiled = compileInputSchema(spec.input, address);
|
|
116
|
+
if ("problem" in compiled) {
|
|
117
|
+
problems.push(compiled.problem);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
tools.push({
|
|
121
|
+
address,
|
|
122
|
+
name,
|
|
123
|
+
description: spec.description,
|
|
124
|
+
inputSchema: compiled.value,
|
|
125
|
+
annotations: spec.annotations ?? {},
|
|
126
|
+
requires,
|
|
127
|
+
file
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (problems.length > 0 || app === void 0) return { problems };
|
|
132
|
+
const services = [...claims.values()].sort(
|
|
133
|
+
(left, right) => left.origin.localeCompare(right.origin)
|
|
134
|
+
);
|
|
135
|
+
const sortedTools = [...tools].sort((left, right) => left.address.localeCompare(right.address));
|
|
136
|
+
const appResult = { key: app.key, label: app.label, description: app.description };
|
|
137
|
+
const manifest = { app: appResult, services, tools: sortedTools };
|
|
138
|
+
return { problems: [], manifest, canonical: canonicalJson(manifest) };
|
|
139
|
+
};
|
|
140
|
+
var digestOf = async (canonical) => {
|
|
141
|
+
const bytes = new Uint8Array(
|
|
142
|
+
await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical))
|
|
143
|
+
);
|
|
144
|
+
return Array.from(bytes.slice(0, 8), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
145
|
+
};
|
|
146
|
+
var manifestOf = async (registry) => {
|
|
147
|
+
const collected = collectRegistry(registry);
|
|
148
|
+
if (collected.problems.length > 0) return { ok: false, problems: collected.problems };
|
|
149
|
+
if (!("manifest" in collected))
|
|
150
|
+
return { ok: false, problems: ["validated registry projection unexpectedly missing"] };
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
result: { digest: `snap:${await digestOf(collected.canonical)}`, ...collected.manifest }
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
var standardValidateOf = (input) => "~standard" in input && typeof input["~standard"].validate === "function" ? input["~standard"].validate : void 0;
|
|
157
|
+
var parseInput = async (spec, input) => {
|
|
158
|
+
const validate = standardValidateOf(spec.input);
|
|
159
|
+
if (validate === void 0)
|
|
160
|
+
return { ok: false, reason: "input schema does not implement Standard Schema validate" };
|
|
161
|
+
try {
|
|
162
|
+
const result = await validate(input);
|
|
163
|
+
if (result.issues !== void 0)
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
reason: `invalid input (${result.issues.map((issue) => issue.message).join("; ")})`
|
|
167
|
+
};
|
|
168
|
+
return { ok: true, value: result.value };
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return { ok: false, reason: `input validation threw (${errorMessage(error)})` };
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
var callTool = async (registry, name, input, ctx) => {
|
|
174
|
+
const registered = registry.tools.get(name);
|
|
175
|
+
if (registered === void 0) return failure("unknown tool");
|
|
176
|
+
const parsed = await parseInput(registered.spec, input);
|
|
177
|
+
if (!parsed.ok) return parsed;
|
|
178
|
+
try {
|
|
179
|
+
const handler = registered.spec.handler;
|
|
180
|
+
return { ok: true, value: await handler(parsed.value, ctx) };
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return failure(`handler failed (${errorMessage(error)})`);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
var discoverTool = async (_registry, _name, _binding) => failure("no dynamic tools yet");
|
|
186
|
+
|
|
187
|
+
export {
|
|
188
|
+
manifestOf,
|
|
189
|
+
callTool,
|
|
190
|
+
discoverTool
|
|
191
|
+
};
|
package/dist/index.d.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/dist/manifest/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
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
|
-
import {
|
|
8
|
+
import { Effect, FileSystem } from "effect";
|
|
7
9
|
import { type Registry } from "../authoring/index";
|
|
8
10
|
import { PublishError, type AppManifestResult } from "./contract";
|
|
9
11
|
export * from "./contract";
|
|
@@ -13,6 +15,6 @@ export interface LoadedApp {
|
|
|
13
15
|
readonly result: AppManifestResult;
|
|
14
16
|
}
|
|
15
17
|
/** Loads an app once and returns both its live registry and manifest. @internal */
|
|
16
|
-
export declare const loadApp: (appDir: string) => Effect.Effect<LoadedApp, PublishError,
|
|
18
|
+
export declare const loadApp: (appDir: string) => Effect.Effect<LoadedApp, PublishError, FileSystem.FileSystem>;
|
|
17
19
|
/** Executes an app and returns its deterministic publish manifest. */
|
|
18
|
-
export declare const manifest: (appDir: string) => Effect.Effect<AppManifestResult, PublishError,
|
|
20
|
+
export declare const manifest: (appDir: string) => Effect.Effect<AppManifestResult, PublishError, FileSystem.FileSystem>;
|
package/dist/manifest/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loadApp,
|
|
3
3
|
manifest
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-646MS7DD.js";
|
|
5
5
|
import "../chunk-LWKQDDLZ.js";
|
|
6
6
|
import {
|
|
7
7
|
JsonSchemaObject,
|
|
8
8
|
PublishError
|
|
9
9
|
} from "../chunk-B7NEUIJL.js";
|
|
10
|
-
import "../chunk-
|
|
10
|
+
import "../chunk-OGCPVWAZ.js";
|
|
11
11
|
export {
|
|
12
12
|
JsonSchemaObject,
|
|
13
13
|
PublishError,
|
package/dist/runner/index.js
CHANGED
|
@@ -1,39 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loadApp
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-646MS7DD.js";
|
|
4
4
|
import "../chunk-LWKQDDLZ.js";
|
|
5
5
|
import "../chunk-B7NEUIJL.js";
|
|
6
6
|
import {
|
|
7
7
|
CallError
|
|
8
8
|
} from "../chunk-NFLNA2SC.js";
|
|
9
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
callTool,
|
|
11
|
+
discoverTool
|
|
12
|
+
} from "../chunk-OGCPVWAZ.js";
|
|
10
13
|
|
|
11
14
|
// src/runner/index.ts
|
|
12
15
|
import { Effect } from "effect";
|
|
13
|
-
var hasStandardValidate = (input) => "~standard" in input && typeof input["~standard"].validate === "function";
|
|
14
|
-
var parseInput = (name, spec, input) => {
|
|
15
|
-
if (!hasStandardValidate(spec.input))
|
|
16
|
-
return Effect.fail(
|
|
17
|
-
new CallError({ target: name, reason: "input schema does not implement Standard Schema validate" })
|
|
18
|
-
);
|
|
19
|
-
const standard = spec.input["~standard"];
|
|
20
|
-
return Effect.tryPromise({
|
|
21
|
-
try: () => Promise.resolve(standard.validate(input)),
|
|
22
|
-
catch: (error) => new CallError({
|
|
23
|
-
target: name,
|
|
24
|
-
reason: `input validation threw (${error instanceof Error ? error.message : String(error)})`
|
|
25
|
-
})
|
|
26
|
-
}).pipe(
|
|
27
|
-
Effect.flatMap(
|
|
28
|
-
(result) => result.issues === void 0 ? Effect.succeed(result.value) : Effect.fail(
|
|
29
|
-
new CallError({
|
|
30
|
-
target: name,
|
|
31
|
-
reason: `invalid input (${result.issues.map((issue) => issue.message).join("; ")})`
|
|
32
|
-
})
|
|
33
|
-
)
|
|
34
|
-
)
|
|
35
|
-
);
|
|
36
|
-
};
|
|
37
16
|
var localRunner = (appDir) => {
|
|
38
17
|
let cachedLoad;
|
|
39
18
|
const loaded = () => Effect.suspend(() => {
|
|
@@ -46,33 +25,25 @@ var localRunner = (appDir) => {
|
|
|
46
25
|
}).pipe(Effect.flatten)
|
|
47
26
|
);
|
|
48
27
|
});
|
|
49
|
-
const
|
|
28
|
+
const registryFor = (target) => loaded().pipe(
|
|
29
|
+
Effect.map(({ registry }) => registry),
|
|
50
30
|
Effect.mapError(
|
|
51
31
|
(error) => new CallError({ target, reason: `app failed to load (${error.message})` })
|
|
52
32
|
)
|
|
53
33
|
);
|
|
34
|
+
const settle = (target, outcome) => outcome.ok ? Effect.succeed(outcome.value) : Effect.fail(new CallError({ target, reason: outcome.reason }));
|
|
54
35
|
return {
|
|
55
36
|
manifest: () => Effect.map(loaded(), ({ result }) => result),
|
|
56
|
-
call: (name, input, ctx) =>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
parsed,
|
|
67
|
-
ctx
|
|
68
|
-
),
|
|
69
|
-
catch: (error) => new CallError({
|
|
70
|
-
target: name,
|
|
71
|
-
reason: `handler failed (${error instanceof Error ? error.message : String(error)})`
|
|
72
|
-
})
|
|
73
|
-
});
|
|
74
|
-
}),
|
|
75
|
-
discover: (name, _binding) => Effect.fail(new CallError({ target: name, reason: "no dynamic tools yet" }))
|
|
37
|
+
call: (name, input, ctx) => registryFor(name).pipe(
|
|
38
|
+
Effect.flatMap((registry) => Effect.promise(() => callTool(registry, name, input, ctx))),
|
|
39
|
+
Effect.flatMap((outcome) => settle(name, outcome))
|
|
40
|
+
),
|
|
41
|
+
discover: (name, binding) => registryFor(name).pipe(
|
|
42
|
+
Effect.flatMap(
|
|
43
|
+
(registry) => Effect.promise(() => discoverTool(registry, name, binding))
|
|
44
|
+
),
|
|
45
|
+
Effect.flatMap((outcome) => settle(name, outcome))
|
|
46
|
+
)
|
|
76
47
|
};
|
|
77
48
|
};
|
|
78
49
|
export {
|
|
@@ -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 {};
|
|
@@ -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"
|
|
@@ -58,4 +62,4 @@
|
|
|
58
62
|
"esbuild": "0.25",
|
|
59
63
|
"typescript": "5.9.3"
|
|
60
64
|
}
|
|
61
|
-
}
|
|
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";
|