Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
2cca149
feat(codemode): add OpenAPI tool adapter
rekram1-node Jul 3, 2026
9793d9f
fix(codemode): address OpenAPI adapter review feedback
rekram1-node Jul 3, 2026
6c40e9a
fix(codemode): handle 2XX wildcards and required bodies in OpenAPI ad…
rekram1-node Jul 3, 2026
b8d4006
fix(codemode): correct OpenAPI response fallthrough and cookie handling
rekram1-node Jul 3, 2026
5f785cf
refactor(codemode): validate OpenAPI calls locally before auth and si…
rekram1-node Jul 3, 2026
264b7c0
fix(codemode): refine OpenAPI response selection and header validation
rekram1-node Jul 3, 2026
a63800c
refactor(codemode): align OpenAPI adapter with style guide
rekram1-node Jul 3, 2026
6efdae2
refactor(codemode): trim OpenAPI adapter to basic converter scope
rekram1-node Jul 3, 2026
bf1e20a
fix(codemode): harden OpenAPI path and record handling, cover naming …
rekram1-node Jul 3, 2026
49ef91f
fix(codemode): harden OpenAPI tool names and required params
rekram1-node Jul 3, 2026
e4e27c4
test(opencode): stabilize websocket idle pruning
rekram1-node Jul 3, 2026
813fb9d
refactor(codemode): move OpenAPI adapter
rekram1-node Jul 3, 2026
d9e870a
refactor(codemode): expose OpenAPI only as adapter
rekram1-node Jul 3, 2026
afb42e1
refactor(codemode): split OpenAPI adapter modules
rekram1-node Jul 3, 2026
34c65b7
refactor(codemode): simplify OpenAPI adapter split
rekram1-node Jul 3, 2026
d8e3b69
refactor(codemode): inline private token estimate
rekram1-node Jul 3, 2026
04c4e90
refactor(codemode): inline OpenAPI error summary
rekram1-node Jul 3, 2026
701bf18
refactor(codemode): inline trivial runtime helpers
rekram1-node Jul 3, 2026
f2e43da
test(codemode): cover OpenAPI adapter with opencode spec
rekram1-node Jul 3, 2026
3bf6636
test(codemode): use fixed OpenAPI fixture
rekram1-node Jul 3, 2026
79a3a12
test(codemode): assert OpenAPI fixture tool shapes
rekram1-node Jul 3, 2026
88ac234
test(codemode): name OpenAPI fixture for v2
rekram1-node Jul 3, 2026
7213242
revert weird change
rekram1-node Jul 3, 2026
e213a08
feat(codemode): preserve OpenAPI operation namespaces
rekram1-node Jul 3, 2026
5a3520a
test(codemode): refresh OpenAPI fixture from v2 API
rekram1-node Jul 3, 2026
adca41c
fix(codemode): preserve constrained OpenAPI types
rekram1-node Jul 3, 2026
2316f5e
fix(codemode): flatten OpenAPI tool inputs
rekram1-node Jul 4, 2026
6dd9a81
fix(codemode): resolve OpenAPI tool conflicts
rekram1-node Jul 4, 2026
e8ecb5e
fix(codemode): skip SSE OpenAPI operations
rekram1-node Jul 4, 2026
50d5ead
refactor(codemode): expose OpenAPI from package root
rekram1-node Jul 4, 2026
f8ee875
refactor(codemode): normalize OpenAPI schemas with Effect
rekram1-node Jul 4, 2026
30d6c8a
fix(codemode): define OpenAPI MVP boundaries
rekram1-node Jul 4, 2026
93af746
fix(codemode): harden OpenAPI MVP behavior
rekram1-node Jul 4, 2026
4f7042c
fix(codemode): close OpenAPI MVP gaps
rekram1-node Jul 4, 2026
bbb3a81
test(codemode): cover OpenAPI happy path
rekram1-node Jul 4, 2026
bbf47d9
refactor(codemode): clarify OpenAPI parsing
rekram1-node Jul 4, 2026
46fd9d5
refactor(codemode): isolate OpenAPI security selection
rekram1-node Jul 4, 2026
876638a
refactor(codemode): remove OpenAPI operation filtering
rekram1-node Jul 4, 2026
6c9a4fa
refactor(codemode): clarify OpenAPI auth context
rekram1-node Jul 4, 2026
9da3a31
refactor(codemode): expose source operation ID
rekram1-node Jul 4, 2026
e5c2b9a
refactor(codemode): raise OpenAPI response limit
rekram1-node Jul 4, 2026
ce4b2d1
refactor(codemode): clarify OpenAPI request building
rekram1-node Jul 4, 2026
52384b4
refactor(codemode): consolidate OpenAPI request building
rekram1-node Jul 4, 2026
54146e8
refactor(codemode): simplify OpenAPI interpretation
rekram1-node Jul 4, 2026
1e32469
refactor(codemode): reuse blocked member policy
rekram1-node Jul 4, 2026
c38304b
feat(codemode): group fallback OpenAPI tools
rekram1-node Jul 4, 2026
4e11b12
refactor(codemode): flatten fallback OpenAPI names
rekram1-node Jul 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/codemode/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
- Keep Code Mode unaware of host session, channel, and conversation models. The hosting application supplies trusted execution scope around it.
- Tool schemas are the model-facing Interface. Keep arguments minimal and natural to the operation; never add unrelated IDs as ambient capability tokens.

