@fungi.computer/forage 0.0.0-stage → 0.0.1
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 +24 -0
- package/README.md +223 -2
- package/THIRD_PARTY_NOTICES +13 -0
- package/dist/app-api.d.ts +62 -0
- package/dist/app-api.js +121 -0
- package/dist/catalog.d.ts +16 -0
- package/dist/catalog.js +108 -0
- package/dist/definitions.d.ts +16 -0
- package/dist/definitions.js +25 -0
- package/dist/describe.d.ts +4 -0
- package/dist/describe.js +66 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +12 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +16 -0
- package/dist/json-schema-boundary.d.ts +10 -0
- package/dist/json-schema-boundary.js +56 -0
- package/dist/json-schema-subset.d.ts +6 -0
- package/dist/json-schema-subset.js +77 -0
- package/dist/list.d.ts +8 -0
- package/dist/list.js +53 -0
- package/dist/normalization.d.ts +22 -0
- package/dist/normalization.js +109 -0
- package/dist/openapi.d.ts +106 -0
- package/dist/openapi.js +519 -0
- package/dist/rendering.d.ts +26 -0
- package/dist/rendering.js +341 -0
- package/dist/schemas.d.ts +231 -0
- package/dist/schemas.js +156 -0
- package/dist/search.d.ts +17 -0
- package/dist/search.js +258 -0
- package/docs/api/README.md +15 -0
- package/docs/api/index.md +859 -0
- package/docs/api/json-schema-subset.md +42 -0
- package/docs/api/openapi.md +308 -0
- package/package.json +71 -3
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
|
-
#
|
|
1
|
+
# Forage (`@fungi.computer/forage`)
|
|
2
2
|
|
|
3
|
-
|
|
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[];
|
package/dist/app-api.js
ADDED
|
@@ -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;
|
package/dist/catalog.js
ADDED
|
@@ -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;
|