Concepts
Pure immutable Zod-typed capability discovery
Forage lists, searches and describes a catalog of operations that a host has already provided, so a caller or a model can find the right one by name or by description. It helps a caller discover a capability. It does not execute that capability or grant permission to use it.
Core concepts
Section titled “Core concepts”A catalog has a caller-supplied revision and a set of operations. Each operation
has a unique dotted path, an input schema, and optional description, capability
notes, instructions, and output schema. Forage validates the catalog, orders its
operations deterministically, and prepares descriptions once when the catalog is
created. To change the catalog, create a new revision instead of mutating the
existing one. Every operation must fit one complete description, and the
catalog’s aggregate metadata remains bounded. A whole namespace can exceed a
description response without making its individually describable operations
unavailable. Use paged list({ namespace }) and describe one operation or
select methods when a broad namespace description exceeds the response bound.
This applies equally to flat namespaces and broad parents of nested namespaces;
responses are never truncated and the response bound is unchanged.
search ranks matching operations. describe returns a bounded TypeScript
description for one operation or a namespace. Operation descriptions also
include inputSchema, projected from the same validator through Standard JSON
Schema, so callers can see length, range and array bounds that TypeScript cannot
express. Paths may have any namespace depth; descriptions declare a callable
nested env with the operation’s input and output types. Generated type aliases
separate every path segment with $: computer.echo declares
computer$echo_Input and computer$echo_Output, and the nested
computers.local.files.read declares computers$local$files$read_Input and
computers$local$files$read_Output. This keeps paths with underscores distinct
from nested paths. The same rule applies at every depth. stamp() returns a
stable identity for the sorted catalog metadata, including schema projections,
capability notes and instructions.
Exhaustive listing
Section titled “Exhaustive listing”forage.list({limit?, cursor?, namespace?}) (or catalog.list directly)
enumerates every registered operation exactly once. Ordering is by namespace
(the first path segment), then full operation path, compared by UTF-16 code
units, not locale. Registration order never changes it. Equivalent operations
are not merged into peers, unlike search.
Each page has results, total, catalogStamp and nextCursor. Results use
the same summary schema as search (path, description, sample and capability
notes); score is zero because listing does not rank matches. The default and
maximum limit is 50; the minimum is 1. Follow nextCursor until it is null.
Total counts all operations under the selected namespace, including its nested
namespaces, not just this page. For example, namespace: "apps.bazaar" also
selects apps.bazaar.windows.open. An absent namespace means every namespace;
an unknown namespace returns an empty page with total zero.
Cursors are opaque to callers and belong to one catalog stamp and namespace
filter. Reusing a cursor after either changes fails explicitly. Begin a fresh
listing instead. A host that narrows a guest’s bound operations filters the
existing summaries and uses the same listOperations owner before paging. It
does not introduce another registry or leak unbound paths. Search and describe
keep their existing contracts.
OpenAPI intake
Section titled “OpenAPI intake”Import normalizeOpenApi and parseOpenApiTable from
@fungi.computer/forage/openapi on the host. This entrypoint uses the
Node-oriented reference resolver; neutral Worker/guest discovery consumers use
the core entrypoint without importing intake.
normalizeOpenApi(specText, {authParameter?}) accepts owner-supplied OpenAPI
3.0/3.1 JSON or YAML. It returns server and authentication hints plus supported
operations and explicit unsupported-operation reasons. It performs no fetch,
credential access or execution. Swagger 2 must be converted first.
Intake is bounded to 2 MiB, depth 32, 50,000 nodes and 500 operations. YAML
aliases and external references are refused. Local references are resolved by
JSON Schema Ref Parser with file and HTTP resolvers disabled; recursive schemas
are unsupported. The expansion budget counts every shared-reference occurrence
before recursive schema decoding or compilation. Input and declared JSON success
schemas use the same jsonSchemaToZod compiler as App methods. Schemas with
constraints the supported compiler subset cannot preserve are unsupported,
rather than widened.
Operations expose scalar path/query/header parameters with default OpenAPI
serialization and an optional JSON body. Other serialization styles, body
media, referenced Path Items and TRACE are unsupported. Required path parameters
must match the path. Reserved transport headers, declared API-key parameters and
an owner-provided authParameter cannot become guest input. GET/HEAD/OPTIONS
are reads; other supported methods are writes. Sanitized method names are unique
and reserve describe, search, then and JavaScript object member names.
Paths cannot change the origin, traverse dot segments or contain query/fragment
syntax or encoded separators. Header names follow RFC token grammar and exclude
transport, forwarding, credential and method-override headers. Prototype names
are refused before decoding. Regex keywords (pattern, patternProperties) and
all format constraints are unsupported. OpenAPI 3.0 nullable is supported;
OpenAPI 3.1 must use null types instead. Unknown schema keywords are refused.
Provider-authored descriptions/schema annotations are bounded to 2,048
characters, titles to 200, and tags to 16 entries of 64 characters each.
Operations mark their text as third_party; hosts must frame it as data. Server
hints require HTTPS without userinfo; relative hints remain relative to the
retrieved spec. Hints confer no network admission and do not select a credential
destination: that belongs to the private Veil execution door.
parseOpenApiTable(selectedOperations) decodes 1–64 supported methods before
storage, rejects duplicate names and tables above 512 KiB, and returns a stable
SHA-256 digest. It stores normalized schemas and call shapes, never raw specs or
credentials. A compiler result with no declared output constraint still needs
the execution owner’s bounded JSON output decoder. Neither intake nor table
validation grants execution permission or validates a server’s network
admission. A connector owner selects from its server-built preview by HTTP
method + path, never client-supplied operation bodies or generated method names;
collision suffixes can change when the spec changes.
The pure intake and selected-table laws are exercised in
test/openapi.test.ts.
Dynamic App tool schemas
Section titled “Dynamic App tool schemas”appToolFamilySchemas(api, reply) validates a dynamic listing through its
signed method output, then compiles each row’s input and output with the
ordinary App schema decoder. It returns the family name, call method, declared
effect and requirements, plus rows and their validators. An unindexed listing
has no tools. App-owned selectors remain row data; the host constructs the
invocation through that App’s call owner. The projection supplies no handlers,
binding defaults or authority. SDK modules and labelled MCP tools can consume
the same rows and validators.