## OpenAPI

- Generate an operation only when its transport semantics are supported; otherwise return a precise `skipped` reason.
- Never guess parameter serialization or malformed security semantics. Unsupported serialization is skipped and malformed security fails closed.
- Render unresolved schema constructs as `unknown`, never as invented TypeScript names.
- Keep network reads bounded and map expected encoding, transport, and decoding failures to model-safe `ToolError` values.
- Test supported behavior directly; do not reproduce adapter algorithms in tests.

## Future Design Notes

- If a captured user-visible output channel returns (an earlier `output.text`/`output.file`/`output.image` API was removed from v1), keep `output` as its name, distinct from the program return value: `return` stays the structured result for the model, while `output.*` describes artifacts the host may render into a conversation or UI after execution. Keep this host-neutral and let applications decide how captured output is delivered. In v1, hosts collect media host-side (outside the sandbox) instead.
Expand Down
27 changes: 27 additions & 0 deletions packages/codemode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,33 @@ interface ExecuteFailure {

`onToolCallEnd` receives `{ index, name, input, durationMs, outcome, message? }` when an admitted call settles. `outcome` is `"success"` or `"failure"`; `message` is the model-safe failure message and is present only on failure. Interrupted calls (for example when the execution timeout fires) do not produce an end event. Both hooks are Effect-returning and must not fail.

### OpenAPI tools

`OpenAPI.fromSpec` turns an OpenAPI 3.x document into a tool subtree - one tool per operation. Dotted `operationId` values form namespaces such as `v2.session.get`. Missing IDs receive a flat method/path fallback such as `getUsersById`; names are sanitized and deduplicated. The host places the subtree under a key in its `tools` tree; that key is the model-visible namespace.

```ts
import { CodeMode, OpenAPI } from "@opencode-ai/codemode"
import { Effect } from "effect"
import { FetchHttpClient } from "effect/unstable/http"

const api = OpenAPI.fromSpec({
spec: await Bun.file("openapi.json").json(), // parsed document (no YAML)
auth: {
resolve: ({ name, scopes, operation }) =>
name === "BearerAuth"
? Effect.succeed({ type: "bearer", token })
: Effect.succeed(undefined),
},
})

const runtime = CodeMode.make({ tools: { opencode: api.tools } })
const result = await Effect.runPromise(runtime.execute(code).pipe(Effect.provide(FetchHttpClient.layer)))
```

`fromSpec` is synchronous and returns `{ tools, skipped }`. The initial adapter supports query `form`/`deepObject`, path/header `simple`, JSON request bodies, JSON responses, and text responses; unsupported parameter encodings, non-JSON request bodies, binary responses, and streaming operations land in `skipped` instead of producing broken tools. Operation and path servers take precedence over document servers unless `baseUrl` explicitly overrides all of them. Tool inputs flatten path, query, header, and closed object-body fields into one model-facing object while retaining their HTTP locations internally. Cross-location name collisions receive a location prefix such as `path_id` and `query_id`; composed, nullable, dictionary, conditionally-required, and non-object JSON bodies remain under `body`. Auth is never model-visible. Responses are limited to 50 MiB, and non-2xx responses become safe tool failures carrying the status and a size-capped body summary. Deferred capabilities are tracked in `src/openapi/TODO.md`.

Supported bearer, basic, header, and query authentication follows OpenAPI `security` semantics and is resolved host-side via `auth.resolve` - credential storage, OAuth flows, and token refresh never enter the compiler. Cookie authentication alternatives are discarded; an operation is skipped when it has no supported alternative. See the option docstrings in `src/openapi/types.ts` for the full semantics. Generated tools require `HttpClient.HttpClient` (from `effect/unstable/http`) in the Effect environment - provide `FetchHttpClient.layer` or a custom/test client layer at execution. The supplied client owns redirect policy; credentialed hosts should reject redirects or strip credentials when the origin changes.

## Discovery

The agent-tool instructions use a budgeted catalog. Every tool namespace is always listed with its tool count regardless of budget, and as many complete tool signatures (each with a one-line description) as fit an estimated-token budget are inlined. Selection is round-robin across namespaces for fairness: in each round (namespaces alphabetical), every namespace still holding un-inlined tools attempts to place its next-cheapest signature line against the shared budget, and a namespace whose next line does not fit drops out while the others keep going - so every namespace gets some representation before any namespace gets everything. The instructions state exactly how comprehensive the list is, both overall (`COMPLETE list` vs `PARTIAL - N of M shown`) and per namespace (`(3 tools)`, `(3 tools, 1 shown)`, `(3 tools, none shown)`).
Expand Down
1 change: 1 addition & 0 deletions packages/codemode/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { ToolError, CodeMode, ExecuteInputSchema, ExecuteResultSchema, toolError } from "./codemode.js"
export { Tool } from "./tool.js"
export * as OpenAPI from "./openapi/index.js"
export type { Definition as ToolDefinition, JsonSchema, ToolSchema } from "./tool.js"
export type { ToolCallEnded, ToolCallHooks } from "./tool-runtime.js"
export type {
Expand Down
19 changes: 19 additions & 0 deletions packages/codemode/src/openapi/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# OpenAPI Follow-ups

The initial adapter intentionally skips operations it cannot execute correctly. Future work may add:

- Cookie parameters, authentication, and cookie-header merging.
- Matrix, label, space-delimited, pipe-delimited, `allowReserved`, and parameter `content` serialization.
- External references and complete nested `$defs` support.
- Relative or templated server URLs and server variables.
- Base URLs containing query strings or fragments.
- Runtime response-schema validation and full content negotiation.
- Binary response values and explicit byte-oriented return types.
- Request/response projection for `readOnly` and `writeOnly` properties.
- SSE, WebSocket, and other streaming transports.
- Recovery of responses rejected by a status-filtering `HttpClient`.
- Configurable request and response size limits.
- Adapter-enforced redirect policy independent of the supplied `HttpClient`.
- Strict UTF-8 and empty-body validation for JSON responses.
- Compile-time rejection of parameter schemas with nested values unsupported by their serialization style; runtime rejects them before auth resolution.
- Complete malformed-security-scheme validation and broader auth-combination coverage.
130 changes: 130 additions & 0 deletions packages/codemode/src/openapi/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { HttpClient } from "effect/unstable/http"
import { Tool, type Definition } from "../tool.js"
import { invoke } from "./runtime.js"
import {
componentDefinitions,
inputSchema,
isRecord,
methods,
nonEmptyString,
operationInput,
operationOutput,
operationPath,
operationSecurityRequirements,
securityRequirements,
securitySchemes,
specServerUrl,
validateBaseUrl,
} from "./spec.js"
import type { Operation, Options, Result, Skipped, Tools } from "./types.js"

export type {
AuthResolver,
Credential,
Document,
Operation,
Options,
Result,
SecurityScheme,
Skipped,
Tools,
} from "./types.js"

/**
* Builds a CodeMode tool subtree from an OpenAPI 3.x document, one tool per
* operation. Auth is resolved host-side via `auth.resolve` and never
* model-visible. Tools require `HttpClient.HttpClient`; unrepresentable
* operations land in `skipped`.
*/
export const fromSpec = (options: Options): Result => {
const document = options.spec
const schemes = securitySchemes(document)
const defaultSecurity = securityRequirements(document.security)
const definitions = componentDefinitions(document)
const paths = isRecord(document.paths) ? document.paths : {}
const used = new Set<string>()
const namespaces = new Set<string>()
const skipped: Array<Skipped> = []
const tools = Object.create(null) as Tools

for (const [path, pathValue] of Object.entries(paths)) {
if (!isRecord(pathValue)) continue
for (const [method, operationValue] of Object.entries(pathValue)) {
if (!methods.has(method) || !isRecord(operationValue)) continue
const segments = operationPath(method, path, operationValue, used, namespaces)
const operation: Operation = {
operationId: nonEmptyString(operationValue.operationId),
method: method.toUpperCase(),
path,
summary: nonEmptyString(operationValue.summary),
description: nonEmptyString(operationValue.description),
}
const output = operationOutput(document, operationValue, definitions)
if (!output.ok) {
skipped.push({ method: operation.method, path, reason: output.reason })
continue
}

const resolvedBaseUrl = (() => {
if (options.baseUrl !== undefined) return validateBaseUrl(options.baseUrl)
if (operationValue.servers !== undefined) return specServerUrl(operationValue)
if (pathValue.servers !== undefined) return specServerUrl(pathValue)
return specServerUrl(document)
})()
if (!resolvedBaseUrl.ok) {
skipped.push({ method: operation.method, path, reason: resolvedBaseUrl.reason })
continue
}
const parsedInput = operationInput(document, pathValue, operationValue)
if (!parsedInput.ok) {
skipped.push({ method: operation.method, path, reason: parsedInput.reason })
continue
}
const input = parsedInput.value

const security = operationSecurityRequirements(operationValue.security, defaultSecurity, schemes)
if (!security.ok) {
skipped.push({ method: operation.method, path, reason: security.reason })
continue
}
const plan = {
operation,
url: `${resolvedBaseUrl.value.replace(/\/+$/, "")}${path}`,
fields: input.fields,
body: input.body,
security: security.value,
schemes,
auth: options.auth,
headers: options.headers ?? {},
}
used.add(segments.join("."))
for (const index of segments.slice(0, -1).keys()) namespaces.add(segments.slice(0, index + 1).join("."))
setTool(
tools,
segments,
Tool.make({
description: operation.description ?? operation.summary ?? `${operation.method} ${path}`,
input: inputSchema(input.fields, definitions),
output: output.value,
run: (input) => invoke(plan, input),
}),
)
}
}

return { tools, skipped }
}

const setTool = (tools: Tools, path: ReadonlyArray<string>, definition: Definition<HttpClient.HttpClient>): void => {
const [head, ...rest] = path
if (head === undefined) return
if (rest.length === 0) {
tools[head] = definition
return
}
const child = tools[head]
if (child === undefined || !isRecord(child) || child._tag === "CodeModeTool") {
tools[head] = Object.create(null) as Tools
}
setTool(tools[head] as Tools, rest, definition)
}
Loading
Loading