@fungi.computer/forage 0.0.0-stage → 0.0.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/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jamstack Consulting LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ This package contains behavior re-expressed from @cloudflare/codemode (MIT).
24
+ Copyright (c) 2025 Cloudflare, Inc. https://github.com/cloudflare/agents
package/README.md CHANGED
@@ -1,3 +1,224 @@
1
- # Temporary Holding Version
1
+ # Forage (`@fungi.computer/forage`)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Forage lists, searches and describes a catalog of operations that a host has
4
+ already provided, so a caller or a model can find the right one by name or by
5
+ description. It helps a caller discover a capability. It does not execute that
6
+ capability or grant permission to use it.
7
+
8
+ ## Install or set up
9
+
10
+ ```sh
11
+ npm install @fungi.computer/forage
12
+ ```
13
+
14
+ The package ships ESM only and depends on Zod, YAML, JSON Schema Ref Parser and
15
+ [App Release](../puffball/README.md) at runtime. Each operation's input and
16
+ output is any schema that implements Standard JSON Schema, such as a Zod 4
17
+ schema; Forage only renders the JSON Schema it describes. Its discovery
18
+ entrypoint is `@fungi.computer/forage`; host-only OpenAPI intake is
19
+ `@fungi.computer/forage/openapi`. The pure JSON-Schema subset decoder is
20
+ `@fungi.computer/forage/json-schema-subset`: `jsonSchemaSubset()` validates
21
+ remote schemas before the shared `jsonSchemaToZod` compiler reads them. The
22
+ subset rejects references, regular-expression constraints and reserved property
23
+ names anywhere in the schema or JSON values. The decoder refuses depth over 32
24
+ and more than 50,000 expanded nodes before recursive decoding; callers bound
25
+ transport bytes separately.
26
+
27
+ ## Quick start
28
+
29
+ This illustrative example creates an immutable catalog, searches it, and
30
+ describes one operation:
31
+
32
+ ```ts
33
+ import { Forage } from "@fungi.computer/forage";
34
+ import { z } from "zod";
35
+
36
+ const catalog = Forage.createCatalog({
37
+ revision: "run-1",
38
+ operations: [
39
+ {
40
+ path: "files.read",
41
+ description: "Read a file",
42
+ input: z.object({ path: z.string() }),
43
+ },
44
+ ],
45
+ });
46
+
47
+ const matches = catalog.search({ query: "read" });
48
+ const description = catalog.describe({ path: "files.read" });
49
+ const firstPage = catalog.list({ limit: 25 });
50
+ ```
51
+
52
+ The catalog tests exercise creation, search, description, and stable stamps in
53
+ [`test/catalog.test.ts`](test/catalog.test.ts).
54
+
55
+ ## Core concepts
56
+
57
+ A catalog has a caller-supplied revision and a set of operations. Each operation
58
+ has a unique dotted path, an input schema, and optional description, capability
59
+ notes, instructions, and output schema. Forage validates the catalog, orders its
60
+ operations deterministically, and prepares descriptions once when the catalog is
61
+ created. To change the catalog, create a new revision instead of mutating the
62
+ existing one.
63
+
64
+ `search` ranks matching operations. `describe` returns a bounded TypeScript
65
+ description for one operation or a namespace. Operation descriptions also
66
+ include `inputSchema`, projected from the same validator through Standard JSON
67
+ Schema, so callers can see length, range and array bounds that TypeScript cannot
68
+ express. Paths may have any namespace depth; descriptions declare a callable
69
+ nested `env` with the operation's input and output types. Generated type aliases
70
+ separate every path segment with `$`: `computer.echo` declares
71
+ `computer$echo_Input` and `computer$echo_Output`, and the nested
72
+ `computers.local.files.read` declares `computers$local$files$read_Input` and
73
+ `computers$local$files$read_Output`. This keeps paths with underscores distinct
74
+ from nested paths. The same rule applies at every depth. `stamp()` returns a
75
+ stable identity for the sorted catalog metadata, including schema projections,
76
+ capability notes and instructions.
77
+
78
+ ## Exhaustive listing
79
+
80
+ `forage.list({limit?, cursor?, namespace?})` (or `catalog.list` directly)
81
+ enumerates every registered operation exactly once. Ordering is by namespace
82
+ (the first path segment), then full operation path, compared by UTF-16 code
83
+ units, not locale. Registration order never changes it. Equivalent operations
84
+ are not merged into peers, unlike search.
85
+
86
+ Each page has `results`, `total`, `catalogStamp` and `nextCursor`. Results use
87
+ the same summary schema as search (path, description, sample and capability
88
+ notes); score is zero because listing does not rank matches. The default and
89
+ maximum limit is 50; the minimum is 1. Follow `nextCursor` until it is null.
90
+ Total counts all operations under the selected namespace, including its nested
91
+ namespaces, not just this page. For example, `namespace: "apps.bazaar"` also
92
+ selects `apps.bazaar.windows.open`. An absent namespace means every namespace;
93
+ an unknown namespace returns an empty page with total zero.
94
+
95
+ Cursors are opaque to callers and belong to one catalog stamp and namespace
96
+ filter. Reusing a cursor after either changes fails explicitly. Begin a fresh
97
+ listing instead. A host that narrows a guest's bound operations filters the
98
+ existing summaries and uses the same `listOperations` owner before paging. It
99
+ does not introduce another registry or leak unbound paths. Search and describe
100
+ keep their existing contracts.
101
+
102
+ ## OpenAPI intake
103
+
104
+ Import `normalizeOpenApi` and `parseOpenApiTable` from
105
+ `@fungi.computer/forage/openapi` on the host. This entrypoint uses the
106
+ Node-oriented reference resolver; neutral Worker/guest discovery consumers use
107
+ the core entrypoint without importing intake.
108
+
109
+ `normalizeOpenApi(specText, {authParameter?})` accepts owner-supplied OpenAPI
110
+ 3.0/3.1 JSON or YAML. It returns server and authentication hints plus supported
111
+ operations and explicit unsupported-operation reasons. It performs no fetch,
112
+ credential access or execution. Swagger 2 must be converted first.
113
+
114
+ Intake is bounded to 2 MiB, depth 32, 50,000 nodes and 500 operations. YAML
115
+ aliases and external references are refused. Local references are resolved by
116
+ JSON Schema Ref Parser with file and HTTP resolvers disabled; recursive schemas
117
+ are unsupported. The expansion budget counts every shared-reference occurrence
118
+ before recursive schema decoding or compilation. Input and declared JSON success
119
+ schemas use the same `jsonSchemaToZod` compiler as App methods. Schemas with
120
+ constraints the supported compiler subset cannot preserve are unsupported,
121
+ rather than widened.
122
+
123
+ Operations expose scalar path/query/header parameters with default OpenAPI
124
+ serialization and an optional JSON `body`. Other serialization styles, body
125
+ media, referenced Path Items and TRACE are unsupported. Required path parameters
126
+ must match the path. Reserved transport headers, declared API-key parameters and
127
+ an owner-provided `authParameter` cannot become guest input. GET/HEAD/OPTIONS
128
+ are reads; other supported methods are writes. Sanitized method names are unique
129
+ and reserve `describe`, `search`, `then` and JavaScript object member names.
130
+ Paths cannot change the origin, traverse dot segments or contain query/fragment
131
+ syntax or encoded separators. Header names follow RFC token grammar and exclude
132
+ transport, forwarding, credential and method-override headers. Prototype names
133
+ are refused before decoding. Regex keywords (`pattern`, `patternProperties`) and
134
+ all `format` constraints are unsupported. OpenAPI 3.0 `nullable` is supported;
135
+ OpenAPI 3.1 must use null types instead. Unknown schema keywords are refused.
136
+
137
+ Provider-authored descriptions/schema annotations are bounded to 2,048
138
+ characters, titles to 200, and tags to 16 entries of 64 characters each.
139
+ Operations mark their text as `third_party`; hosts must frame it as data. Server
140
+ hints require HTTPS without userinfo; relative hints remain relative to the
141
+ retrieved spec. Hints confer no network admission and do not select a credential
142
+ destination: that belongs to the private Veil execution door.
143
+
144
+ `parseOpenApiTable(selectedOperations)` decodes 1–64 supported methods before
145
+ storage, rejects duplicate names and tables above 512 KiB, and returns a stable
146
+ SHA-256 digest. It stores normalized schemas and call shapes, never raw specs or
147
+ credentials. A compiler result with no declared output constraint still needs
148
+ the execution owner's bounded JSON output decoder. Neither intake nor table
149
+ validation grants execution permission or validates a server's network
150
+ admission. A connector owner selects from its server-built preview by HTTP
151
+ method + path, never client-supplied operation bodies or generated method names;
152
+ collision suffixes can change when the spec changes.
153
+
154
+ The pure intake and selected-table laws are exercised in
155
+ [`test/openapi.test.ts`](test/openapi.test.ts).
156
+
157
+ ## API reference
158
+
159
+ ### App backend methods
160
+
161
+ `appApiOperations(bindingName, api)` projects a decoded App Release API into
162
+ operations such as `PRINTS.generate`, called as `env.PRINTS.generate()`; a
163
+ dotted method `desktop.arrange` is `env.TOURTOIS.desktop.arrange()`. The App
164
+ author generates that API through Agar during the build; Forage uses the same
165
+ declaration instead of a second method list. The host supplies the
166
+ installation's binding name and can project the same API under an explicit role
167
+ alias.
168
+
169
+ `appMethodSchemas(api)` supplies the same converted input and output schemas,
170
+ with the declared description, effect and requirements, to execution owners.
171
+ `appToolSchemas(api)` uses the same converter after excluding methods marked
172
+ `hidden: true`. `appApiOperations` and host Agent/MCP discovery share this
173
+ filtered projection. `appMethodSchemas` keeps every declared method for direct
174
+ RPC validators. Converting a declaration grants no authority to execute it.
175
+
176
+ Each visible method is searchable and has input and output types in `describe`.
177
+ Describing `PRINTS` returns the callable nested `env` declaration with
178
+ asynchronous methods. The projection includes declared effects and capability
179
+ requirements as discovery notes. It supplies no execution handler or permission
180
+ grant.
181
+
182
+ The generated-declaration round trip and nested descriptions are exercised by
183
+ Hub’s `app-api-discovery` test.
184
+
185
+ See the generated [Forage API reference](docs/api/README.md). It covers
186
+ `Forage.createCatalog`, the `createCatalog` function, operation definitions, and
187
+ the search and description result types.
188
+
189
+ ## Examples and checks
190
+
191
+ The existing catalog suite covers validation, deterministic search and
192
+ description, and catalog stamps. Run it with:
193
+
194
+ ```sh
195
+ pnpm --filter @fungi.computer/forage test
196
+ ```
197
+
198
+ ## Limits and errors
199
+
200
+ Forage is immutable discovery data. It has no executor, credentials,
201
+ authorization state, refresh operation, or host client. A result from `search`
202
+ or `describe` does not run the operation. An execution owner must make that
203
+ decision separately.
204
+
205
+ Catalogs must contain at least one operation and cannot repeat an operation
206
+ path. Forage bounds metadata and rendered descriptions. Invalid or oversized
207
+ input throws `ForageError`. The stable stamp identifies the immutable discovery
208
+ snapshot, not authority to execute its operations.
209
+
210
+ ## Contribute and license
211
+
212
+ Forage is MIT licensed. See [`LICENSE`](LICENSE). Its retained source
213
+ attribution and notice are in [`THIRD_PARTY_NOTICES`](THIRD_PARTY_NOTICES).
214
+
215
+ ## Dynamic App tool schemas
216
+
217
+ `appToolFamilySchemas(api, reply)` validates a dynamic listing through its
218
+ signed method output, then compiles each row's input and output with the
219
+ ordinary App schema decoder. It returns the family name, call method, declared
220
+ effect and requirements, plus rows and their validators. An `unindexed` listing
221
+ has no tools. App-owned selectors remain row data; the host constructs the
222
+ invocation through that App's call owner. The projection supplies no handlers,
223
+ binding defaults or authority. SDK modules and labelled MCP tools can consume
224
+ the same rows and validators.
@@ -0,0 +1,13 @@
1
+ Forage re-expresses discovery behavior from @cloudflare/codemode.
2
+ Copyright (c) 2025 Cloudflare, Inc.; MIT License.
3
+ Source: https://github.com/cloudflare/agents
4
+
5
+ YAML 2.9.0 (yaml): ISC License.
6
+ Copyright Eemeli Aro <eemeli@gmail.com>.
7
+ Source: https://github.com/eemeli/yaml
8
+ License: https://github.com/eemeli/yaml/blob/v2.9.0/LICENSE
9
+
10
+ JSON Schema Ref Parser 16.0.3: MIT License.
11
+ Copyright (c) 2015 James Messinger.
12
+ Source: https://github.com/APIDevTools/json-schema-ref-parser
13
+ License: https://github.com/APIDevTools/json-schema-ref-parser/blob/v16.0.3/LICENSE
@@ -0,0 +1,62 @@
1
+ import { z } from "zod";
2
+ import type { AppApi } from "@fungi.computer/puffball";
3
+ import type { Operation } from "./schemas.js";
4
+ /** The shared JSON-Schema compiler for App methods and OpenAPI operations. */
5
+ export declare function jsonSchemaToZod(source: unknown, defaultTarget?: "draft-2020-12" | "openapi-3.0"): z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
6
+ /** Compile App validators and their Standard JSON Schema descriptions.
7
+ * Validation enforces both the declaration and JSON-only values; discovery
8
+ * describes the declaration without adding JSON guard nesting.
9
+ * Agar's emitted output already includes Pending for opted-in methods. */
10
+ export declare function appMethodSchemas(api: AppApi): {
11
+ [k: string]: {
12
+ readonly description: string;
13
+ readonly effect: "write" | "external" | "read";
14
+ readonly requires: readonly ("agent.list" | "agent.manage" | "agent.rename" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage")[];
15
+ readonly pending: true | undefined;
16
+ readonly input: Pick<z.ZodType<z.core.util.JSONType, unknown, z.core.$ZodTypeInternals<z.core.util.JSONType, unknown>>, "~standard" | "parse" | "safeParse">;
17
+ readonly output: Pick<z.ZodType<z.core.util.JSONType, unknown, z.core.$ZodTypeInternals<z.core.util.JSONType, unknown>>, "~standard" | "parse" | "safeParse">;
18
+ };
19
+ };
20
+ /** Compile discoverable App methods only; direct RPC validation uses appMethodSchemas. */
21
+ export declare function appToolSchemas(api: AppApi): {
22
+ [k: string]: {
23
+ readonly description: string;
24
+ readonly effect: "write" | "external" | "read";
25
+ readonly requires: readonly ("agent.list" | "agent.manage" | "agent.rename" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage")[];
26
+ readonly pending: true | undefined;
27
+ readonly input: Pick<z.ZodType<z.core.util.JSONType, unknown, z.core.$ZodTypeInternals<z.core.util.JSONType, unknown>>, "~standard" | "parse" | "safeParse">;
28
+ readonly output: Pick<z.ZodType<z.core.util.JSONType, unknown, z.core.$ZodTypeInternals<z.core.util.JSONType, unknown>>, "~standard" | "parse" | "safeParse">;
29
+ };
30
+ };
31
+ /** Decode the signed listing before compiling dynamic discovery schemas.
32
+ * Rows retain App-owned selectors as data. Hosts supply call admission and
33
+ * invocation arguments; this projection supplies neither defaults nor handlers. */
34
+ export declare function appToolFamilySchemas(api: AppApi, reply: unknown): {
35
+ name: string;
36
+ call: string;
37
+ effect: "write" | "external" | "read";
38
+ requires: readonly ("agent.list" | "agent.manage" | "agent.rename" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage")[];
39
+ kind: "ready" | "unindexed";
40
+ tools: {
41
+ row: {
42
+ [x: string]: z.core.util.JSONType;
43
+ tool: string;
44
+ title: string;
45
+ description: string;
46
+ input: Record<string, z.core.util.JSONType>;
47
+ output: Record<string, z.core.util.JSONType>;
48
+ call: Record<string, z.core.util.JSONType>;
49
+ };
50
+ description: string;
51
+ input: Pick<z.ZodType<z.core.util.JSONType, unknown, z.core.$ZodTypeInternals<z.core.util.JSONType, unknown>>, "~standard" | "parse" | "safeParse">;
52
+ output: Pick<z.ZodType<z.core.util.JSONType, unknown, z.core.$ZodTypeInternals<z.core.util.JSONType, unknown>>, "~standard" | "parse" | "safeParse">;
53
+ }[];
54
+ } | null;
55
+ /**
56
+ * Project an already decoded Release API into discovery data with the same
57
+ * paths the Agent catalog uses (`PRINTS.generate`, called as
58
+ * `env.PRINTS.generate()`). The introduction owner supplies the installation's
59
+ * binding name; this function grants no access and supplies no execution
60
+ * handler. Role aliases can project the same API.
61
+ */
62
+ export declare function appApiOperations(name: string, api: AppApi): readonly Operation[];
@@ -0,0 +1,121 @@
1
+ import { z } from "zod";
2
+ const bindingName = z
3
+ .string()
4
+ .min(1)
5
+ .max(75)
6
+ .regex(/^[A-Za-z][A-Za-z0-9_]*$/);
7
+ const jsonSchema = z.union([z.record(z.string(), z.json()), z.boolean()]);
8
+ /** The shared JSON-Schema compiler for App methods and OpenAPI operations. */
9
+ export function jsonSchemaToZod(source, defaultTarget = "draft-2020-12") {
10
+ return z.fromJSONSchema(jsonSchema.parse(source), { defaultTarget });
11
+ }
12
+ /** Validate JSON with Zod; describe the declaration without the redundant JSON guard. */
13
+ function appMethodSchema(source) {
14
+ const declared = jsonSchemaToZod(source);
15
+ const validator = z.intersection(declared, z.json());
16
+ return {
17
+ parse: (value, options) => validator.parse(value, options),
18
+ safeParse: (value, options) => validator.safeParse(value, options),
19
+ "~standard": {
20
+ ...validator["~standard"],
21
+ jsonSchema: declared["~standard"].jsonSchema,
22
+ },
23
+ };
24
+ }
25
+ /** Compile App validators and their Standard JSON Schema descriptions.
26
+ * Validation enforces both the declaration and JSON-only values; discovery
27
+ * describes the declaration without adding JSON guard nesting.
28
+ * Agar's emitted output already includes Pending for opted-in methods. */
29
+ export function appMethodSchemas(api) {
30
+ return Object.fromEntries(Object.entries(api.methods).map(([method, declaration]) => [
31
+ method,
32
+ {
33
+ description: declaration.description,
34
+ effect: declaration.effect,
35
+ requires: declaration.requires,
36
+ pending: declaration.pending,
37
+ input: appMethodSchema(declaration.input),
38
+ output: appMethodSchema(declaration.output),
39
+ },
40
+ ]));
41
+ }
42
+ /** Compile discoverable App methods only; direct RPC validation uses appMethodSchemas. */
43
+ export function appToolSchemas(api) {
44
+ return appMethodSchemas({
45
+ ...api,
46
+ methods: Object.fromEntries(Object.entries(api.methods).filter(([, method]) => method.hidden !== true)),
47
+ });
48
+ }
49
+ const familyRow = z
50
+ .object({
51
+ tool: bindingName,
52
+ title: z.string().max(120),
53
+ description: z.string().max(1000),
54
+ input: z.record(z.string(), z.json()),
55
+ output: z.record(z.string(), z.json()),
56
+ // Opaque extra arguments the declaring App wants merged with the
57
+ // caller's own `input` when a host dispatches to the family's `call`
58
+ // method (e.g. which of its own resources this row identifies, and any
59
+ // App-to-App bindings only the declaring App can resolve). Required, so
60
+ // a row that needs none still says so with `{}`. A host spreads this
61
+ // verbatim; it never reads into or defaults any of it.
62
+ call: z.record(z.string(), z.json()),
63
+ })
64
+ .catchall(z.json());
65
+ const familyInventory = z.discriminatedUnion("kind", [
66
+ z.strictObject({ kind: z.literal("unindexed") }),
67
+ z.strictObject({
68
+ kind: z.literal("ready"),
69
+ tools: z
70
+ .array(familyRow)
71
+ .max(100)
72
+ .refine((rows) => new Set(rows.map((row) => row.tool)).size === rows.length, { message: "Tool family names must be distinct" }),
73
+ }),
74
+ ]);
75
+ /** Decode the signed listing before compiling dynamic discovery schemas.
76
+ * Rows retain App-owned selectors as data. Hosts supply call admission and
77
+ * invocation arguments; this projection supplies neither defaults nor handlers. */
78
+ export function appToolFamilySchemas(api, reply) {
79
+ const family = api.toolFamily;
80
+ if (family === undefined)
81
+ return null;
82
+ const list = api.methods[family.list];
83
+ const call = api.methods[family.call];
84
+ if (list === undefined || call === undefined)
85
+ throw new Error("Tool family references undeclared methods");
86
+ const inventory = familyInventory.parse(appMethodSchema(list.output).parse(reply));
87
+ return {
88
+ name: family.name,
89
+ call: family.call,
90
+ effect: call.effect,
91
+ requires: call.requires,
92
+ kind: inventory.kind,
93
+ tools: inventory.kind === "unindexed"
94
+ ? []
95
+ : inventory.tools.map((row) => ({
96
+ row,
97
+ description: [row.title, row.description]
98
+ .filter((text) => text.length > 0)
99
+ .join("\n"),
100
+ input: appMethodSchema(row.input),
101
+ output: appMethodSchema(row.output),
102
+ })),
103
+ };
104
+ }
105
+ /**
106
+ * Project an already decoded Release API into discovery data with the same
107
+ * paths the Agent catalog uses (`PRINTS.generate`, called as
108
+ * `env.PRINTS.generate()`). The introduction owner supplies the installation's
109
+ * binding name; this function grants no access and supplies no execution
110
+ * handler. Role aliases can project the same API.
111
+ */
112
+ export function appApiOperations(name, api) {
113
+ const binding = bindingName.parse(name);
114
+ return Object.entries(appToolSchemas(api)).map(([method, declaration]) => ({
115
+ path: `${binding}.${method}`,
116
+ description: declaration.description,
117
+ input: declaration.input,
118
+ output: declaration.output,
119
+ capabilityNotes: `effect: ${declaration.effect}; requires: ${declaration.requires.join(", ") || "none"}`,
120
+ }));
121
+ }
@@ -0,0 +1,16 @@
1
+ import { type CatalogInput, type ForageDescribeInput, type ForageSearchInput, type SearchOutput, type DescribeOutput, type ForageListInput, type ListOutput } from "./schemas.js";
2
+ /** Immutable public facade for one prepared catalog revision. */
3
+ export type ForageCatalog = Readonly<{
4
+ /** Immutable revision supplied when the catalog was created. */
5
+ revision: string;
6
+ /** Deterministically rank provisioned operations for a bounded query. */
7
+ search: (rawInput: ForageSearchInput) => SearchOutput;
8
+ /** Render bounded types and operation input constraints from the validator. */
9
+ describe: (rawInput: ForageDescribeInput) => DescribeOutput;
10
+ /** Enumerate every operation exactly once in stable namespace/path order. */
11
+ list: (rawInput: ForageListInput) => ListOutput;
12
+ /** Return the stable identity stamp for this catalog revision. */
13
+ stamp: () => string;
14
+ }>;
15
+ /** Construct one immutable catalog revision with a narrow public facade. */
16
+ export declare function createCatalog(input: CatalogInput): ForageCatalog;
@@ -0,0 +1,108 @@
1
+ import { ForageError } from "./errors.js";
2
+ import { catalogInputSchema, forageDescribeInput, forageSearchInput, maximumDescriptionTypes, } from "./schemas.js";
3
+ import { describeCatalog } from "./describe.js";
4
+ import { catalogStamp, prepareOperation, searchCatalog, } from "./search.js";
5
+ import { compareCodeUnits } from "./normalization.js";
6
+ import { listOperations } from "./list.js";
7
+ import { renderNamespaceIndex, renderOperationTypes, schemaProjectionSize, } from "./rendering.js";
8
+ const maximumCatalogProjectionBytes = 1_000_000;
9
+ function parseCatalog(input) {
10
+ const parsed = catalogInputSchema.safeParse(input);
11
+ if (!parsed.success) {
12
+ throw new ForageError("invalid_input", parsed.error.message);
13
+ }
14
+ if (parsed.data.operations.length === 0) {
15
+ throw new ForageError("empty_catalog", "Catalog must contain an operation");
16
+ }
17
+ const operations = [...parsed.data.operations].sort((left, right) => compareCodeUnits(left.path, right.path));
18
+ const paths = new Set();
19
+ let projectionBytes = 0;
20
+ for (const operation of operations) {
21
+ if (paths.has(operation.path)) {
22
+ throw new ForageError("duplicate_operation", `Duplicate operation path: ${operation.path}`);
23
+ }
24
+ paths.add(operation.path);
25
+ const inputProjection = schemaProjectionSize(operation.input, "input");
26
+ if (inputProjection === undefined) {
27
+ throw new ForageError("invalid_input", `Schema projection is too large: ${operation.path}`);
28
+ }
29
+ const outputProjection = operation.output
30
+ ? schemaProjectionSize(operation.output, "output")
31
+ : 0;
32
+ if (outputProjection === undefined) {
33
+ throw new ForageError("invalid_input", `Schema output projection is too large: ${operation.path}`);
34
+ }
35
+ projectionBytes +=
36
+ inputProjection + outputProjection + operation.path.length;
37
+ projectionBytes += operation.description?.length ?? 0;
38
+ projectionBytes += operation.capabilityNotes?.length ?? 0;
39
+ projectionBytes += operation.instructions?.length ?? 0;
40
+ if (projectionBytes > maximumCatalogProjectionBytes) {
41
+ throw new ForageError("invalid_input", "Catalog metadata exceeds the bounded projection");
42
+ }
43
+ }
44
+ const preparedOperations = operations.map((operation) => prepareOperation(operation));
45
+ const namespaces = new Map();
46
+ for (const operation of preparedOperations) {
47
+ const namespace = operation.path.slice(0, operation.path.lastIndexOf("."));
48
+ const group = namespaces.get(namespace) ?? [];
49
+ group.push(operation);
50
+ namespaces.set(namespace, group);
51
+ }
52
+ for (const [namespace, group] of namespaces) {
53
+ const rendered = group.map((operation) => operation.rendered);
54
+ const index = renderNamespaceIndex(rendered);
55
+ const full = renderOperationTypes(rendered);
56
+ if (index.length > maximumDescriptionTypes ||
57
+ full.length > maximumDescriptionTypes) {
58
+ throw new ForageError("invalid_input", `Namespace description exceeds the bounded output: ${namespace}`);
59
+ }
60
+ }
61
+ return {
62
+ revision: parsed.data.revision,
63
+ operations: preparedOperations,
64
+ };
65
+ }
66
+ /** Construct one immutable catalog revision with a narrow public facade. */
67
+ export function createCatalog(input) {
68
+ const parsed = parseCatalog(input);
69
+ const operations = Object.freeze(parsed.operations);
70
+ const stamp = catalogStamp(operations);
71
+ const summaries = operations.map((operation) => {
72
+ const summary = { path: operation.path, score: 0 };
73
+ if (operation.description !== undefined)
74
+ summary.description = operation.description;
75
+ if (operation.sample !== undefined)
76
+ summary.sample = operation.sample;
77
+ if (operation.capabilityNotes !== undefined)
78
+ summary.capabilityNotes = operation.capabilityNotes;
79
+ return summary;
80
+ });
81
+ const search = (rawInput) => {
82
+ const parsed = forageSearchInput.safeParse(rawInput);
83
+ if (!parsed.success) {
84
+ throw new ForageError("invalid_input", parsed.error.message);
85
+ }
86
+ if (parsed.data.query.trim().length === 0) {
87
+ throw new ForageError("empty_query", "Search query must not be empty");
88
+ }
89
+ return searchCatalog(operations, parsed.data.query, parsed.data.maxResults, stamp);
90
+ };
91
+ const describe = (rawInput) => {
92
+ const parsed = forageDescribeInput.safeParse(rawInput);
93
+ if (!parsed.success) {
94
+ throw new ForageError("invalid_input", parsed.error.message);
95
+ }
96
+ if (parsed.data.path.trim().length === 0) {
97
+ throw new ForageError("empty_query", "Description target must not be empty");
98
+ }
99
+ return describeCatalog(operations, parsed.data);
100
+ };
101
+ return Object.freeze({
102
+ revision: parsed.revision,
103
+ search,
104
+ describe,
105
+ list: (input) => listOperations(summaries, stamp, input),
106
+ stamp: () => stamp,
107
+ });
108
+ }
@@ -0,0 +1,16 @@
1
+ import { describeOutputSchema, forageDescribeInput, forageSearchInput, searchOutputSchema, forageListInput, listOutputSchema } from "./schemas.js";
2
+ /** Structural operation definition consumed by a capability composer. */
3
+ export type ForageDefinition = Readonly<{
4
+ /** Pure catalog lookup; the enclosing Mycelium execute program is still unsafe. */
5
+ replay: "safe";
6
+ path: string;
7
+ description: string;
8
+ input: typeof forageSearchInput | typeof forageDescribeInput | typeof forageListInput;
9
+ output: typeof searchOutputSchema | typeof describeOutputSchema | typeof listOutputSchema;
10
+ }>;
11
+ /** The search operation has schema metadata only; it has no host behavior. */
12
+ export declare const forageSearchDefinition: ForageDefinition;
13
+ /** The description operation has schema metadata only; it has no host behavior. */
14
+ export declare const forageDescribeDefinition: ForageDefinition;
15
+ /** Pure exhaustive catalog listing; no execution or authority is granted. */
16
+ export declare const forageListDefinition: ForageDefinition;
@@ -0,0 +1,25 @@
1
+ import { describeOutputSchema, forageDescribeInput, forageSearchInput, searchOutputSchema, forageListInput, listOutputSchema, } from "./schemas.js";
2
+ /** The search operation has schema metadata only; it has no host behavior. */
3
+ export const forageSearchDefinition = Object.freeze({
4
+ replay: "safe",
5
+ path: "forage.search",
6
+ description: "Search operation names and descriptions with short keywords, such as read or shell. If no results match, retry with one relevant keyword. Describe known operation paths directly with forage.describe.",
7
+ input: forageSearchInput,
8
+ output: searchOutputSchema,
9
+ });
10
+ /** The description operation has schema metadata only; it has no host behavior. */
11
+ export const forageDescribeDefinition = Object.freeze({
12
+ replay: "safe",
13
+ path: "forage.describe",
14
+ description: "Describe a capability",
15
+ input: forageDescribeInput,
16
+ output: describeOutputSchema,
17
+ });
18
+ /** Pure exhaustive catalog listing; no execution or authority is granted. */
19
+ export const forageListDefinition = Object.freeze({
20
+ replay: "safe",
21
+ path: "forage.list",
22
+ description: "List every available operation in stable namespace then path order (UTF-16 code units). Use limit (1–50) and nextCursor to page without duplicates, optionally filtering by exact namespace. total counts all operations in that filter; summaries match search, with score 0 and no peer merging. A cursor belongs to this catalog and filter; describe paths before calling them.",
23
+ input: forageListInput,
24
+ output: listOutputSchema,
25
+ });
@@ -0,0 +1,4 @@
1
+ import { type DescribeOutput, type ForageDescribeInput } from "./schemas.js";
2
+ import type { PreparedOperation } from "./search.js";
3
+ /** Pure deterministic description of an operation or its namespace. */
4
+ export declare function describeCatalog(operations: readonly PreparedOperation[], rawInput: ForageDescribeInput): DescribeOutput;