Skip to content
Concepts

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.

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.

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.

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.

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.