From 1aae67e853babebcd2211f6e5cd3cebad3d6080c Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 9 Oct 2026 01:36:06 -0700 Subject: [PATCH] chore: upgrade Effect to rc.118 (#50231) --- bun.lock | 62 +- package.json | 11 +- packages/ai/example/tutorial.ts | 2 +- packages/ai/script/setup-recording-env.ts | 4 +- packages/ai/src/experimental/system-one.ts | 2 +- packages/ai/src/media.ts | 13 +- .../ai/src/protocols/anthropic-messages.ts | 2 +- .../src/protocols/assemblyai-transcription.ts | 2 +- packages/ai/src/protocols/bedrock-converse.ts | 7 +- .../ai/src/protocols/bedrock-event-stream.ts | 7 +- packages/ai/src/protocols/bfl-images.ts | 2 +- .../src/protocols/deepgram-transcription.ts | 2 +- .../src/protocols/elevenlabs-transcription.ts | 2 +- packages/ai/src/protocols/fal-images.ts | 2 +- packages/ai/src/protocols/fal-video.ts | 2 +- packages/ai/src/protocols/google-images.ts | 2 +- packages/ai/src/protocols/google-video.ts | 2 +- packages/ai/src/protocols/meta-images.ts | 2 +- packages/ai/src/protocols/meta-responses.ts | 5 +- .../src/protocols/open-responses-channel.ts | 2 +- packages/ai/src/protocols/openai-responses.ts | 7 +- packages/ai/src/protocols/replicate-images.ts | 2 +- packages/ai/src/protocols/runway-video.ts | 2 +- packages/ai/src/protocols/shared.ts | 2 +- packages/ai/src/protocols/stability-images.ts | 2 +- .../ai/src/protocols/utils/bedrock-auth.ts | 2 +- .../ai/src/protocols/utils/bedrock-media.ts | 7 +- packages/ai/src/protocols/utils/fal-queue.ts | 2 +- .../ai/src/protocols/utils/media-input.ts | 7 +- .../ai/src/protocols/utils/tool-schema.ts | 12 +- packages/ai/src/protocols/xai-images.ts | 2 +- packages/ai/src/protocols/xai-video.ts | 2 +- packages/ai/src/protocols/zai-images.ts | 2 +- .../ai/src/providers/vercel-ai-gateway.ts | 2 +- packages/ai/src/route/auth.ts | 4 +- packages/ai/src/route/executor-service.ts | 2 +- packages/ai/src/route/executor.ts | 2 +- packages/ai/src/route/framing.ts | 2 +- packages/ai/src/route/media-protocol.ts | 2 +- packages/ai/src/route/media.ts | 2 +- packages/ai/src/route/transport/http.ts | 2 +- .../src/route/transport/websocket-channel.ts | 2 +- packages/ai/src/route/transport/websocket.ts | 34 +- packages/ai/src/schema/messages.ts | 17 +- packages/ai/src/tool.ts | 23 +- packages/ai/test/auth-options.types.ts | 2 +- packages/ai/test/auth.test.ts | 2 +- packages/ai/test/compaction-http.test.ts | 2 +- packages/ai/test/compile.test.ts | 2 +- packages/ai/test/evaluation.test.ts | 2 +- packages/ai/test/executor.test.ts | 36 +- packages/ai/test/image.test.ts | 2 +- packages/ai/test/lib/http.ts | 2 +- packages/ai/test/media.test.ts | 2 +- packages/ai/test/promise.test.ts | 2 +- packages/ai/test/provider/alibaba.test.ts | 2 +- .../test/provider/anthropic-messages.test.ts | 2 +- .../ai/test/provider/bedrock-converse.test.ts | 7 +- .../ai/test/provider/bedrock-mantle.test.ts | 2 +- packages/ai/test/provider/cloudflare.test.ts | 2 +- .../google-interactions.recorded.test.ts | 2 +- .../ai/test/provider/google-vertex.test.ts | 2 +- .../test/provider/meta-capabilities.test.ts | 2 +- packages/ai/test/provider/meta.test.ts | 2 +- packages/ai/test/provider/minimax.test.ts | 2 +- .../ai/test/provider/mistral-chat.test.ts | 2 +- packages/ai/test/provider/moonshot.test.ts | 2 +- .../ai/test/provider/native-providers.test.ts | 2 +- packages/ai/test/provider/openai-chat.test.ts | 2 +- .../provider/openai-compatible-chat.test.ts | 2 +- ...penai-responses-websocket.recorded.test.ts | 2 +- .../ai/test/provider/openai-responses.test.ts | 2 +- packages/ai/test/provider/venice.test.ts | 2 +- packages/ai/test/provider/xai-images.test.ts | 2 +- packages/ai/test/provider/zai-images.test.ts | 2 +- packages/ai/test/provider/zai.test.ts | 2 +- packages/ai/test/recorded-test.ts | 2 +- packages/ai/test/speech.test.ts | 7 +- .../ai/test/tool-schema-projection.test.ts | 198 ++- packages/ai/test/transcription.test.ts | 2 +- packages/app/e2e/utils/mock-api.ts | 2 +- packages/app/e2e/utils/mock-server.ts | 4 +- .../app/src/providers/models/selection.tsx | 2 +- .../app/src/runtime/persistence/README.md | 10 +- .../src/runtime/persistence/schema.test.ts | 39 +- .../app/src/runtime/persistence/schema.ts | 14 +- .../app/src/runtime/server/persistence.ts | 2 +- packages/app/src/settings/model.tsx | 8 +- packages/app/src/shell/state/layout.tsx | 8 +- .../test-browser/schema-persistence.test.ts | 14 +- packages/cli/package.json | 2 +- packages/cli/script/schema.ts | 2 +- packages/cli/src/acp/client.ts | 2 +- packages/cli/src/commands/commands.ts | 194 +-- packages/cli/src/commands/handlers/acp.ts | 2 +- .../cli/src/commands/handlers/session/list.ts | 2 +- packages/cli/src/env.ts | 4 +- packages/cli/src/framework/runtime.ts | 2 +- packages/cli/src/framework/spec.ts | 2 +- packages/cli/src/server-process.ts | 14 +- .../cli/src/services/service-registration.ts | 5 +- packages/cli/src/services/standalone.ts | 2 +- packages/cli/src/services/updater.ts | 2 +- packages/cli/src/services/web-ui.ts | 2 +- packages/cli/src/ssh-askpass.test.ts | 27 + packages/cli/src/ssh-askpass.ts | 33 +- packages/cli/test/acp/wire-fixture.ts | 2 +- packages/cli/test/cors.test.ts | 2 +- packages/cli/test/fixture/upgrade.ts | 2 +- packages/cli/test/service.test.ts | 5 +- packages/cli/test/updater-install.test.ts | 2 +- packages/cli/test/web-ui.test.ts | 2 +- packages/client/package.json | 2 +- packages/client/src/effect/client.ts | 2 +- .../client/src/effect/generated/client.ts | 6 +- packages/client/test/effect.test.ts | 2 +- packages/codemode/src/openapi/index.ts | 2 +- packages/codemode/src/openapi/runtime.ts | 2 +- packages/codemode/src/openapi/types.ts | 2 +- packages/codemode/src/tool-schema.ts | 59 +- packages/codemode/src/tool.ts | 1 + packages/codemode/test/openapi.test.ts | 2 +- packages/codemode/test/signature.test.ts | 258 +++- packages/core/package.json | 3 +- .../core/script/benchmark-location-memory.ts | 2 +- packages/core/src/aisdk.ts | 4 +- packages/core/src/config.ts | 5 +- packages/core/src/config/normalize.ts | 2 +- packages/core/src/config/plugin/agent.ts | 4 +- packages/core/src/config/plugin/command.ts | 2 +- packages/core/src/database/database.ts | 2 +- .../database/drizzle/effect-sqlite/driver.ts | 2 +- .../database/drizzle/effect-sqlite/session.ts | 4 +- .../database/drizzle/sqlite-core/effect/db.ts | 2 +- .../drizzle/sqlite-core/effect/session.ts | 2 +- packages/core/src/database/sqlite.bun.ts | 6 +- packages/core/src/database/sqlite.node.ts | 6 +- packages/core/src/database/sqlite.ts | 6 +- packages/core/src/database/sqlite.workerd.ts | 8 +- packages/core/src/effect/app-node-platform.ts | 2 +- .../core/src/effect/websocket-constructor.ts | 19 +- packages/core/src/environment/driver.ts | 2 +- packages/core/src/environment/environment.ts | 2 +- .../core/src/environment/exec-defaults.ts | 4 +- packages/core/src/environment/local.ts | 2 +- packages/core/src/environment/memory.ts | 2 +- packages/core/src/environment/unavailable.ts | 2 +- packages/core/src/formatter.ts | 2 +- packages/core/src/formatter/builtins.ts | 2 +- packages/core/src/git.ts | 2 +- packages/core/src/integration.ts | 2 +- packages/core/src/mcp/stdio.ts | 4 +- packages/core/src/models-dev.ts | 2 +- packages/core/src/plugin/internal.ts | 2 +- packages/core/src/plugin/provider/azure.ts | 4 +- packages/core/src/plugin/provider/chatgpt.ts | 2 +- .../core/src/plugin/provider/digitalocean.ts | 2 +- packages/core/src/plugin/provider/gitlab.ts | 2 +- packages/core/src/plugin/provider/local.ts | 2 +- packages/core/src/plugin/provider/opencode.ts | 2 +- packages/core/src/plugin/provider/poe.ts | 2 +- .../src/plugin/provider/snowflake-cortex.ts | 2 +- packages/core/src/plugin/vcs/git.ts | 2 +- packages/core/src/plugin/vcs/hg.ts | 2 +- packages/core/src/plugin/websearch/exa.ts | 2 +- .../core/src/plugin/websearch/firecrawl.ts | 2 +- packages/core/src/plugin/websearch/mcp.ts | 2 +- .../core/src/plugin/websearch/parallel.ts | 2 +- packages/core/src/plugin/websearch/tavily.ts | 2 +- .../core/src/plugin/websearch/tinyfish.ts | 2 +- packages/core/src/project.ts | 2 +- packages/core/src/ripgrep.ts | 2 +- packages/core/src/ripgrep/binary.ts | 6 +- packages/core/src/session/model-request.ts | 2 +- packages/core/src/session/model-transport.ts | 4 +- packages/core/src/shell.ts | 2 +- packages/core/src/skill/discovery.ts | 2 +- packages/core/src/tool/http-body.ts | 2 +- packages/core/src/tool/plugin/webfetch.ts | 2 +- packages/core/src/tool/plugin/websearch.ts | 2 +- packages/core/src/tool/runtime.ts | 12 +- packages/core/src/v1/config/agent.ts | 19 +- packages/core/src/v1/config/migrate.ts | 7 +- packages/core/src/v1/config/permission.ts | 65 +- packages/core/src/websearch.ts | 2 +- packages/core/src/wellknown.ts | 2 +- packages/core/src/workspace.ts | 2 +- packages/core/src/worktree.ts | 2 +- packages/core/test/aisdk.test.ts | 2 +- packages/core/test/codemode.test.ts | 58 +- packages/core/test/config/agent.test.ts | 9 + packages/core/test/config/config.test.ts | 65 +- .../core/test/config/normalization.test.ts | 47 +- packages/core/test/database-drizzle.test.ts | 4 +- packages/core/test/database-migration.test.ts | 4 +- .../test/effect-app-node-platform.test.ts | 4 +- .../test/effect/cross-spawn-spawner.test.ts | 2 +- packages/core/test/environment.test.ts | 2 +- packages/core/test/fixture/environment.ts | 2 +- .../core/test/fixture/v1-config/config.ts | 2 +- packages/core/test/generate.test.ts | 2 +- packages/core/test/lib/effect.ts | 2 +- packages/core/test/lib/websearch.ts | 2 +- packages/core/test/mcp.test.ts | 4 +- packages/core/test/model-resolver.test.ts | 2 +- packages/core/test/models.test.ts | 2 +- packages/core/test/network-guard.test.ts | 2 +- packages/core/test/plugin/module.test.ts | 17 +- .../core/test/plugin/provider-chatgpt.test.ts | 2 +- .../test/plugin/provider-digitalocean.test.ts | 2 +- .../test/plugin/provider-gitlab-oauth.test.ts | 2 +- .../test/plugin/provider-opencode.test.ts | 2 +- .../core/test/plugin/provider-poe.test.ts | 2 +- .../plugin/provider-snowflake-cortex.test.ts | 4 +- .../core/test/plugin/websearch-fixture.ts | 2 +- packages/core/test/process/process.test.ts | 2 +- .../test/session-checkpoint-transport.test.ts | 2 +- .../test/session-compaction-transport.test.ts | 4 +- .../test/session-model-request-hooks.test.ts | 2 +- .../test/session-model-transport-live.test.ts | 4 +- .../core/test/session-model-transport.test.ts | 2 +- packages/core/test/shell-retention.test.ts | 4 +- packages/core/test/skill-discovery.test.ts | 2 +- packages/core/test/sqlite-workerd.test.ts | 4 +- packages/core/test/state-replay.test.ts | 2 +- .../core/test/tool-read-filesystem.test.ts | 2 +- packages/core/test/tool-registry.test.ts | 2 +- packages/core/test/tool-schema.test.ts | 118 +- packages/core/test/tool-webfetch.test.ts | 2 +- packages/core/test/tool-websearch.test.ts | 2 +- packages/core/test/v1-migration.test.ts | 2 +- packages/core/test/wellknown.test.ts | 2 +- packages/core/test/workspace.test.ts | 4 +- .../src/main/files/attachment-picker.ts | 4 +- .../desktop/src/main/ipc-transport.test.ts | 4 +- packages/desktop/src/main/ipc-transport.ts | 2 +- packages/desktop/src/main/ipc.ts | 2 +- packages/desktop/src/renderer/ipc-client.ts | 2 +- packages/desktop/src/shared/ipc-rpc.ts | 2 +- packages/desktop/src/shared/ipc-rpc/app.ts | 2 +- packages/desktop/src/shared/ipc-rpc/events.ts | 2 +- .../desktop/src/shared/ipc-rpc/extensions.ts | 4 +- packages/desktop/src/shared/ipc-rpc/files.ts | 4 +- packages/desktop/src/shared/ipc-rpc/menu.ts | 2 +- .../desktop/src/shared/ipc-rpc/storage.ts | 4 +- packages/desktop/src/shared/ipc-rpc/window.ts | 2 +- packages/gui-extensions/src/browser/ipc.ts | 4 +- .../gui-extensions/src/browser/network.ts | 8 +- packages/gui-extensions/src/browser/pane.ts | 2 +- .../gui-extensions/src/ssh/askpass.test.ts | 79 +- packages/gui-extensions/src/ssh/askpass.ts | 35 +- packages/gui-extensions/src/ssh/bootstrap.ts | 2 +- packages/gui-extensions/src/ssh/command.ts | 2 +- packages/gui-extensions/src/ssh/controller.ts | 6 +- packages/gui-extensions/src/ssh/main.ts | 2 +- packages/gui-extensions/src/ssh/remote-cli.ts | 2 +- packages/http-recorder/README.md | 34 +- packages/http-recorder/package.json | 2 +- .../http-recorder/script/verify-package.ts | 4 +- packages/http-recorder/src/http/recorder.ts | 8 +- packages/http-recorder/src/index.ts | 4 +- .../http-recorder/src/websocket/recorder.ts | 332 ++--- packages/http-recorder/test/http.test.ts | 2 +- packages/http-recorder/test/redaction.test.ts | 2 +- packages/http-recorder/test/support.ts | 2 +- packages/http-recorder/test/websocket.test.ts | 384 +++-- packages/httpapi-codegen/src/index.ts | 30 +- packages/httpapi-codegen/test/fixture.ts | 2 +- .../httpapi-codegen/test/generate.test.ts | 15 +- .../test/generated-consumer.ts | 2 +- .../httpapi-codegen/test/generated/client.ts | 2 +- .../httpapi-codegen/test/generated/event.ts | 6 +- .../httpapi-codegen/test/generated/session.ts | 6 +- .../httpapi-codegen/test/generated/system.ts | 6 +- packages/httpapi-codegen/test/write.test.ts | 4 +- packages/plugin-browser/src/rpc.ts | 8 +- packages/plugin-browser/src/tools.ts | 5 +- packages/plugin-browser/test/rpc.test.ts | 10 + packages/plugin/src/promise/adapter.ts | 2 +- packages/protocol/openapi.json | 1232 +++++++++++++---- packages/protocol/script/generate-openapi.ts | 2 +- packages/protocol/src/api.ts | 2 +- packages/protocol/src/client.ts | 2 +- packages/protocol/src/groups/agent.ts | 2 +- packages/protocol/src/groups/command.ts | 2 +- packages/protocol/src/groups/config.ts | 2 +- packages/protocol/src/groups/credential.ts | 2 +- packages/protocol/src/groups/debug.ts | 2 +- packages/protocol/src/groups/event.ts | 2 +- packages/protocol/src/groups/form.ts | 2 +- packages/protocol/src/groups/fs.ts | 2 +- packages/protocol/src/groups/generate.ts | 2 +- packages/protocol/src/groups/integration.ts | 2 +- packages/protocol/src/groups/location.ts | 2 +- packages/protocol/src/groups/mcp.ts | 2 +- packages/protocol/src/groups/message.ts | 2 +- packages/protocol/src/groups/migration.ts | 2 +- packages/protocol/src/groups/model.ts | 2 +- packages/protocol/src/groups/permission.ts | 2 +- .../protocol/src/groups/persistent-pty.ts | 2 +- packages/protocol/src/groups/plugin.ts | 2 +- packages/protocol/src/groups/project.ts | 2 +- packages/protocol/src/groups/provider.ts | 2 +- packages/protocol/src/groups/pty.ts | 2 +- packages/protocol/src/groups/reference.ts | 2 +- packages/protocol/src/groups/rpc.ts | 2 +- packages/protocol/src/groups/server.ts | 2 +- packages/protocol/src/groups/session.ts | 12 +- packages/protocol/src/groups/shell.ts | 2 +- packages/protocol/src/groups/skill.ts | 2 +- packages/protocol/src/groups/vcs.ts | 2 +- packages/protocol/src/groups/websearch.ts | 2 +- packages/protocol/src/groups/worktree.ts | 2 +- .../protocol/src/middleware/authorization.ts | 2 +- .../protocol/src/middleware/schema-error.ts | 2 +- packages/protocol/src/simulation.ts | 2 +- .../protocol/test/openapi-patterns.test.ts | 28 + packages/schema/src/config/agent.ts | 2 +- packages/schema/src/config/model.ts | 8 +- packages/schema/src/event.ts | 2 +- packages/schema/src/form.ts | 2 +- packages/schema/src/instruction-entry.ts | 2 +- packages/schema/src/instruction.ts | 4 +- packages/schema/src/permission.ts | 2 +- packages/schema/src/project.ts | 2 +- packages/schema/src/prompt.ts | 2 +- packages/schema/src/pty.ts | 2 +- packages/schema/src/schema.ts | 7 +- packages/schema/src/session-id.ts | 2 +- packages/schema/src/session-message.ts | 2 +- packages/schema/src/shell.ts | 2 +- packages/schema/src/v1/permission.ts | 2 +- packages/schema/src/v1/session.ts | 4 +- packages/schema/src/workspace-id.ts | 2 +- packages/sdk/src/effect/opencode.ts | 2 +- packages/sdk/src/internal/host.ts | 2 +- packages/sdk/test/transport.test.ts | 2 +- packages/server/package.json | 4 +- packages/server/src/fetch.ts | 2 +- packages/server/src/handlers/agent.ts | 2 +- packages/server/src/handlers/command.ts | 2 +- packages/server/src/handlers/config.ts | 2 +- packages/server/src/handlers/credential.ts | 2 +- packages/server/src/handlers/debug.ts | 2 +- packages/server/src/handlers/event.ts | 4 +- packages/server/src/handlers/form.ts | 2 +- packages/server/src/handlers/fs.ts | 4 +- packages/server/src/handlers/generate.ts | 2 +- packages/server/src/handlers/integration.ts | 2 +- packages/server/src/handlers/location.ts | 2 +- packages/server/src/handlers/mcp.ts | 2 +- packages/server/src/handlers/message.ts | 2 +- packages/server/src/handlers/migration.ts | 2 +- packages/server/src/handlers/model.ts | 2 +- packages/server/src/handlers/permission.ts | 2 +- .../server/src/handlers/persistent-pty.ts | 77 +- packages/server/src/handlers/plugin.ts | 2 +- packages/server/src/handlers/project.ts | 2 +- packages/server/src/handlers/provider.ts | 2 +- packages/server/src/handlers/pty-socket.ts | 38 +- packages/server/src/handlers/pty.ts | 68 +- packages/server/src/handlers/reference.ts | 2 +- packages/server/src/handlers/rpc.ts | 2 +- packages/server/src/handlers/server.ts | 4 +- packages/server/src/handlers/session.ts | 2 +- packages/server/src/handlers/shell.ts | 2 +- packages/server/src/handlers/skill.ts | 2 +- packages/server/src/handlers/vcs.ts | 2 +- packages/server/src/handlers/websearch.ts | 2 +- packages/server/src/handlers/worktree.ts | 2 +- packages/server/src/location.ts | 4 +- .../server/src/middleware/authorization.ts | 7 +- .../server/src/middleware/form-location.ts | 4 +- .../server/src/middleware/schema-error.ts | 2 +- .../server/src/middleware/session-location.ts | 4 +- packages/server/src/process.ts | 5 +- packages/server/src/routes.ts | 4 +- packages/server/test/fixture/server.ts | 2 +- packages/server/test/persistent-pty.test.ts | 62 +- packages/server/test/process.test.ts | 2 +- packages/server/test/provider.test.ts | 2 +- .../server/test/pty-socket.property.test.ts | 240 ++++ packages/server/test/pty-socket.test.ts | 89 +- packages/server/test/pty-transport.test.ts | 137 ++ packages/server/test/request-tracing.test.ts | 2 +- .../server/test/rpc-handler-errors.test.ts | 2 +- .../server/test/session-instances.test.ts | 2 +- packages/server/test/sse-failure.test.ts | 71 + packages/simulation/src/backend/index.ts | 4 +- packages/simulation/src/backend/network.ts | 6 +- packages/simulation/src/backend/openai.ts | 4 +- packages/simulation/src/frontend/actions.ts | 5 +- .../simulation/src/frontend/simulation.ts | 2 +- packages/simulation/src/manifest.ts | 4 +- packages/simulation/test/network.test.ts | 2 +- packages/simulation/test/openai.test.ts | 4 +- packages/stats/core/src/config.ts | 2 +- packages/stats/core/src/database.ts | 4 +- packages/stats/server/src/router.ts | 2 +- packages/stats/server/src/server.ts | 6 +- packages/util/src/cross-spawn-spawner.ts | 4 +- packages/util/src/effect/app-node-platform.ts | 2 +- packages/util/src/observability.ts | 4 +- packages/util/src/observability/logging.ts | 4 +- packages/util/src/observability/otlp.ts | 2 +- packages/util/src/process.ts | 4 +- services/www/openapi.json | 1232 +++++++++++++---- services/www/public/openapi.json | 1232 +++++++++++++---- .../src/docs/content/build/client/effect.mdx | 6 +- .../src/docs/content/build/plugins/effect.mdx | 4 +- .../docs/content/build/plugins/effect/rpc.mdx | 2 +- 411 files changed, 5756 insertions(+), 2161 deletions(-) create mode 100644 packages/protocol/test/openapi-patterns.test.ts create mode 100644 packages/server/test/pty-socket.property.test.ts create mode 100644 packages/server/test/pty-transport.test.ts create mode 100644 packages/server/test/sse-failure.test.ts diff --git a/bun.lock b/bun.lock index 12134e5b24b0..0215a1424c30 100644 --- a/bun.lock +++ b/bun.lock @@ -131,7 +131,7 @@ "@opencode/util": "workspace:*", "@opentui/core": "catalog:", "@opentui/solid": "catalog:", - "@opentunnel/client": "0.2.0", + "@opentunnel/client": "0.4.0", "@parcel/watcher": "2.5.1", "@silvia-odwyer/photon-node": "0.3.4", "diff": "catalog:", @@ -195,7 +195,7 @@ "solid-js": "catalog:", }, "peerDependencies": { - "effect": "4.0.0-rc.112", + "effect": "4.0.0-rc.118", "solid-js": ">=1.9.0", }, "optionalPeers": [ @@ -403,6 +403,7 @@ "@types/node": "catalog:", "@types/which": "3.0.4", "drizzle-kit": "catalog:", + "fast-check": "catalog:", }, }, "packages/desktop": { @@ -557,7 +558,7 @@ "name": "@opencode/http-recorder", "version": "2.0.26", "dependencies": { - "@effect/platform-node-shared": "4.0.0-rc.112", + "@effect/platform-node-shared": "4.0.0-rc.118", }, "devDependencies": { "@effect/platform-node": "catalog:", @@ -755,11 +756,13 @@ "effect": "catalog:", }, "devDependencies": { + "@opencode-ai/pty": "0.2.0", "@opencode/client": "workspace:*", "@opencode/plugin": "workspace:*", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:", + "fast-check": "catalog:", }, }, "packages/session-ui": { @@ -1147,10 +1150,10 @@ "catalog": { "@cloudflare/workers-types": "4.20251008.0", "@corvu/drawer": "0.2.4", - "@effect/opentelemetry": "4.0.0-rc.112", - "@effect/platform-node": "4.0.0-rc.112", - "@effect/platform-node-shared": "4.0.0-rc.112", - "@effect/sql-sqlite-bun": "4.0.0-rc.112", + "@effect/opentelemetry": "4.0.0-rc.118", + "@effect/platform-node": "4.0.0-rc.118", + "@effect/platform-node-shared": "4.0.0-rc.118", + "@effect/sql-sqlite-bun": "4.0.0-rc.118", "@hono/standard-validator": "0.2.0", "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.13", @@ -1190,7 +1193,8 @@ "dompurify": "3.4.14", "drizzle-kit": "1.0.0-rc.5-ab785fc", "drizzle-orm": "1.0.0-rc.5-169397b", - "effect": "4.0.0-rc.112", + "effect": "4.0.0-rc.118", + "fast-check": "4.9.0", "fuzzysort": "3.1.0", "get-east-asian-width": "1.6.0", "hono": "4.10.7", @@ -1728,13 +1732,13 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.12.0", "", {}, "sha512-mlUE+rZ8CatQekLhnaiN91Iemdd+e2gFKooGlnRB3oPTL3VghLfX24dx7HrzMNeC1JrIB/0kpsfyty3f5HNfxQ=="], - "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-rc.112", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.0-rc.112" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-OTRv1DxTHUmnakgJ6XVM8wVgF1KgZH4UXnOemwSLUwUjXO+RCikzF8oR/rlVmOGq81KtQzj1URM4M4nchlQOuQ=="], + "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-rc.118", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.0-rc.118" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-N5dvEeCj1W/9zgBvyyp/GucOKOEJdV7QNirWp+4g/axFKTB/cfV2gx/ZhMDUb69HzlyRmOelNvKEmB3RA0PdVw=="], - "@effect/platform-node": ["@effect/platform-node@4.0.0-rc.112", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.112", "mime": "^4.1.0", "undici": "^8.10.0" }, "peerDependencies": { "effect": "^4.0.0-rc.112", "redis": ">=5.0.0 <7.0.0" } }, "sha512-/BMAcdNGQQskLmI0Zoa95KfTZkr9HV9N4NSxaSrusG6GeW6Ulp9KvZ+Rlaiw8lnOt43CXjFLdfll5/k5rxL4hQ=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-rc.118", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.118", "undici": "^8.11.2" }, "peerDependencies": { "effect": "^4.0.0-rc.118", "redis": ">=5.0.0 <7.0.0" } }, "sha512-qdAxPZ+5bKxVnzkwziqcHZTHRHz2hyfFX6NhwEmm75CBpmaIzXIZR2LRqGd1/8eI3J3cOrXcAYzByhln7T1/EA=="], - "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-rc.112", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.3" }, "peerDependencies": { "effect": "^4.0.0-rc.112" } }, "sha512-ttjz0xKamFN7vL8pNDYVwddJLjZvqKePc05djlz2VcdaKbLsnYbtMnL1rbOfHgEnIUSHGh7FkjaN4DM1Ov81sQ=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-rc.118", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.22.0" }, "peerDependencies": { "effect": "^4.0.0-rc.118" } }, "sha512-pNbmIlxbAUxAthUNsp2xm1NLG9kxoYecsPkoYl9mNF2oxPcKDxAvkOar7tYnLSUpXJeATm4pxYVipdZjQfqO4Q=="], - "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-rc.112", "", { "peerDependencies": { "effect": "^4.0.0-rc.112" } }, "sha512-EqR8pWZo3VzedRvP6qo5HcfqNmFvSa+sQLWQl3oBMx2KY1FCc8NYjep79wYjNkW0qABnffrPUXCMTOncd1A+5g=="], + "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-rc.118", "", { "peerDependencies": { "effect": "^4.0.0-rc.118" } }, "sha512-UF0oVOK7mQvEwpxySR9VTk5YoP5luRI//Z8NMOJKVG/SbZbsORGACd7Np+KZRiXXjgzSXEpfGb7Gs3G1cKfUCg=="], "@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.5", "", {}, "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA=="], @@ -2070,18 +2074,6 @@ "@motionone/utils": ["@motionone/utils@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw=="], - "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], - - "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], - - "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="], - - "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="], - - "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="], - - "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], - "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="], @@ -2328,9 +2320,9 @@ "@opentui/solid": ["@opentui/solid@0.5.17", "", { "dependencies": { "@babel/core": "7.29.7", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.17", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-YShxRqJYyg3ysh4qR2NpCDhr+I1OpIfmcn8nQch7EsBpmUOZh8INqThQI722tXBULOAYhOQWqzr5APjJxYDFbQ=="], - "@opentunnel/client": ["@opentunnel/client@0.2.0", "", { "dependencies": { "@opentunnel/protocol": "0.2.0", "effect": "4.0.0-rc.112" } }, "sha512-UHbLK+y2aPdcTuu4FaCz3Ezcm52Z4TxGp5Hdy9aLYLsz9GUlPpPFaYXjdq8pFyfQlA9qAyI/z5zx5FlZx3aSbw=="], + "@opentunnel/client": ["@opentunnel/client@0.4.0", "", { "dependencies": { "@opentunnel/protocol": "0.4.0" }, "peerDependencies": { "effect": "^4.0.0" } }, "sha512-Y6UiO/ouJ/vtdIy4QmCTRY2DPTRpHy2+jR0F6+AKXoD57ozd8W9ZTTP2J1KDqTb7ZfJmRTuqtx1ZR2TnejlZGQ=="], - "@opentunnel/protocol": ["@opentunnel/protocol@0.2.0", "", { "dependencies": { "effect": "4.0.0-rc.112" } }, "sha512-XQAm3InG70HysML+NiSPxCCfoP+ZzWuxXcCFFFh/qxrvMxuJyOrxsPT49vTo2rDCgrYYdbiFqpz8oXIJh0BYXw=="], + "@opentunnel/protocol": ["@opentunnel/protocol@0.4.0", "", { "peerDependencies": { "effect": "^4.0.0" } }, "sha512-rxmfgJ5O1EYxgzawkOti9DyiM44l0Bkeue5KuHpCFaPAP2n9i4xDYCU8XQp9WAEFt7ifZZqI9giOyzJE61faaA=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -3918,7 +3910,7 @@ "editorconfig": ["editorconfig@1.0.7", "", { "dependencies": { "@one-ini/wasm": "0.1.1", "commander": "^10.0.0", "minimatch": "^9.0.1", "semver": "^7.5.3" }, "bin": { "editorconfig": "bin/editorconfig" } }, "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw=="], - "effect": ["effect@4.0.0-rc.112", "", { "dependencies": { "fast-check": "^4.9.0", "msgpackr": "^2.0.5" } }, "sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A=="], + "effect": ["effect@4.0.0-rc.118", "", {}, "sha512-hxhpCd+swzrtu9lnyPpEFre2ODQzJ9qxgyL1oHIEY8NKnW4PxTbM1XzplMZXiAsLD62HO+KJc41Nqxr9T791ZQ=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], @@ -4756,7 +4748,7 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="], + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], @@ -4804,10 +4796,6 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@2.0.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA=="], - - "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], - "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], @@ -4850,8 +4838,6 @@ "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], - "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], - "node-html-parser": ["node-html-parser@7.1.0", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ=="], "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], @@ -5648,7 +5634,7 @@ "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], - "undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="], + "undici": ["undici@8.11.2", "", {}, "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -6098,15 +6084,13 @@ "@bufbuild/protoplugin/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], - "@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], - "@cloudflare/vite-plugin/vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="], "@cloudflare/vite-plugin/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - "@effect/platform-node-shared/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + "@effect/platform-node-shared/ws": ["ws@8.22.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="], "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], @@ -6688,6 +6672,8 @@ "tw-to-css/tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="], + "unifont/undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="], + "unplugin/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], "unused-filename/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], diff --git a/package.json b/package.json index 635b9444e60a..2aa7d04fca11 100644 --- a/package.json +++ b/package.json @@ -43,10 +43,10 @@ "packages/stats/*" ], "catalog": { - "@effect/opentelemetry": "4.0.0-rc.112", - "@effect/platform-node": "4.0.0-rc.112", - "@effect/platform-node-shared": "4.0.0-rc.112", - "@effect/sql-sqlite-bun": "4.0.0-rc.112", + "@effect/opentelemetry": "4.0.0-rc.118", + "@effect/platform-node": "4.0.0-rc.118", + "@effect/platform-node-shared": "4.0.0-rc.118", + "@effect/sql-sqlite-bun": "4.0.0-rc.118", "@npmcli/arborist": "9.4.0", "@types/bun": "1.4.0", "@types/cross-spawn": "6.0.6", @@ -80,7 +80,8 @@ "dompurify": "3.4.14", "drizzle-kit": "1.0.0-rc.5-ab785fc", "drizzle-orm": "1.0.0-rc.5-169397b", - "effect": "4.0.0-rc.112", + "effect": "4.0.0-rc.118", + "fast-check": "4.9.0", "ai": "6.0.168", "cross-spawn": "7.0.6", "hono": "4.10.7", diff --git a/packages/ai/example/tutorial.ts b/packages/ai/example/tutorial.ts index b97fbb212df0..eb7b75f39a94 100644 --- a/packages/ai/example/tutorial.ts +++ b/packages/ai/example/tutorial.ts @@ -15,7 +15,7 @@ import { OpenAI } from "@opencode/ai/providers" * hover imports and local values to see how the public API is typed. */ -const apiKey = Config.redacted("OPENAI_API_KEY") +const apiKey = Config.Redacted("OPENAI_API_KEY") // 1. Configure a provider. The configured facade records provider identity, // deployment options, authentication, and defaults. Per-modality selectors pick diff --git a/packages/ai/script/setup-recording-env.ts b/packages/ai/script/setup-recording-env.ts index 94eac741d2c4..4b583076a890 100644 --- a/packages/ai/script/setup-recording-env.ts +++ b/packages/ai/script/setup-recording-env.ts @@ -5,7 +5,7 @@ import * as path from "node:path" import * as prompts from "@clack/prompts" import { AwsV4Signer } from "aws4fetch" import { Config, ConfigProvider, Effect, FileSystem, PlatformError, Redacted } from "effect" -import { FetchHttpClient, HttpClient, HttpClientRequest, type HttpClientResponse } from "effect/unstable/http" +import { FetchHttpClient, HttpClient, HttpClientRequest, type HttpClientResponse } from "effect/http" import * as ProviderShared from "../src/protocols/shared" import { CloudflareAIGateway } from "../src/providers/cloudflare-ai-gateway.js" import { CloudflareWorkersAI } from "../src/providers/cloudflare-workers-ai.js" @@ -383,7 +383,7 @@ const readEnvFile = Effect.fn("RecordingEnv.readFile")(function* () { }) const readConfigString = (provider: ConfigProvider.ConfigProvider, name: string) => - Config.string(name) + Config.String(name) .parse(provider) .pipe( Effect.match({ diff --git a/packages/ai/src/experimental/system-one.ts b/packages/ai/src/experimental/system-one.ts index 55a386b79ddd..3e924f2810e4 100644 --- a/packages/ai/src/experimental/system-one.ts +++ b/packages/ai/src/experimental/system-one.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import { Headers, HttpClientRequest } from "effect/unstable/http" +import { Headers, HttpClientRequest } from "effect/http" import { ChoiceQuestion, EvaluationInput, diff --git a/packages/ai/src/media.ts b/packages/ai/src/media.ts index fca61a3cc2a3..91c0d832c9eb 100644 --- a/packages/ai/src/media.ts +++ b/packages/ai/src/media.ts @@ -1,7 +1,8 @@ export * as Media from "./media.js" -import { Effect, Encoding, FileSystem, Schema, SchemaGetter } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { Effect, FileSystem, Schema, SchemaGetter } from "effect" +import { Base64 } from "effect/encoding" +import { HttpClientRequest } from "effect/http" import { ProviderID } from "./schema/ids.js" import { AIError, HttpContext, InvalidProviderOutputError, InvalidRequestError } from "./schema/errors.js" import { ProviderMetadata } from "./schema/options.js" @@ -140,7 +141,7 @@ export class Asset { inline(): Inline | undefined { const source = this.source if (source.type !== "bytes" && source.type !== "base64") return undefined - const base64 = source.type === "base64" ? source.data : (this.#base64 ??= Encoding.encodeBase64(source.data)) + const base64 = source.type === "base64" ? source.data : (this.#base64 ??= Base64.encode(source.data)) const mime = this.mediaType.toLowerCase() return { mime, base64, dataUrl: `data:${mime};base64,${base64}` } } @@ -155,7 +156,7 @@ export class Asset { return Effect.fail(invalid(`Cannot materialize provider ref ${source.provider}:${source.id}`)) const decoded = source.type === "base64" - ? Effect.fromResult(Encoding.decodeBase64(source.data)).pipe( + ? Effect.fromResult(Base64.decode(source.data)).pipe( Effect.mapError((cause) => invalid(`Media asset contains invalid base64 data`, cause)), ) : download(source, this.headers) @@ -168,7 +169,7 @@ export class Asset { const source = this.source if (source.type === "base64") return Effect.succeed(source.data) if (this.#base64 !== undefined) return Effect.succeed(this.#base64) - return this.bytes().pipe(Effect.map((data) => (this.#base64 = Encoding.encodeBase64(data)))) + return this.bytes().pipe(Effect.map((data) => (this.#base64 = Base64.encode(data)))) }) } @@ -183,7 +184,7 @@ export class Asset { toJSON() { const source = this.source return { - source: source.type === "bytes" ? { ...source, data: Encoding.encodeBase64(source.data) } : source, + source: source.type === "bytes" ? { ...source, data: Base64.encode(source.data) } : source, info: this.info, providerMetadata: this.providerMetadata, } diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index 5bb2d27d24d3..1483249ec507 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -6,7 +6,7 @@ import { Auth } from "../route/auth.js" import { Endpoint } from "../route/endpoint.js" import { Framing } from "../route/framing.js" import { Protocol } from "../route/protocol.js" -import { Headers } from "effect/unstable/http" +import { Headers } from "effect/http" import { HttpTransport } from "../route/transport/index.js" import { AIError, diff --git a/packages/ai/src/protocols/assemblyai-transcription.ts b/packages/ai/src/protocols/assemblyai-transcription.ts index 46c07413225b..6446b04f4821 100644 --- a/packages/ai/src/protocols/assemblyai-transcription.ts +++ b/packages/ai/src/protocols/assemblyai-transcription.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import type { Status } from "../generation.js" import { Media } from "../media.js" import { MediaProtocol } from "../route/media-protocol.js" diff --git a/packages/ai/src/protocols/bedrock-converse.ts b/packages/ai/src/protocols/bedrock-converse.ts index d512e91e0419..f4d467108e3c 100644 --- a/packages/ai/src/protocols/bedrock-converse.ts +++ b/packages/ai/src/protocols/bedrock-converse.ts @@ -1,4 +1,5 @@ -import { Effect, Encoding, Schema } from "effect" +import { Effect, Schema } from "effect" +import { Base64 } from "effect/encoding" import { Route } from "../route/client.js" import { Endpoint } from "../route/endpoint.js" import { Protocol } from "../route/protocol.js" @@ -612,7 +613,7 @@ interface ParserState { readonly reasoningRedactedContent: Readonly> } -const encodeRedactedContent = (chunks: ReadonlyArray) => Encoding.encodeBase64(concatBytes(chunks)) +const encodeRedactedContent = (chunks: ReadonlyArray) => Base64.encode(concatBytes(chunks)) const step = (state: ParserState, event: BedrockEvent) => Effect.gen(function* () { @@ -661,7 +662,7 @@ const step = (state: ParserState, event: BedrockEvent) => const events: LLMEvent[] = [] const redactedChunk = yield* (() => { if (reasoning.redactedContent === undefined) return Effect.succeed(undefined) - return Effect.fromResult(Encoding.decodeBase64(reasoning.redactedContent)).pipe( + return Effect.fromResult(Base64.decode(reasoning.redactedContent)).pipe( Effect.mapError((cause) => ProviderShared.eventError( ADAPTER, diff --git a/packages/ai/src/protocols/bedrock-event-stream.ts b/packages/ai/src/protocols/bedrock-event-stream.ts index a29cc1c529dd..af81da859c2b 100644 --- a/packages/ai/src/protocols/bedrock-event-stream.ts +++ b/packages/ai/src/protocols/bedrock-event-stream.ts @@ -1,6 +1,7 @@ import { EventStreamCodec } from "@smithy/eventstream-codec" import { fromUtf8, toUtf8 } from "@smithy/util-utf8" -import { Effect, Encoding, Stream } from "effect" +import { Effect, Stream } from "effect" +import { Base64 } from "effect/encoding" import { AIError, AIErrorReason, InvalidProviderOutputError } from "../schema/index.js" import { Framing } from "../route/framing.js" import { ProviderShared } from "./shared.js" @@ -47,7 +48,7 @@ const consumeFrames = (route: string) => (state: FrameBufferState, input: FrameI route, classification: "incomplete-stream", message: `Incomplete Bedrock Converse event-stream frame: ${remaining.length} buffered bytes remain at end of stream`, - body: Encoding.encodeBase64(remaining), + body: Base64.encode(remaining), }), }) return [state, []] as const @@ -68,7 +69,7 @@ const consumeFrames = (route: string) => (state: FrameBufferState, input: FrameI `Failed to decode Bedrock Converse event-stream frame: ${ error instanceof Error ? error.message : String(error) }`, - Encoding.encodeBase64(view.subarray(0, totalLength)), + Base64.encode(view.subarray(0, totalLength)), error, ), }) diff --git a/packages/ai/src/protocols/bfl-images.ts b/packages/ai/src/protocols/bfl-images.ts index b5f7b532bf0e..ca4e128f568e 100644 --- a/packages/ai/src/protocols/bfl-images.ts +++ b/packages/ai/src/protocols/bfl-images.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import type { Status } from "../generation.js" import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js" import { Media } from "../media.js" diff --git a/packages/ai/src/protocols/deepgram-transcription.ts b/packages/ai/src/protocols/deepgram-transcription.ts index 1c6e8517c8cc..c3868c71cd6d 100644 --- a/packages/ai/src/protocols/deepgram-transcription.ts +++ b/packages/ai/src/protocols/deepgram-transcription.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import { MediaProtocol } from "../route/media-protocol.js" import { MediaRoute } from "../route/media.js" import { mergeJsonRecords, type OpenString } from "../schema/index.js" diff --git a/packages/ai/src/protocols/elevenlabs-transcription.ts b/packages/ai/src/protocols/elevenlabs-transcription.ts index 626df5e8e0a9..1f971231a8ce 100644 --- a/packages/ai/src/protocols/elevenlabs-transcription.ts +++ b/packages/ai/src/protocols/elevenlabs-transcription.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import { MediaProtocol } from "../route/media-protocol.js" import { MediaRoute } from "../route/media.js" import { mergeJsonRecords, type OpenString } from "../schema/index.js" diff --git a/packages/ai/src/protocols/fal-images.ts b/packages/ai/src/protocols/fal-images.ts index d3de229dc4ef..ce722a5e0784 100644 --- a/packages/ai/src/protocols/fal-images.ts +++ b/packages/ai/src/protocols/fal-images.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js" import { Media } from "../media.js" import { MediaProtocol } from "../route/media-protocol.js" diff --git a/packages/ai/src/protocols/fal-video.ts b/packages/ai/src/protocols/fal-video.ts index 02e77db373fe..dc5a0664d211 100644 --- a/packages/ai/src/protocols/fal-video.ts +++ b/packages/ai/src/protocols/fal-video.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import { Media } from "../media.js" import { MediaProtocol } from "../route/media-protocol.js" import { MediaRoute } from "../route/media.js" diff --git a/packages/ai/src/protocols/google-images.ts b/packages/ai/src/protocols/google-images.ts index b8f7ff1584cb..ed67d294bba5 100644 --- a/packages/ai/src/protocols/google-images.ts +++ b/packages/ai/src/protocols/google-images.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js" import { MediaProtocol } from "../route/media-protocol.js" import { MediaRoute } from "../route/media.js" diff --git a/packages/ai/src/protocols/google-video.ts b/packages/ai/src/protocols/google-video.ts index c3ff3a621fe7..af1adc31f4f9 100644 --- a/packages/ai/src/protocols/google-video.ts +++ b/packages/ai/src/protocols/google-video.ts @@ -1,5 +1,5 @@ import { Duration, Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import type { Status } from "../generation.js" import { Media } from "../media.js" import { MediaProtocol } from "../route/media-protocol.js" diff --git a/packages/ai/src/protocols/meta-images.ts b/packages/ai/src/protocols/meta-images.ts index 4e83f8478012..e0efbe34ae1c 100644 --- a/packages/ai/src/protocols/meta-images.ts +++ b/packages/ai/src/protocols/meta-images.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js" import { Media } from "../media.js" import { MediaProtocol } from "../route/media-protocol.js" diff --git a/packages/ai/src/protocols/meta-responses.ts b/packages/ai/src/protocols/meta-responses.ts index 083a753e866d..33c88caa8c40 100644 --- a/packages/ai/src/protocols/meta-responses.ts +++ b/packages/ai/src/protocols/meta-responses.ts @@ -1,4 +1,5 @@ -import { Effect, Encoding, Schema } from "effect" +import { Effect, Schema } from "effect" +import { Base64 } from "effect/encoding" import { Protocol } from "../route/protocol.js" import { LLMEvent, LLMRequest, Message, ToolResultPart } from "../schema/index.js" import { OpenResponses } from "./open-responses.js" @@ -113,7 +114,7 @@ const HOSTED_TOOLS = { "Meta returned an image without data", ProviderShared.encodeJson(raw), ) - const data = yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe( + const data = yield* Effect.fromResult(Base64.decode(item.result)).pipe( Effect.mapError((cause) => ProviderShared.eventError( ADAPTER, diff --git a/packages/ai/src/protocols/open-responses-channel.ts b/packages/ai/src/protocols/open-responses-channel.ts index 15abd8a62c32..5ff0e956f7a8 100644 --- a/packages/ai/src/protocols/open-responses-channel.ts +++ b/packages/ai/src/protocols/open-responses-channel.ts @@ -1,5 +1,5 @@ import { Effect, Schema, Stream } from "effect" -import { Headers } from "effect/unstable/http" +import { Headers } from "effect/http" import { Framing } from "../route/framing.js" import type { HttpContext } from "../schema/index.js" import { diff --git a/packages/ai/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts index b711db7805b9..55437c254265 100644 --- a/packages/ai/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -1,5 +1,6 @@ -import { Effect, Encoding, Schema } from "effect" -import { Headers } from "effect/unstable/http" +import { Effect, Schema } from "effect" +import { Base64 } from "effect/encoding" +import { Headers } from "effect/http" import { Route } from "../route/client.js" import { Auth } from "../route/auth.js" import { Endpoint } from "../route/endpoint.js" @@ -240,7 +241,7 @@ const checkpointBody = { const hostedToolResult = Effect.fnUntraced(function* (item: ResponsesHostedTools.Item) { const isError = item.error !== undefined && item.error !== null if (item.type === "image_generation_call" && item.result) { - yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe( + yield* Effect.fromResult(Base64.decode(item.result)).pipe( Effect.mapError((cause) => ProviderShared.eventError(ADAPTER, "OpenAI Responses returned invalid image base64", undefined, cause), ), diff --git a/packages/ai/src/protocols/replicate-images.ts b/packages/ai/src/protocols/replicate-images.ts index 087bd57df9e7..f98815198660 100644 --- a/packages/ai/src/protocols/replicate-images.ts +++ b/packages/ai/src/protocols/replicate-images.ts @@ -1,5 +1,5 @@ import { Duration, Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import type { Status } from "../generation.js" import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js" import { Media } from "../media.js" diff --git a/packages/ai/src/protocols/runway-video.ts b/packages/ai/src/protocols/runway-video.ts index bc3a68f064f9..14a9ec8ab064 100644 --- a/packages/ai/src/protocols/runway-video.ts +++ b/packages/ai/src/protocols/runway-video.ts @@ -1,5 +1,5 @@ import { Duration, Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import type { Status } from "../generation.js" import { Media } from "../media.js" import { MediaProtocol } from "../route/media-protocol.js" diff --git a/packages/ai/src/protocols/shared.ts b/packages/ai/src/protocols/shared.ts index 0cfd5f4bcac2..5fc486d1cbd7 100644 --- a/packages/ai/src/protocols/shared.ts +++ b/packages/ai/src/protocols/shared.ts @@ -1,6 +1,6 @@ import { Tool } from "@opencode/schema/tool" import { Effect, Option, Schema } from "effect" -import { Headers, HttpClientRequest } from "effect/unstable/http" +import { Headers, HttpClientRequest } from "effect/http" import { Media } from "../media.js" import { InvalidProviderOutputError, diff --git a/packages/ai/src/protocols/stability-images.ts b/packages/ai/src/protocols/stability-images.ts index ed52c502a378..605ae5604cc4 100644 --- a/packages/ai/src/protocols/stability-images.ts +++ b/packages/ai/src/protocols/stability-images.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js" import { MediaProtocol } from "../route/media-protocol.js" import { MediaRoute } from "../route/media.js" diff --git a/packages/ai/src/protocols/utils/bedrock-auth.ts b/packages/ai/src/protocols/utils/bedrock-auth.ts index 54cd33939cb0..b3ddeff8976b 100644 --- a/packages/ai/src/protocols/utils/bedrock-auth.ts +++ b/packages/ai/src/protocols/utils/bedrock-auth.ts @@ -1,6 +1,6 @@ import { AwsV4Signer } from "aws4fetch" import { Effect } from "effect" -import { Headers } from "effect/unstable/http" +import { Headers } from "effect/http" import { Auth, type AuthInput } from "../../route/auth.js" import { AIError, AuthenticationError } from "../../schema/index.js" import { ProviderShared } from "../shared.js" diff --git a/packages/ai/src/protocols/utils/bedrock-media.ts b/packages/ai/src/protocols/utils/bedrock-media.ts index f93e3c868870..18e883d87b58 100644 --- a/packages/ai/src/protocols/utils/bedrock-media.ts +++ b/packages/ai/src/protocols/utils/bedrock-media.ts @@ -1,4 +1,5 @@ -import { Effect, Encoding, Schema } from "effect" +import { Effect, Schema } from "effect" +import { Base64 } from "effect/encoding" import type { MediaPart } from "../../schema/index.js" import { ProviderShared } from "../shared.js" @@ -78,12 +79,12 @@ function documentName(filename: string | undefined, names: Set) { const mediaBase64 = Effect.fnUntraced(function* (part: MediaPart) { const media = yield* ProviderShared.requireInlineMedia("Bedrock Converse", part.media) - const bytes = yield* Effect.fromResult(Encoding.decodeBase64(media.base64)).pipe( + const bytes = yield* Effect.fromResult(Base64.decode(media.base64)).pipe( Effect.mapError((cause) => ProviderShared.invalidRequest("Bedrock Converse media data must be valid base64", cause), ), ) - return Encoding.encodeBase64(bytes) + return Base64.encode(bytes) }) // Route by MIME. Known image/document formats lower into a typed block; anything diff --git a/packages/ai/src/protocols/utils/fal-queue.ts b/packages/ai/src/protocols/utils/fal-queue.ts index cd2dc5c526b2..02f5c5bb4646 100644 --- a/packages/ai/src/protocols/utils/fal-queue.ts +++ b/packages/ai/src/protocols/utils/fal-queue.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import type { Status } from "../../generation.js" import type { Media } from "../../media.js" import { MediaProtocol } from "../../route/media-protocol.js" diff --git a/packages/ai/src/protocols/utils/media-input.ts b/packages/ai/src/protocols/utils/media-input.ts index 2116b8cb542b..2120e35e040b 100644 --- a/packages/ai/src/protocols/utils/media-input.ts +++ b/packages/ai/src/protocols/utils/media-input.ts @@ -1,4 +1,5 @@ -import { Effect, Encoding } from "effect" +import { Effect } from "effect" +import { Base64 } from "effect/encoding" import { Media } from "../../media.js" import type { MediaProtocol } from "../../route/media-protocol.js" import { mergeJsonRecords, type AIError, type ProviderID } from "../../schema/index.js" @@ -10,7 +11,7 @@ export const inlineBytes = (route: string, asset: Media.Asset): Effect.Effect ProviderShared.invalidRequest(`${route} media contains invalid base64 data`, cause)), ) } @@ -52,7 +53,7 @@ export const decodedAsset = ( mediaType: string | undefined, options?: Media.AssetOptions, ) => - Effect.fromResult(Encoding.decodeBase64(data)).pipe( + Effect.fromResult(Base64.decode(data)).pipe( Effect.mapError((cause) => invalid(`${label} contains invalid base64 data`, cause)), Effect.map((bytes) => Media.bytes(bytes, mediaType, options)), ) diff --git a/packages/ai/src/protocols/utils/tool-schema.ts b/packages/ai/src/protocols/utils/tool-schema.ts index 013cd4d321ab..69b6a6c78693 100644 --- a/packages/ai/src/protocols/utils/tool-schema.ts +++ b/packages/ai/src/protocols/utils/tool-schema.ts @@ -54,9 +54,17 @@ const MODEL_NAMES = [ ] as const // Tool arguments are always a JSON object, and most providers reject a tool schema whose root does not -// declare `type: "object"`, such as `{}` or a bare `properties` map. Effect encodes an empty struct as -// `anyOf` object or array; every object matches its bare object branch, so that `anyOf` is dropped. +// declare `type: "object"`, such as `{}` or a bare `properties` map. An object root makes Effect's +// empty-struct markers redundant: `{ not: { type: "null" } }`, and `anyOf` object or array from older +// releases. Both are dropped so the root is a plain object schema. const objectRoot = (schema: JsonSchema): JsonSchema => { + if ( + (schema.type === undefined || schema.type === "object") && + isRecord(schema.not) && + schema.not.type === "null" && + Object.keys(schema.not).length === 1 + ) + return { type: "object", ...Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "not")) } if (schema.type !== undefined) return schema if ( Array.isArray(schema.anyOf) && diff --git a/packages/ai/src/protocols/xai-images.ts b/packages/ai/src/protocols/xai-images.ts index 01593ffcf373..438b93a701ef 100644 --- a/packages/ai/src/protocols/xai-images.ts +++ b/packages/ai/src/protocols/xai-images.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js" import { Media } from "../media.js" import { MediaProtocol } from "../route/media-protocol.js" diff --git a/packages/ai/src/protocols/xai-video.ts b/packages/ai/src/protocols/xai-video.ts index 45d1eac0b655..e23d92f7f81c 100644 --- a/packages/ai/src/protocols/xai-video.ts +++ b/packages/ai/src/protocols/xai-video.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import type { Status } from "../generation.js" import { Media } from "../media.js" import { MediaProtocol } from "../route/media-protocol.js" diff --git a/packages/ai/src/protocols/zai-images.ts b/packages/ai/src/protocols/zai-images.ts index 736e650361f9..375686a4a22c 100644 --- a/packages/ai/src/protocols/zai-images.ts +++ b/packages/ai/src/protocols/zai-images.ts @@ -1,5 +1,5 @@ import { Duration, Effect, Schema } from "effect" -import type { HttpClientResponse } from "effect/unstable/http" +import type { HttpClientResponse } from "effect/http" import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js" import { MediaProtocol } from "../route/media-protocol.js" import { MediaRoute } from "../route/media.js" diff --git a/packages/ai/src/providers/vercel-ai-gateway.ts b/packages/ai/src/providers/vercel-ai-gateway.ts index 713acdd8606e..50111c0ab4a0 100644 --- a/packages/ai/src/providers/vercel-ai-gateway.ts +++ b/packages/ai/src/providers/vercel-ai-gateway.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import { Headers, HttpClientRequest } from "effect/unstable/http" +import { Headers, HttpClientRequest } from "effect/http" import { EvaluationAnswer, EvaluationInput, diff --git a/packages/ai/src/route/auth.ts b/packages/ai/src/route/auth.ts index 5494a86ebdcc..9a8a4d9a2178 100644 --- a/packages/ai/src/route/auth.ts +++ b/packages/ai/src/route/auth.ts @@ -1,5 +1,5 @@ import { Config, Effect, Option, Redacted } from "effect" -import { Headers } from "effect/unstable/http" +import { Headers } from "effect/http" import { AuthenticationError, AIError, type HttpOptions } from "../schema/index.js" export class MissingCredentialError extends Error { @@ -95,7 +95,7 @@ export const optional = (secret: Secret | undefined, source = "optional value") export const config = (name: string) => credential( Effect.gen(function* () { - const secret = yield* Config.option(Config.redacted(name)) + const secret = yield* Config.option(Config.Redacted(name)) if (Option.isSome(secret) && Redacted.value(secret.value) !== "") return secret.value return yield* Effect.fail(new MissingCredentialError(name, `${name} is not set`)) }), diff --git a/packages/ai/src/route/executor-service.ts b/packages/ai/src/route/executor-service.ts index 5cd0c3fd24c1..8087867c9bdc 100644 --- a/packages/ai/src/route/executor-service.ts +++ b/packages/ai/src/route/executor-service.ts @@ -1,5 +1,5 @@ import { Context, type Effect } from "effect" -import type { HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import type { HttpClientRequest, HttpClientResponse } from "effect/http" import type { AIError } from "../schema/errors.js" // The service tag lives in its own leaf module so `Media.Asset` (imported by the schema layer) can require the diff --git a/packages/ai/src/route/executor.ts b/packages/ai/src/route/executor.ts index 242170701043..ed3d89818753 100644 --- a/packages/ai/src/route/executor.ts +++ b/packages/ai/src/route/executor.ts @@ -6,7 +6,7 @@ import { HttpClientError, HttpClientRequest, HttpClientResponse, -} from "effect/unstable/http" +} from "effect/http" import { HttpContext, HttpRateLimitDetails, AIError, TransportError } from "../schema/index.js" import { classifyProviderFailure, providerErrorMessage } from "../provider-error.js" import { Service, type HttpMiddleware, type Interface } from "./executor-service.js" diff --git a/packages/ai/src/route/framing.ts b/packages/ai/src/route/framing.ts index f098d3bd564a..4ff92c2abc60 100644 --- a/packages/ai/src/route/framing.ts +++ b/packages/ai/src/route/framing.ts @@ -1,5 +1,5 @@ import { Effect, Stream } from "effect" -import { makeParser } from "effect/unstable/encoding/Sse" +import { makeParser } from "effect/encoding/Sse" import { AIError, InvalidProviderOutputError } from "../schema/index.js" /** diff --git a/packages/ai/src/route/media-protocol.ts b/packages/ai/src/route/media-protocol.ts index 27b8d6de5b29..47514a352c99 100644 --- a/packages/ai/src/route/media-protocol.ts +++ b/packages/ai/src/route/media-protocol.ts @@ -1,5 +1,5 @@ import { Clock, Duration, Effect, Schema, type Stream } from "effect" -import { HttpClientResponse } from "effect/unstable/http" +import { HttpClientResponse } from "effect/http" import type { Snapshot, Status } from "../generation.js" import { Media } from "../media.js" import type { AuthInput } from "./auth.js" diff --git a/packages/ai/src/route/media.ts b/packages/ai/src/route/media.ts index 884fece763ea..5af63162d68d 100644 --- a/packages/ai/src/route/media.ts +++ b/packages/ai/src/route/media.ts @@ -1,5 +1,5 @@ import { Duration, Effect, Schedule, Schema, Stream } from "effect" -import { Headers, HttpClientRequest, type HttpClientResponse } from "effect/unstable/http" +import { Headers, HttpClientRequest, type HttpClientResponse } from "effect/http" import { Auth, type AuthInput } from "./auth.js" import { Endpoint } from "./endpoint.js" import { RequestExecutorService, type Interface } from "./executor-service.js" diff --git a/packages/ai/src/route/transport/http.ts b/packages/ai/src/route/transport/http.ts index 5e330f61d5c4..c87f0d75c2d5 100644 --- a/packages/ai/src/route/transport/http.ts +++ b/packages/ai/src/route/transport/http.ts @@ -1,5 +1,5 @@ import { Clock, Duration, Effect, Stream } from "effect" -import { Headers, HttpClientRequest } from "effect/unstable/http" +import { Headers, HttpClientRequest } from "effect/http" import { Auth } from "../auth.js" import { render as renderEndpoint } from "../endpoint.js" import { Framing } from "../framing.js" diff --git a/packages/ai/src/route/transport/websocket-channel.ts b/packages/ai/src/route/transport/websocket-channel.ts index f858c828585c..8c5c69ee2afc 100644 --- a/packages/ai/src/route/transport/websocket-channel.ts +++ b/packages/ai/src/route/transport/websocket-channel.ts @@ -1,5 +1,5 @@ import type { Effect, Scope, Stream } from "effect" -import type { Headers } from "effect/unstable/http" +import type { Headers } from "effect/http" import type { AIError, HttpContext } from "../../schema/index.js" export interface WebSocketChannelExecutor { diff --git a/packages/ai/src/route/transport/websocket.ts b/packages/ai/src/route/transport/websocket.ts index 647c259008bc..f01c6acb63db 100644 --- a/packages/ai/src/route/transport/websocket.ts +++ b/packages/ai/src/route/transport/websocket.ts @@ -1,6 +1,6 @@ import { Cause, Effect, Queue, Stream } from "effect" -import { Headers } from "effect/unstable/http" -import { Socket } from "effect/unstable/socket" +import { Headers } from "effect/http" +import { Socket } from "effect/socket" import { AIError, AIErrorReason, @@ -33,11 +33,6 @@ export interface WebSocketConnector { readonly open: (input: WebSocketRequest) => Effect.Effect } -type WebSocketConstructorWithHeaders = ( - url: string, - options?: { readonly headers?: Headers.Headers }, -) => globalThis.WebSocket - const MAX_FRAME_BYTES = 16 * 1024 * 1024 const transportError = ( message: string, @@ -81,9 +76,9 @@ const annotateTransportError = ( }) : error -const eventMessage = (event: Event) => { +const eventMessage = (event: Socket.WebSocketEvent) => { if ("message" in event && typeof event.message === "string") return event.message - return event.type + return event.type ?? "error" } const binaryMessage = (data: unknown) => { @@ -93,7 +88,7 @@ const binaryMessage = (data: unknown) => { return undefined } -const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => { +const waitOpen = (ws: Socket.WebSocketLike, input: WebSocketRequest) => { if (ws.readyState === globalThis.WebSocket.OPEN) return Effect.void if (ws.readyState === globalThis.WebSocket.CLOSING || ws.readyState === globalThis.WebSocket.CLOSED) { return Effect.fail( @@ -125,7 +120,7 @@ const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => { cleanup() resume(Effect.void) } - const onError = (event: Event) => { + const onError = (event: Socket.WebSocketEvent) => { cleanup() resume( Effect.fail( @@ -139,7 +134,7 @@ const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => { ), ) } - const onClose = (event: CloseEvent) => { + const onClose = (event: Socket.WebSocketEvent) => { cleanup() resume( Effect.fail( @@ -191,12 +186,7 @@ export const open = (input: WebSocketRequest) => Effect.gen(function* () { const constructor = yield* Socket.WebSocketConstructor const ws = yield* Effect.try({ - try: () => - // Platform implementations may extend Effect's browser-compatible constructor with handshake options. - // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion - (constructor as unknown as WebSocketConstructorWithHeaders)(input.url, { - headers: input.headers, - }), + try: () => constructor(input.url, { headers: input.headers }), catch: (error) => transportError(error instanceof Error ? error.message : "Failed to construct WebSocket", { cause: error, @@ -210,7 +200,7 @@ export const open = (input: WebSocketRequest) => }) export const fromWebSocket = ( - ws: globalThis.WebSocket, + ws: Socket.WebSocketLike, input: WebSocketRequest, ): Effect.Effect => Effect.gen(function* () { @@ -243,7 +233,7 @@ export const fromWebSocket = ( Queue.offerUnsafe(messages, message) } - const onMessage = (event: MessageEvent) => { + const onMessage = (event: Socket.WebSocketEvent) => { if (typeof event.data === "string") return offer(event.data) const binary = binaryMessage(event.data) if (binary) return offer(binary) @@ -260,7 +250,7 @@ export const fromWebSocket = ( ), ) } - const onError = (event: Event) => { + const onError = (event: Socket.WebSocketEvent) => { Queue.failCauseUnsafe( messages, Cause.fail( @@ -274,7 +264,7 @@ export const fromWebSocket = ( ), ) } - const onClose = (event: CloseEvent) => { + const onClose = (event: Socket.WebSocketEvent) => { Queue.failCauseUnsafe( messages, Cause.fail( diff --git a/packages/ai/src/schema/messages.ts b/packages/ai/src/schema/messages.ts index 5787e3dcc683..cb27dd00e350 100644 --- a/packages/ai/src/schema/messages.ts +++ b/packages/ai/src/schema/messages.ts @@ -1,4 +1,5 @@ import { Schema } from "effect" +import { withStatics } from "@opencode/schema/schema" import { Tool } from "@opencode/schema/tool" import { CacheHint, @@ -28,7 +29,7 @@ export type SystemPart = Schema.Schema.Type const makeSystemPart = (text: string): SystemPart => ({ type: "text", text }) -export const SystemPart = Object.assign(systemPartSchema, { +export const SystemPart = withStatics(systemPartSchema, { make: makeSystemPart, content: (input?: string | SystemPart | ReadonlyArray) => { if (input === undefined) return [] @@ -76,7 +77,7 @@ const toolResultValueSchema = Schema.Union([ export type ToolResultValue = Schema.Schema.Type const isToolResultValue = Schema.is(toolResultValueSchema) -export const ToolResultValue = Object.assign(toolResultValueSchema, { +export const ToolResultValue = withStatics(toolResultValueSchema, { is: isToolResultValue, make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => { if (isToolResultValue(value)) return value @@ -90,7 +91,7 @@ export interface ToolOutput { readonly content: ReadonlyArray } -export const ToolOutput = Object.assign( +export const ToolOutput = withStatics( Schema.Struct({ structured: Schema.Unknown, content: Schema.Array(Tool.Content), @@ -127,7 +128,7 @@ const toolResultText = (value: unknown) => { } } -export const ToolCallPart = Object.assign( +export const ToolCallPart = withStatics( Schema.Struct({ type: Schema.Literal("tool-call"), id: Schema.String, @@ -145,7 +146,7 @@ export const ToolCallPart = Object.assign( ) export type ToolCallPart = Schema.Schema.Type -export const ToolResultPart = Object.assign( +export const ToolResultPart = withStatics( Schema.Struct({ type: Schema.Literal("tool-result"), id: Schema.String, @@ -209,7 +210,7 @@ const compactionPartSchema = Schema.Struct({ ) .annotate({ identifier: "LLM.Content.Compaction" }) export type CompactionPart = typeof compactionPartSchema.Type -export const CompactionPart = Object.assign(compactionPartSchema, { +export const CompactionPart = withStatics(compactionPartSchema, { make: (input: Omit & CompactionContent): CompactionPart => Schema.decodeUnknownSync(compactionPartSchema)({ type: "compaction", ...input }), }) @@ -326,7 +327,7 @@ export type ToolNamespaceEntryInput = ToolNamespaceInput & { readonly type: "nam export const ToolNamespace: Schema.Codec & { readonly make: (input: ToolNamespace | ToolNamespaceInput) => ToolNamespace -} = Object.assign( +} = withStatics( Schema.Struct({ type: Schema.Literal("namespace"), name: Schema.String, @@ -346,7 +347,7 @@ export type ToolEntry = ToolDefinition | ToolNamespace export type ToolEntryInput = ToolDefinition.Input | ToolNamespaceEntryInput export const ToolEntry: Schema.Codec & { readonly make: (input: ToolEntryInput) => ToolEntry -} = Object.assign( +} = withStatics( Schema.Union([ToolDefinition, ToolNamespace]).pipe( Schema.toTaggedUnion("type"), Schema.annotate({ identifier: "LLM.ToolEntry" }), diff --git a/packages/ai/src/tool.ts b/packages/ai/src/tool.ts index c04b666887c0..46eab97d44ff 100644 --- a/packages/ai/src/tool.ts +++ b/packages/ai/src/tool.ts @@ -6,6 +6,7 @@ import type { ToolOutput as ToolOutputType, } from "./schema/index.js" import { ToolDefinition, ToolFailure, ToolOutput } from "./schema/index.js" +import { isRecord } from "./utils/record.js" /** * Schema constraint for tool parameters / success values: no decoding or @@ -200,7 +201,7 @@ export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool { _definition: new ToolDefinition({ name: "", description: config.description, - inputSchema: toJsonSchema(config.parameters), + inputSchema: emptyInputJsonSchema(config.parameters) ?? toJsonSchema(config.parameters), outputSchema: toJsonSchema(config.success), }), } @@ -230,8 +231,26 @@ export const toDefinitions = (tools: Tools): ReadonlyArray }), ) +/** + * Describes a no-argument Effect tool input as an empty object, or returns + * `undefined` when the input takes arguments. + * + * Effect emits `{ not: { type: "null" } }` only for an encoded struct with no + * properties or index signatures, but providers require tool parameters to + * describe an object. Annotations are kept; any other keyword, such as a + * check's `minProperties`, leaves the schema as Effect emitted it. + */ +export const emptyInputJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema | undefined => { + const { not, ...rest } = Schema.toJsonSchemaDocument(schema, { referencePolicy: () => undefined }).schema + if (!isRecord(not) || not.type !== "null" || Object.keys(not).length !== 1) return undefined + if (!Object.keys(rest).every((key) => annotationKeywords.has(key))) return undefined + return { ...rest, type: "object", properties: {}, additionalProperties: false } +} + +const annotationKeywords = new Set(["title", "description", "default", "examples", "readOnly", "writeOnly"]) + const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => { - const document = Schema.toJsonSchemaDocument(schema) + const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: "error" }) if (Object.keys(document.definitions).length === 0) return document.schema return { ...document.schema, $defs: document.definitions } } diff --git a/packages/ai/test/auth-options.types.ts b/packages/ai/test/auth-options.types.ts index d9f8264b1409..dcebd156e007 100644 --- a/packages/ai/test/auth-options.types.ts +++ b/packages/ai/test/auth-options.types.ts @@ -29,7 +29,7 @@ type LanguageModel = { declare const auth: Auth.Definition declare const optionalAuthModel: LanguageModelFactory declare const requiredAuthModel: LanguageModelFactory -const configApiKey = Config.redacted("OPENAI_API_KEY") +const configApiKey = Config.Redacted("OPENAI_API_KEY") OpenAIChat.route.model({ id: "gpt-4.1-mini" }) diff --git a/packages/ai/test/auth.test.ts b/packages/ai/test/auth.test.ts index cabf3013bbd6..d76d75b3ef5f 100644 --- a/packages/ai/test/auth.test.ts +++ b/packages/ai/test/auth.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { ConfigProvider, Effect } from "effect" -import { Headers } from "effect/unstable/http" +import { Headers } from "effect/http" import { LLM } from "../src/index.js" import { Auth } from "../src/route/auth.js" import * as OpenAIChat from "../src/protocols/openai-chat.js" diff --git a/packages/ai/test/compaction-http.test.ts b/packages/ai/test/compaction-http.test.ts index 9472463762ec..8b235ab650b6 100644 --- a/packages/ai/test/compaction-http.test.ts +++ b/packages/ai/test/compaction-http.test.ts @@ -1,6 +1,6 @@ import { expect } from "bun:test" import { Effect } from "effect" -import { FetchHttpClient } from "effect/unstable/http" +import { FetchHttpClient } from "effect/http" import { LLM, LLMRequest, Message } from "../src/index.js" import { LLMClient } from "../src/route/client.js" import { OpenAI } from "../src/providers.js" diff --git a/packages/ai/test/compile.test.ts b/packages/ai/test/compile.test.ts index e72bcf6fe178..2171993c57be 100644 --- a/packages/ai/test/compile.test.ts +++ b/packages/ai/test/compile.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { Effect, Ref, Schema } from "effect" -import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClientRequest, HttpClientResponse } from "effect/http" import { LLM, LLMRequest, diff --git a/packages/ai/test/evaluation.test.ts b/packages/ai/test/evaluation.test.ts index 8f821e202ca6..d0f4245a8ce6 100644 --- a/packages/ai/test/evaluation.test.ts +++ b/packages/ai/test/evaluation.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { Evaluation, EvaluationClient } from "../src/experimental.js" import { OpenCodeZen, OpenRouter, TypeSafeAI, VercelAIGateway } from "../src/providers.js" import { it } from "./lib/effect.js" diff --git a/packages/ai/test/executor.test.ts b/packages/ai/test/executor.test.ts index f4edd2199a38..a0bd3d17ea9c 100644 --- a/packages/ai/test/executor.test.ts +++ b/packages/ai/test/executor.test.ts @@ -1,6 +1,7 @@ import { describe, expect } from "bun:test" import { Deferred, Effect, Fiber, Layer, Ref, Stream } from "effect" -import { Headers, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { Headers, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http" +import { Socket } from "effect/socket" import { LLM, AIError, HttpContext, InvalidProviderOutputError, TransportError, isRetryable } from "../src/index.js" import { LLMClient, @@ -711,11 +712,18 @@ describe("WebSocket channel execution", () => { close() {} } const socket = new TestSocket() - const connection = yield* WebSocketTransport.fromWebSocket( - // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion - socket as unknown as globalThis.WebSocket, - { url: "wss://provider.test/responses", headers: Headers.empty }, + const headers = Headers.fromInput({ authorization: "Bearer secret", "openai-beta": "responses-websocket" }) + let observed: { url: string; options: Socket.WebSocketConstructorOptions | undefined } | undefined + const connection = yield* WebSocketTransport.open({ + url: "wss://provider.test/responses", + headers, + }).pipe( + Effect.provideService(Socket.WebSocketConstructor, (url, options) => { + observed = { url, options } + return socket + }), ) + expect(observed).toEqual({ url: "wss://provider.test/responses", options: { headers } }) const event = new CloseEvent("close", { code: 1011, reason: "upstream trace: req_close" }) socket.dispatchEvent(event) const error = yield* connection.messages.pipe(Stream.runDrain, Effect.flip) @@ -740,11 +748,10 @@ describe("WebSocket channel execution", () => { close() {} } const socket = new TestSocket() - const open = WebSocketTransport.fromWebSocket( - // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion - socket as unknown as globalThis.WebSocket, - { url: "wss://provider.test/responses", headers: Headers.empty }, - ) + const open = WebSocketTransport.fromWebSocket(socket, { + url: "wss://provider.test/responses", + headers: Headers.empty, + }) const fiber = yield* open.pipe(Effect.flip, Effect.forkChild({ startImmediately: true })) const event = new ErrorEvent("error", { message: "handshake rejected", error: cause }) socket.dispatchEvent(event) @@ -902,11 +909,10 @@ describe("WebSocket channel execution", () => { close() {} } const socket = new ClosedBeforeSend() - const connection = yield* WebSocketTransport.fromWebSocket( - // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion - socket as unknown as globalThis.WebSocket, - { url: "wss://api.openai.test/v1/responses", headers: Headers.empty }, - ) + const connection = yield* WebSocketTransport.fromWebSocket(socket, { + url: "wss://api.openai.test/v1/responses", + headers: Headers.empty, + }) socket.readyState = globalThis.WebSocket.CLOSED const error = yield* connection.sendText("create").pipe(Effect.flip) diff --git a/packages/ai/test/image.test.ts b/packages/ai/test/image.test.ts index 9b6768a6cef6..7a6435e88d1a 100644 --- a/packages/ai/test/image.test.ts +++ b/packages/ai/test/image.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Layer, Stream } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { Image, ImageClient, Media } from "../src/index.js" import { BlackForestLabs, Fal, Google, OpenAI, Replicate, Stability, XAI, ZAI } from "../src/providers.js" import { it } from "./lib/effect.js" diff --git a/packages/ai/test/lib/http.ts b/packages/ai/test/lib/http.ts index 11196323db1e..35e30fef94a8 100644 --- a/packages/ai/test/lib/http.ts +++ b/packages/ai/test/lib/http.ts @@ -1,6 +1,6 @@ import { Effect, Fiber, Layer, Ref } from "effect" import * as TestClock from "effect/testing/TestClock" -import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http" import { LLMClient, RequestExecutor } from "../../src/route.js" import type { Service as LLMClientService } from "../../src/route/client.js" import type { Service as RequestExecutorService } from "../../src/route/executor.js" diff --git a/packages/ai/test/media.test.ts b/packages/ai/test/media.test.ts index 32fc8f65560a..27245a8e08a4 100644 --- a/packages/ai/test/media.test.ts +++ b/packages/ai/test/media.test.ts @@ -2,7 +2,7 @@ import { describe, expect } from "bun:test" import { NodeFileSystem } from "@effect/platform-node" import { Effect, Ref, Schema } from "effect" import { FileSystem } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { AIError, Media, Message } from "../src/index.js" import { it } from "./lib/effect.js" import { dynamicResponse, scriptedResponses } from "./lib/http.js" diff --git a/packages/ai/test/promise.test.ts b/packages/ai/test/promise.test.ts index cc3ede886f0a..c3c53c6a23e9 100644 --- a/packages/ai/test/promise.test.ts +++ b/packages/ai/test/promise.test.ts @@ -3,7 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" import { Effect, Layer } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { AIError, LLMEvent, Media, SpeechEvent, TranscriptionEvent } from "../src/index.js" import { RequestExecutor } from "../src/route.js" import { AI } from "../src/promise.js" diff --git a/packages/ai/test/provider/alibaba.test.ts b/packages/ai/test/provider/alibaba.test.ts index 92b955f1d5bc..908f939de6c1 100644 --- a/packages/ai/test/provider/alibaba.test.ts +++ b/packages/ai/test/provider/alibaba.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test" import { ConfigProvider, Effect } from "effect" -import { Headers } from "effect/unstable/http" +import { Headers } from "effect/http" import { Auth, LLM, LLMClient, LLMRequest, Message, ReasoningPart, ToolDefinition } from "../../src/index.js" import { Alibaba } from "../../src/providers.js" import { compileRequest } from "../../src/route/client.js" diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index 7ce174a94d5f..37e021301c14 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { CacheHint, LLM, diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index 0a85e7bcffd7..251581254940 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -1,8 +1,9 @@ import { EventStreamCodec } from "@smithy/eventstream-codec" import { fromUtf8, toUtf8 } from "@smithy/util-utf8" import { describe, expect } from "bun:test" -import { Effect, Encoding, Ref, Schema, Stream } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { Effect, Ref, Schema, Stream } from "effect" +import { Base64 } from "effect/encoding" +import { HttpClientRequest } from "effect/http" import { Media, CacheHint, @@ -865,7 +866,7 @@ describe("Bedrock Converse route", () => { reason: { _tag: "InvalidProviderOutput", classification: "incomplete-stream" }, message: `Incomplete Bedrock Converse event-stream frame: ${partial.length} buffered bytes remain at end of stream`, }) - expect(error.reason.body).toBe(Encoding.encodeBase64(partial)) + expect(error.reason.body).toBe(Base64.encode(partial)) } }), ) diff --git a/packages/ai/test/provider/bedrock-mantle.test.ts b/packages/ai/test/provider/bedrock-mantle.test.ts index 7a682384e59e..c4c8085b258a 100644 --- a/packages/ai/test/provider/bedrock-mantle.test.ts +++ b/packages/ai/test/provider/bedrock-mantle.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { LLM, LLMEvent, LLMRequest, Message, ToolDefinition } from "../../src/index.js" import { AmazonBedrockMantle } from "../../src/providers.js" import { OpenResponses } from "../../src/protocols/open-responses.js" diff --git a/packages/ai/test/provider/cloudflare.test.ts b/packages/ai/test/provider/cloudflare.test.ts index de5e574714d4..7123a825c6ff 100644 --- a/packages/ai/test/provider/cloudflare.test.ts +++ b/packages/ai/test/provider/cloudflare.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { ConfigProvider, Effect, Schema } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { LLM, LLMEvent } from "../../src/index.js" import { CloudflareAIGateway } from "../../src/providers/cloudflare-ai-gateway.js" import { CloudflareWorkersAI } from "../../src/providers/cloudflare-workers-ai.js" diff --git a/packages/ai/test/provider/google-interactions.recorded.test.ts b/packages/ai/test/provider/google-interactions.recorded.test.ts index 4a4a33e5e080..59edb401d206 100644 --- a/packages/ai/test/provider/google-interactions.recorded.test.ts +++ b/packages/ai/test/provider/google-interactions.recorded.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Schema } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { LLM, LLMEvent, Message } from "../../src/index.js" import { Google } from "../../src/providers.js" import { LLMClient, RequestExecutor } from "../../src/route.js" diff --git a/packages/ai/test/provider/google-vertex.test.ts b/packages/ai/test/provider/google-vertex.test.ts index eb0e22aebc77..e09aeac3d678 100644 --- a/packages/ai/test/provider/google-vertex.test.ts +++ b/packages/ai/test/provider/google-vertex.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { Effect } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { LanguageModel, LLM, Message, ToolCallPart } from "../../src/index.js" import { GoogleVertex, GoogleVertexChat, GoogleVertexMessages, GoogleVertexResponses } from "../../src/providers.js" import { LLMClient } from "../../src/route.js" diff --git a/packages/ai/test/provider/meta-capabilities.test.ts b/packages/ai/test/provider/meta-capabilities.test.ts index 604e32b66938..ebc7e1afd16c 100644 --- a/packages/ai/test/provider/meta-capabilities.test.ts +++ b/packages/ai/test/provider/meta-capabilities.test.ts @@ -1,6 +1,6 @@ import { expect } from "bun:test" import { Effect, Layer } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { Image, ImageClient, LLM, LLMEvent, LLMRequest, Media, Message, ToolDefinition } from "../../src/index.js" import { Meta } from "../../src/providers/meta.js" import { MetaMessages } from "../../src/protocols/meta-messages.js" diff --git a/packages/ai/test/provider/meta.test.ts b/packages/ai/test/provider/meta.test.ts index 5cfa52ff886f..293ae904ddd2 100644 --- a/packages/ai/test/provider/meta.test.ts +++ b/packages/ai/test/provider/meta.test.ts @@ -1,6 +1,6 @@ import { expect } from "bun:test" import { ConfigProvider, Effect } from "effect" -import { Headers } from "effect/unstable/http" +import { Headers } from "effect/http" import { Auth, LLM, LLMClient } from "../../src/index.js" import { Meta } from "../../src/providers/index.js" import { OpenAIChat } from "../../src/protocols/openai-chat.js" diff --git a/packages/ai/test/provider/minimax.test.ts b/packages/ai/test/provider/minimax.test.ts index eb12815efad3..bc4c8112d9e7 100644 --- a/packages/ai/test/provider/minimax.test.ts +++ b/packages/ai/test/provider/minimax.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { ConfigProvider, Effect } from "effect" -import { Headers } from "effect/unstable/http" +import { Headers } from "effect/http" import { LLM } from "../../src/index.js" import { MiniMax } from "../../src/providers.js" import { AnthropicMessages } from "../../src/protocols/anthropic-messages.js" diff --git a/packages/ai/test/provider/mistral-chat.test.ts b/packages/ai/test/provider/mistral-chat.test.ts index 357f12691fb4..fc2535392fb7 100644 --- a/packages/ai/test/provider/mistral-chat.test.ts +++ b/packages/ai/test/provider/mistral-chat.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { ConfigProvider, Effect } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { LLM, LLMEvent, Message, SystemPart, ToolDefinition, Media } from "../../src/index.js" import { Mistral } from "../../src/providers/index.js" import { MistralChat } from "../../src/protocols/index.js" diff --git a/packages/ai/test/provider/moonshot.test.ts b/packages/ai/test/provider/moonshot.test.ts index 9bcf0aa5d7f0..cbc1a9a5b251 100644 --- a/packages/ai/test/provider/moonshot.test.ts +++ b/packages/ai/test/provider/moonshot.test.ts @@ -1,6 +1,6 @@ import { expect } from "bun:test" import { ConfigProvider, Effect } from "effect" -import { Headers } from "effect/unstable/http" +import { Headers } from "effect/http" import { Auth, LLM, LLMClient, Message, ReasoningPart, ToolDefinition } from "../../src/index.js" import { Moonshot } from "../../src/providers.js" import { AnthropicMessages } from "../../src/protocols/anthropic-messages.js" diff --git a/packages/ai/test/provider/native-providers.test.ts b/packages/ai/test/provider/native-providers.test.ts index 1d08278c523e..55f60d0a7933 100644 --- a/packages/ai/test/provider/native-providers.test.ts +++ b/packages/ai/test/provider/native-providers.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { ConfigProvider, Effect } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { LLM, Message, ToolDefinition } from "../../src/index.js" import { AmazonBedrock, diff --git a/packages/ai/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts index 5179a888ba94..46576a740949 100644 --- a/packages/ai/test/provider/openai-chat.test.ts +++ b/packages/ai/test/provider/openai-chat.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Ref, Schema, Stream } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { Media, HttpOptions, diff --git a/packages/ai/test/provider/openai-compatible-chat.test.ts b/packages/ai/test/provider/openai-compatible-chat.test.ts index c7c9405f1438..b1f45be09bd9 100644 --- a/packages/ai/test/provider/openai-compatible-chat.test.ts +++ b/packages/ai/test/provider/openai-compatible-chat.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Schema } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { LLM, LLMRequest, Message, ToolCallPart, ToolChoice, ToolDefinition } from "../../src/index.js" import { Auth, LLMClient } from "../../src/route.js" import { compileRequest } from "../../src/route/client.js" diff --git a/packages/ai/test/provider/openai-responses-websocket.recorded.test.ts b/packages/ai/test/provider/openai-responses-websocket.recorded.test.ts index 38e62cbc3334..667248bdea2e 100644 --- a/packages/ai/test/provider/openai-responses-websocket.recorded.test.ts +++ b/packages/ai/test/provider/openai-responses-websocket.recorded.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Stream } from "effect" -import { Socket } from "effect/unstable/socket" +import { Socket } from "effect/socket" import { LLM, LLMRequest, Message, ToolRuntime } from "../../src/index.js" import { LLMClient, diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index f634dafddef8..5d481a0150e2 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { ConfigProvider, Effect, Layer, Logger, Ref, Schema, Stream } from "effect" -import { Headers, HttpClientRequest } from "effect/unstable/http" +import { Headers, HttpClientRequest } from "effect/http" import { Media, LLM, diff --git a/packages/ai/test/provider/venice.test.ts b/packages/ai/test/provider/venice.test.ts index 877599a48099..91914bc00c66 100644 --- a/packages/ai/test/provider/venice.test.ts +++ b/packages/ai/test/provider/venice.test.ts @@ -1,6 +1,6 @@ import { expect } from "bun:test" import { ConfigProvider, Effect } from "effect" -import { Headers } from "effect/unstable/http" +import { Headers } from "effect/http" import { Auth, LLM, LLMClient, Message } from "../../src/index.js" import { OpenAI, Venice } from "../../src/providers.js" import { compileRequest } from "../../src/route/client.js" diff --git a/packages/ai/test/provider/xai-images.test.ts b/packages/ai/test/provider/xai-images.test.ts index de7366e9ae2f..29a891ef9c55 100644 --- a/packages/ai/test/provider/xai-images.test.ts +++ b/packages/ai/test/provider/xai-images.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" -import { Headers, HttpClientRequest } from "effect/unstable/http" +import { Headers, HttpClientRequest } from "effect/http" import { Image, ImageClient } from "../../src/index.js" import { XAI } from "../../src/providers.js" import { Auth } from "../../src/route.js" diff --git a/packages/ai/test/provider/zai-images.test.ts b/packages/ai/test/provider/zai-images.test.ts index c33d3fe483d6..43187a7c034f 100644 --- a/packages/ai/test/provider/zai-images.test.ts +++ b/packages/ai/test/provider/zai-images.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { Image, ImageClient } from "../../src/index.js" import { ZAI } from "../../src/providers.js" import { it } from "../lib/effect.js" diff --git a/packages/ai/test/provider/zai.test.ts b/packages/ai/test/provider/zai.test.ts index b7986b7e36f4..ccbe8b9531f2 100644 --- a/packages/ai/test/provider/zai.test.ts +++ b/packages/ai/test/provider/zai.test.ts @@ -1,6 +1,6 @@ import { expect } from "bun:test" import { ConfigProvider, Effect } from "effect" -import { Headers } from "effect/unstable/http" +import { Headers } from "effect/http" import { Auth, LLM, LLMClient, Message, ReasoningPart, ToolDefinition } from "../../src/index.js" import { ZAI, ZAICodingPlan } from "../../src/providers.js" import { compileRequest } from "../../src/route/client.js" diff --git a/packages/ai/test/recorded-test.ts b/packages/ai/test/recorded-test.ts index 2c507f5a0ddd..005773a2a9a8 100644 --- a/packages/ai/test/recorded-test.ts +++ b/packages/ai/test/recorded-test.ts @@ -1,7 +1,7 @@ import { HttpRecorder } from "@opencode/http-recorder" import { NodeSocket } from "@effect/platform-node" import { Layer } from "effect" -import { Socket } from "effect/unstable/socket" +import { Socket } from "effect/socket" import * as path from "node:path" import { fileURLToPath } from "node:url" import { LLMClient, RequestExecutor } from "../src/route.js" diff --git a/packages/ai/test/speech.test.ts b/packages/ai/test/speech.test.ts index 6d88a7aea22b..3d2810db65b2 100644 --- a/packages/ai/test/speech.test.ts +++ b/packages/ai/test/speech.test.ts @@ -1,5 +1,6 @@ import { describe, expect } from "bun:test" -import { Effect, Encoding, Layer, Stream } from "effect" +import { Effect, Layer, Stream } from "effect" +import { Base64 } from "effect/encoding" import { Speech, SpeechClient, SpeechEvent } from "../src/index.js" import { Cartesia, Deepgram, ElevenLabs, Google, OpenAI } from "../src/providers.js" import { it } from "./lib/effect.js" @@ -47,7 +48,7 @@ describe("Speech", () => { JSON.stringify({ candidates: [ { - content: { parts: [{ inlineData: { mimeType: "audio/wav", data: Encoding.encodeBase64(bytes) } }] }, + content: { parts: [{ inlineData: { mimeType: "audio/wav", data: Base64.encode(bytes) } }] }, finishReason: "STOP", }, ], @@ -244,7 +245,7 @@ describe("Speech", () => { Effect.gen(function* () { const record = (bytes: ReadonlyArray, character: string, start: number) => JSON.stringify({ - audio_base64: Encoding.encodeBase64(Uint8Array.from(bytes)), + audio_base64: Base64.encode(Uint8Array.from(bytes)), alignment: { characters: [character], character_start_times_seconds: [start], diff --git a/packages/ai/test/tool-schema-projection.test.ts b/packages/ai/test/tool-schema-projection.test.ts index 648233a52190..a4f7e267d03e 100644 --- a/packages/ai/test/tool-schema-projection.test.ts +++ b/packages/ai/test/tool-schema-projection.test.ts @@ -1,14 +1,201 @@ import { describe, expect, test } from "bun:test" -import { Effect, Schema } from "effect" +import { Effect, Schema, SchemaGetter } from "effect" import { LLM } from "../src/index.js" -import { OpenAIChat } from "../src/protocols.js" +import { AnthropicMessages, Gemini, OpenAIChat, OpenAIResponses } from "../src/protocols.js" import { ToolSchemaProjection } from "../src/protocols/utils/tool-schema.js" import { Tool, toDefinitions } from "../src/tool.js" import { Auth } from "../src/route.js" import { compileRequest } from "../src/route/client.js" import { it } from "./lib/effect.js" +const Token = Schema.Struct({ token: Schema.String }) +const token = { + type: "object", + properties: { token: { type: "string" } }, + required: ["token"], + additionalProperties: false, +} +const fromEmpty = { + decode: SchemaGetter.transform(() => ({ token: "default" })), + encode: SchemaGetter.transform(() => ({})), +} +// Callers send `{}`; the handler receives a token. +const defaulted = Schema.Struct({}).pipe(Schema.decodeTo(Token, fromEmpty)) +// Callers send a token; the handler receives an empty struct. +const reversed = Token.pipe( + Schema.decodeTo(Schema.Struct({}), { + decode: SchemaGetter.transform(() => ({})), + encode: SchemaGetter.transform(() => ({ token: "default" })), + }), +) + describe("tool schema projections", () => { + test("only normalizes typed empty input structs, preserving raw schemas and output schemas", () => { + const schema = { not: { type: "null" } } + const definitions = Tool.toDefinitions({ + typed: Tool.make({ + description: "Typed", + parameters: Schema.Struct({}), + success: Schema.Struct({}), + execute: () => Effect.succeed({}), + }), + raw: Tool.make({ description: "Raw", jsonSchema: schema, execute: () => Effect.succeed({}) }), + }) + expect(definitions[0]?.inputSchema).toEqual({ type: "object", properties: {}, additionalProperties: false }) + expect(definitions[0]?.outputSchema).toEqual(schema) + expect(definitions[1]?.inputSchema).toEqual(schema) + }) + + test("retains empty input descriptions and preserves named outputs and explicit raw schemas", () => { + const parameters = Schema.Struct({}).annotate({ identifier: "Ping", description: "No arguments" }) + const raw = { $ref: "#/$defs/Raw", $defs: { Raw: { not: { type: "null" }, description: "Raw schema" } } } + const definitions = Tool.toDefinitions({ + typed: Tool.make({ description: "Ping", parameters, success: parameters, execute: () => Effect.succeed({}) }), + raw: Tool.make({ description: "Raw", jsonSchema: raw, execute: () => Effect.succeed({}) }), + }) + expect(definitions[0]?.inputSchema).toEqual({ + type: "object", + properties: {}, + additionalProperties: false, + description: "No arguments", + }) + expect(definitions[0]?.outputSchema).toEqual({ + $ref: "#/$defs/Ping", + $defs: { Ping: { not: { type: "null" }, description: "No arguments" } }, + }) + expect(definitions[1]?.inputSchema).toEqual(raw) + }) + + test("normalizes inputs whose encoded side is an empty struct", () => { + const definitions = Tool.toDefinitions({ + defaulted: Tool.make({ description: "Defaulted", parameters: defaulted, success: defaulted }), + named: Tool.make({ + description: "Named", + parameters: defaulted.annotate({ identifier: "Defaulted" }), + success: Schema.String, + }), + encodedMetadata: Tool.make({ + description: "Encoded metadata", + parameters: Schema.Struct({}) + .annotate({ identifier: "Ping", title: "Ping", description: "No arguments" }) + .pipe(Schema.decodeTo(Token, fromEmpty)), + success: Schema.String, + }), + decodedCheck: Tool.make({ + description: "Decoded check", + parameters: defaulted.check(Schema.makeFilter(() => true, { toJsonSchema: () => ({ minProperties: 1 }) })), + success: Schema.String, + }), + reversed: Tool.make({ description: "Reversed", parameters: reversed, success: Schema.String }), + encodedCheck: Tool.make({ + description: "Encoded check", + parameters: Schema.Struct({}).check(Schema.isMinProperties(1)), + success: Schema.String, + }), + // A filter without a JSON Schema form emits nothing, so the input still describes an empty object. + encodedFilter: Tool.make({ + description: "Encoded filter", + parameters: Schema.Struct({}).check(Schema.makeFilter(() => true)), + success: Schema.String, + }), + }) + expect(definitions[0]?.inputSchema).toEqual(empty) + expect(definitions[0]?.outputSchema).toEqual({ not: { type: "null" } }) + expect(definitions[1]?.inputSchema).toEqual(empty) + expect(definitions[2]?.inputSchema).toEqual({ ...empty, title: "Ping", description: "No arguments" }) + expect(definitions[3]?.inputSchema).toEqual(empty) + expect(definitions[4]?.inputSchema).toEqual(token) + expect(definitions[5]?.inputSchema).toEqual({ not: { type: "null" }, minProperties: 1 }) + expect(definitions[6]?.inputSchema).toEqual(empty) + }) + + const empty = { type: "object", properties: {}, additionalProperties: false } + const nonempty = { + type: "object", + properties: { query: { type: "string" } }, + required: ["query"], + additionalProperties: false, + } + const raw = { type: "object", properties: { value: { type: "number" } }, additionalProperties: true } + const names = ["ping", "lookup", "raw", "reversed", "checked"] + const schemas = [empty, nonempty, raw, token, { type: "object", minProperties: 1 }] + + for (const scenario of [ + { + route: OpenAIChat.route, + tools: schemas.map((parameters, index) => ({ + type: "function", + function: { name: names[index], parameters }, + })), + }, + { + route: OpenAIResponses.route, + tools: schemas.map((parameters, index) => ({ + type: "function", + name: names[index], + parameters, + })), + }, + { + route: AnthropicMessages.route, + tools: schemas.map((input_schema, index) => ({ + name: names[index], + input_schema, + })), + }, + { + route: Gemini.route, + tools: [ + { + functionDeclarations: schemas.map((parametersJsonSchema, index) => ({ + name: names[index], + parametersJsonSchema, + })), + }, + ], + }, + ]) { + it.effect(`${scenario.route.id} prepares empty Effect Struct tools alongside nonempty and raw schemas`, () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLM.request({ + model: scenario.route.with({ auth: Auth.bearer("test") }).model({ id: "test-model" }), + prompt: "Use a tool.", + tools: Tool.toDefinitions({ + ping: Tool.make({ + description: "Ping", + parameters: Schema.Struct({}), + success: Schema.String, + execute: () => Effect.succeed("pong"), + }), + lookup: Tool.make({ + description: "Lookup", + parameters: Schema.Struct({ query: Schema.String }), + success: Schema.String, + execute: (input) => Effect.succeed(input.query), + }), + raw: Tool.make({ description: "Raw", jsonSchema: raw, execute: () => Effect.succeed("raw") }), + reversed: Tool.make({ + description: "Reversed", + parameters: reversed, + success: Schema.String, + execute: () => Effect.succeed("reversed"), + }), + checked: Tool.make({ + description: "Checked", + parameters: Schema.Struct({}).check(Schema.isMinProperties(1)), + success: Schema.String, + execute: () => Effect.succeed("checked"), + }), + }), + }), + ) + expect(prepared.body.tools).toMatchObject(scenario.tools) + expect(JSON.stringify(prepared.body.tools)).not.toContain('"not"') + }), + ) + } + test("moonshot strips $ref siblings and converts tuple arrays to a schema object", () => { expect( ToolSchemaProjection.moonshot({ @@ -91,7 +278,12 @@ describe("tool schema projections", () => { const exclusive = { oneOf: [{ type: "object" }, { type: "object", required: ["a"] }] } const object = { type: "object", properties: {} } - expect(yield* parameters(parameterless)).toEqual({ type: "object", description: "No input." }) + expect(yield* parameters(parameterless)).toEqual({ + type: "object", + properties: {}, + additionalProperties: false, + description: "No input.", + }) expect(yield* parameters({})).toEqual({ type: "object" }) expect(yield* parameters({ description: "Query", properties: { q: { type: "string" } } })).toEqual({ type: "object", diff --git a/packages/ai/test/transcription.test.ts b/packages/ai/test/transcription.test.ts index 8a980afc92a2..b7dab85d2a2b 100644 --- a/packages/ai/test/transcription.test.ts +++ b/packages/ai/test/transcription.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { Effect, Fiber, Layer, Stream } from "effect" import * as TestClock from "effect/testing/TestClock" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { Media, Transcription, TranscriptionClient, type TranscriptionEvent } from "../src/index.js" import { AssemblyAI, Deepgram, ElevenLabs, Google, OpenAI } from "../src/providers.js" import { it } from "./lib/effect.js" diff --git a/packages/app/e2e/utils/mock-api.ts b/packages/app/e2e/utils/mock-api.ts index c7b03792b104..31f67677554a 100644 --- a/packages/app/e2e/utils/mock-api.ts +++ b/packages/app/e2e/utils/mock-api.ts @@ -1,5 +1,5 @@ import { Predicate, Schema, SchemaGetter } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi" +import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api" import { Pty } from "@opencode/schema/pty" import { Worktree } from "@opencode/schema/worktree" diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index ab482c04ffc3..41300d788265 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -3,8 +3,8 @@ import type { OpenCodeEvent, SessionMessageInfo } from "@opencode/client/promise import { Permission } from "@opencode/schema/permission" import { Worktree } from "@opencode/schema/worktree" import { Duration, Effect, Layer, Option, Predicate, Schema } from "effect" -import { HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http" -import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" +import { HttpRouter, HttpServer, HttpServerResponse } from "effect/http" +import { HttpApiBuilder, HttpApiSchema } from "effect/http-api" import { SERVER } from "./app" import { MockApi, diff --git a/packages/app/src/providers/models/selection.tsx b/packages/app/src/providers/models/selection.tsx index dbc1c342cbc7..4b82b05c44a7 100644 --- a/packages/app/src/providers/models/selection.tsx +++ b/packages/app/src/providers/models/selection.tsx @@ -48,7 +48,7 @@ const Current = Persistence.struct({ session: SessionsSchema }) export const ModelSelectionSchema = Persistence.migrate( Current, - Schema.Struct({ + Persistence.legacy({ session: Persistence.optional(Schema.Record(Schema.String, Schema.Unknown)), pick: Persistence.optional(Schema.Record(Schema.String, Schema.Unknown)), }).pipe( diff --git a/packages/app/src/runtime/persistence/README.md b/packages/app/src/runtime/persistence/README.md index 63df55436d96..7660dcf43b0d 100644 --- a/packages/app/src/runtime/persistence/README.md +++ b/packages/app/src/runtime/persistence/README.md @@ -89,10 +89,12 @@ distinctions such as an absent current field identifying an older format. It returns a candidate in the current encoded shape; the current schema then owns recovery and validation. -The migration reader preserves excess properties so a migration can describe -only the fields it observes without dropping unrelated saved preferences. The -current schema strips fields outside its contract. Writes use only the current -schema's encoder, never the legacy reader's encoder. +Plain structs strip fields they do not list, so build every struct level of a +migration reader, including structs nested in unions or nullable values, with +`Persistence.legacy(fields)`. It keeps the fields the reader does not name, so a +migration can describe only the fields it observes without dropping unrelated +saved preferences. The current schema strips fields outside its contract. Writes +use only the current schema's encoder, never the legacy reader's encoder. `Persistence.withInitial(schemaOrMigration, initial)` exposes the same initialized codec for focused tests. Test canonical encoding and decode/encode/decode stability diff --git a/packages/app/src/runtime/persistence/schema.test.ts b/packages/app/src/runtime/persistence/schema.test.ts index f492bd763e96..7ea76ce35279 100644 --- a/packages/app/src/runtime/persistence/schema.test.ts +++ b/packages/app/src/runtime/persistence/schema.test.ts @@ -34,7 +34,7 @@ describe("persistence schemas", () => { test("legacy migration observes missing fields before initial defaults are applied", () => { const current = Persistence.struct({ mode: Schema.Literals(["compact", "full"]), enabled: Schema.Boolean }) - const stored = Schema.Struct({ + const stored = Persistence.legacy({ mode: Schema.optional(Schema.Unknown), expanded: Schema.optional(Schema.Boolean), }).pipe( @@ -56,6 +56,43 @@ describe("persistence schemas", () => { expect(Schema.encodeSync(schema)(decode({ expanded: true }))).toEqual({ mode: "full", enabled: true }) }) + test("legacy read shapes carry unknown nested fields through to the current schema", () => { + const current = Persistence.struct({ + general: Persistence.struct({ + mode: Schema.Literals(["compact", "full"]), + theme: Schema.String, + nested: Persistence.struct({ keep: Schema.Boolean }), + }), + other: Persistence.struct({ flag: Schema.Boolean }), + }) + + const stored = Persistence.legacy({ + general: Persistence.optional(Persistence.legacy({ expanded: Persistence.optional(Schema.Boolean) })), + }).pipe( + Schema.decode({ + decode: SchemaGetter.transform((value) => { + const general = value.general + + if (!general || general.expanded === undefined) return value + + return { ...value, general: { ...general, mode: general.expanded ? "full" : "compact" } } + }), + encode: SchemaGetter.passthrough(), + }), + ) + + const schema = Persistence.withInitial(Persistence.migrate(current, stored), { + general: { mode: "compact", theme: "light", nested: { keep: false } }, + other: { flag: false }, + }) + + const decode = Schema.decodeUnknownSync(schema) + expect(decode({ general: { expanded: true, theme: "dark", nested: { keep: true } }, other: { flag: true } })).toEqual({ + general: { mode: "full", theme: "dark", nested: { keep: true } }, + other: { flag: true }, + }) + }) + test("initial merging preserves field codecs and replaces arrays rather than merging indexes", () => { const current = Persistence.struct({ amount: Schema.NumberFromString.check(Schema.isFinite()), diff --git a/packages/app/src/runtime/persistence/schema.ts b/packages/app/src/runtime/persistence/schema.ts index 0818b8609b2c..6bdc9dd56e69 100644 --- a/packages/app/src/runtime/persistence/schema.ts +++ b/packages/app/src/runtime/persistence/schema.ts @@ -23,16 +23,12 @@ export function withInitial>( initial: NoInfer, ) { const schema = isMigrated(definition) ? definition.current : definition - - const read = isMigrated(definition) - ? SchemaParser.decodeUnknownResult(definition.read, { onExcessProperty: "preserve" }) - : Result.succeed - + const read = isMigrated(definition) ? SchemaParser.decodeUnknownResult(definition.read) : Result.succeed const encode = Schema.encodeUnknownSync(schema) return Schema.Unknown.pipe( Schema.decode({ - decode: SchemaGetter.transformOrFail((value) => + decode: SchemaGetter.transformEffect((value) => Effect.fromResult(Result.map(read(value), (stored) => merge(initial, recover(schema.ast, stored, initial)))), ), encode: SchemaGetter.transform((value) => encode(value)), @@ -41,6 +37,12 @@ export function withInitial>( ) } +// A legacy read shape declares only the fields it migrates. Decoding strips unknown keys, so every +// struct level in a `migrate` read schema must stay open for the current fields it does not name. +export function legacy(fields: Fields) { + return Schema.StructWithRest(Schema.Struct(fields), [Schema.Record(Schema.String, Schema.Unknown)]) +} + // Object-level codecs own their recovery. Plain structs can recover fields independently. // SAFETY: stored values and their defaults are untyped trees walked against `ast`; `withInitial` decodes the // recovered and merged result with the current schema before anyone reads it. diff --git a/packages/app/src/runtime/server/persistence.ts b/packages/app/src/runtime/server/persistence.ts index caffeebdd96d..fae7307ffafc 100644 --- a/packages/app/src/runtime/server/persistence.ts +++ b/packages/app/src/runtime/server/persistence.ts @@ -57,7 +57,7 @@ const State = Persistence.struct({ export function serverState(canonicalLocalServer: () => string | undefined = () => undefined) { return Persistence.migrate( State, - Schema.Struct({ projects: Projects, lastProject: LastProject }).pipe( + Persistence.legacy({ projects: Projects, lastProject: LastProject }).pipe( Schema.decode({ decode: SchemaGetter.transform((value) => { const canonical = canonicalLocalServer() diff --git a/packages/app/src/settings/model.tsx b/packages/app/src/settings/model.tsx index 2f9c15cf8a27..93be1fa0b2de 100644 --- a/packages/app/src/settings/model.tsx +++ b/packages/app/src/settings/model.tsx @@ -169,7 +169,7 @@ export const settingsSchema = Persistence.struct({ function storedTimelineCategory(category: TimelineCategory) { return Persistence.optional( Schema.Union([ - Schema.Struct({ + Persistence.legacy({ placement: Persistence.optional(placementSchema), details: Persistence.optional(detailsSchema), }), @@ -213,13 +213,13 @@ function legacyTimelineActivity(value: boolean | "hidden" | "compact" | "full" | export const settingsPersistence = Persistence.migrate( settingsSchema, - Schema.Struct({ + Persistence.legacy({ general: Persistence.optional( - Schema.Struct({ + Persistence.legacy({ // Keep invalid explicit values distinct from absent values so legacy preferences cannot replace them. timelineDetail: Schema.optional( Schema.NullOr( - Schema.Struct({ + Persistence.legacy({ shell: storedTimelineCategory("shell"), edit: storedTimelineCategory("edit"), thinking: storedTimelineCategory("thinking"), diff --git a/packages/app/src/shell/state/layout.tsx b/packages/app/src/shell/state/layout.tsx index e696fc1a751c..77686c6cec12 100644 --- a/packages/app/src/shell/state/layout.tsx +++ b/packages/app/src/shell/state/layout.tsx @@ -131,16 +131,16 @@ export const layoutSchema = Persistence.struct({ export const layoutPersistence = Persistence.migrate( layoutSchema, - Schema.Struct({ + Persistence.legacy({ sidebar: Persistence.optional( - Schema.Struct({ + Persistence.legacy({ workspaces: Persistence.optional(Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Schema.Boolean)])), workspacesDefault: Persistence.optional(Schema.Boolean), }), ), - review: Persistence.optional(Schema.Struct({ panelOpened: Persistence.optional(Schema.Boolean) })), + review: Persistence.optional(Persistence.legacy({ panelOpened: Persistence.optional(Schema.Boolean) })), fileTree: Persistence.optional( - Schema.Struct({ + Persistence.legacy({ opened: Persistence.optional(Schema.Boolean), width: Persistence.optional(Schema.Finite), tab: Persistence.optional(Schema.Literals(["changes", "all"])), diff --git a/packages/app/test-browser/schema-persistence.test.ts b/packages/app/test-browser/schema-persistence.test.ts index cce8a0cf13aa..7cba7d888716 100644 --- a/packages/app/test-browser/schema-persistence.test.ts +++ b/packages/app/test-browser/schema-persistence.test.ts @@ -16,7 +16,7 @@ const initial = { enabled: true, label: "default" } const Stored = Persistence.migrate( Current, - Schema.Struct({ oldLabel: Schema.optional(Schema.String), label: Schema.optional(Schema.String) }).pipe( + Persistence.legacy({ oldLabel: Schema.optional(Schema.String), label: Schema.optional(Schema.String) }).pipe( Schema.decode({ decode: SchemaGetter.transform((value) => value.oldLabel === undefined ? value : { ...value, label: value.oldLabel }, @@ -87,6 +87,18 @@ describe("schema-backed persistence", () => { }) }) + test("keeps valid current fields that the migration reader does not name", () => { + const target = Persist.global("schema-unnamed-field") + const key = `${target.storage}:${target.key}` + localStorage.setItem(key, JSON.stringify({ enabled: false, label: "kept" })) + createRoot((dispose) => { + const [state] = persisted(target, Stored, initial, web) + expect(state).toEqual({ enabled: false, label: "kept" }) + expect(JSON.parse(localStorage.getItem(key)!)).toEqual({ enabled: false, label: "kept" }) + dispose() + }) + }) + test("recovers invalid fields and strips fields outside the schema", () => { const target = Persist.global("schema-invalid-field") localStorage.setItem( diff --git a/packages/cli/package.json b/packages/cli/package.json index 52589552cce4..91677e95ccc2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,7 +37,7 @@ "@opencode/util": "workspace:*", "@opentui/core": "catalog:", "@opentui/solid": "catalog:", - "@opentunnel/client": "0.2.0", + "@opentunnel/client": "0.4.0", "@parcel/watcher": "2.5.1", "@silvia-odwyer/photon-node": "0.3.4", "diff": "catalog:", diff --git a/packages/cli/script/schema.ts b/packages/cli/script/schema.ts index 999ec0c4c1a2..3086aec462aa 100644 --- a/packages/cli/script/schema.ts +++ b/packages/cli/script/schema.ts @@ -5,7 +5,7 @@ import { Info, SchemaURL } from "../src/config/schema" const target = process.argv[2] if (!target) throw new Error("A schema output path is required") -const document = Schema.toJsonSchemaDocument(Info) +const document = Schema.toJsonSchemaDocument(Info, { onExcessProperty: "error" }) const content = await format( JSON.stringify({ $schema: "https://json-schema.org/draft/2020-12/schema", diff --git a/packages/cli/src/acp/client.ts b/packages/cli/src/acp/client.ts index 3639f2341f41..21ec0ce8abd9 100644 --- a/packages/cli/src/acp/client.ts +++ b/packages/cli/src/acp/client.ts @@ -2,7 +2,7 @@ import { ClientError } from "@opencode/client/effect" import { InvalidCursorError, InvalidRequestError, SessionNotFoundError } from "@opencode/protocol/errors" import { Session } from "@opencode/schema/session" import { Effect, Schema } from "effect" -import { HttpClientError } from "effect/unstable/http" +import { HttpClientError } from "effect/http" import { ACPError } from "./error" export function classify(error: unknown): Effect.Effect { diff --git a/packages/cli/src/commands/commands.ts b/packages/cli/src/commands/commands.ts index a1f0a7da2c4e..060b6160057a 100644 --- a/packages/cli/src/commands/commands.ts +++ b/packages/cli/src/commands/commands.ts @@ -1,10 +1,10 @@ -import { Argument, Flag, GlobalFlag } from "effect/unstable/cli" +import { Argument, Flag, GlobalFlag } from "effect/cli" import { Schema } from "effect" import { Spec } from "../framework/spec" import { Updater } from "../services/updater" -export const PrintLogs = GlobalFlag.setting("print-logs")({ - flag: Flag.boolean("print-logs").pipe( +export const PrintLogs = GlobalFlag.Setting("print-logs")({ + flag: Flag.Boolean("print-logs").pipe( Flag.withDescription("Print logs to stderr (server logs require --standalone)"), Flag.withDefault(false), ), @@ -13,23 +13,23 @@ export const PrintLogs = GlobalFlag.setting("print-logs")({ declare const OPENCODE_CLI_NAME: string | undefined const ServerParams = { - standalone: Flag.boolean("standalone").pipe( + standalone: Flag.Boolean("standalone").pipe( Flag.withDescription("Run with a private server instead of the background service"), Flag.withDefault(false), ), - server: Flag.string("server").pipe( + server: Flag.String("server").pipe( Flag.withDescription("Connect to a server URL instead of the background service"), Flag.optional, ), } const PermissionParams = { - auto: Flag.boolean("auto").pipe( + auto: Flag.Boolean("auto").pipe( Flag.withDescription("Auto-approve permissions that are not explicitly denied"), Flag.withDefault(false), ), - yolo: Flag.boolean("yolo").pipe(Flag.withDefault(false), Flag.withHidden), - dangerouslySkipPermissions: Flag.boolean("dangerously-skip-permissions").pipe( + yolo: Flag.Boolean("yolo").pipe(Flag.withDefault(false), Flag.withHidden), + dangerouslySkipPermissions: Flag.Boolean("dangerously-skip-permissions").pipe( Flag.withDefault(false), Flag.withHidden, ), @@ -40,32 +40,32 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME params: { ...ServerParams, ...PermissionParams, - directory: Argument.string("directory").pipe( + directory: Argument.String("directory").pipe( Argument.withDescription("Directory to start OpenCode in"), Argument.optional, ), - continue: Flag.boolean("continue").pipe( + continue: Flag.Boolean("continue").pipe( Flag.withAlias("c"), Flag.withDescription("Continue the last session"), Flag.withDefault(false), ), - session: Flag.string("session").pipe( + session: Flag.String("session").pipe( Flag.withAlias("s"), Flag.withDescription("Session ID to continue, or to create if it does not exist"), Flag.optional, ), - prompt: Flag.string("prompt").pipe(Flag.withDescription("Prompt to use"), Flag.optional), + prompt: Flag.String("prompt").pipe(Flag.withDescription("Prompt to use"), Flag.optional), }, commands: [ Spec.make("upgrade", { description: "Upgrade OpenCode to the latest or a specific version", aliases: ["update"], params: { - target: Argument.string("target").pipe( + target: Argument.String("target").pipe( Argument.withDescription("Version to upgrade to (with or without a leading v)"), Argument.optional, ), - method: Flag.choice("method", Updater.methods).pipe( + method: Flag.Literals("method", Updater.methods).pipe( Flag.withAlias("m"), Flag.withDescription("Installation method to use"), Flag.optional, @@ -75,11 +75,11 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME Spec.make("uninstall", { description: "Uninstall OpenCode, keeping session data, configuration, and state", params: { - dryRun: Flag.boolean("dry-run").pipe( + dryRun: Flag.Boolean("dry-run").pipe( Flag.withDescription("Show what would be removed without removing"), Flag.withDefault(false), ), - force: Flag.boolean("force").pipe( + force: Flag.Boolean("force").pipe( Flag.withAlias("f"), Flag.withDescription("Skip confirmation prompts"), Flag.withDefault(false), @@ -89,7 +89,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME Spec.make("acp", { description: "Start an Agent Client Protocol server", params: { - login: Flag.boolean("login").pipe( + login: Flag.Boolean("login").pipe( Flag.withDescription("Run auth login instead of starting the server"), Flag.withDefault(false), ), @@ -99,17 +99,17 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME description: "Make a request to the running server", params: { ...ServerParams, - request: Argument.string("operation | method path").pipe( + request: Argument.String("operation | method path").pipe( Argument.withDescription("OpenAPI operation ID, or an HTTP method followed by a path"), Argument.variadic({ min: 1, max: 2 }), ), - data: Flag.string("data").pipe(Flag.withAlias("d"), Flag.withDescription("Request body"), Flag.optional), - header: Flag.string("header").pipe( + data: Flag.String("data").pipe(Flag.withAlias("d"), Flag.withDescription("Request body"), Flag.optional), + header: Flag.String("header").pipe( Flag.withAlias("H"), Flag.withDescription("Request header in name:value form"), Flag.atMost(100), ), - param: Flag.keyValuePair("param").pipe(Flag.withDescription("OpenAPI path or query parameter"), Flag.optional), + param: Flag.KeyValuePair("param").pipe(Flag.withDescription("OpenAPI path or query parameter"), Flag.optional), }, }), Spec.make("debug", { @@ -120,7 +120,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME Spec.make("paths", { description: "Show global paths (data, config, cache, state)", params: { - name: Argument.choice("name", [ + name: Argument.Literals("name", [ "db", "home", "data", @@ -132,7 +132,9 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME "log", "repos", ]).pipe( - Argument.withDescription("Print only one path: db, home, data, config, cache, state, tmp, bin, log, repos"), + Argument.withDescription( + "Print only one path: db, home, data, config, cache, state, tmp, bin, log, repos", + ), Argument.optional, ), }, @@ -146,7 +148,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME description: "list integrations and credentials", params: { ...ServerParams, - format: Flag.choice("format", ["default", "json"]).pipe( + format: Flag.Literals("format", ["default", "json"]).pipe( Flag.withDescription("Output format"), Flag.withDefault("default"), ), @@ -156,12 +158,12 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME description: "connect an integration", params: { ...ServerParams, - target: Argument.string("target").pipe( + target: Argument.String("target").pipe( Argument.withDescription("Integration ID, name, or well-known provider URL"), Argument.optional, ), - method: Flag.string("method").pipe(Flag.withDescription("Authentication method ID"), Flag.optional), - answer: Flag.string("answer").pipe( + method: Flag.String("method").pipe(Flag.withDescription("Authentication method ID"), Flag.optional), + answer: Flag.String("answer").pipe( Flag.withDescription("Provider form answer (key=value; repeat for multiple fields)"), Flag.atMost(100), ), @@ -171,11 +173,11 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME description: "log out of a saved account", params: { ...ServerParams, - target: Argument.string("target").pipe( + target: Argument.String("target").pipe( Argument.withDescription("Integration ID or name"), Argument.optional, ), - credential: Argument.string("credential").pipe( + credential: Argument.String("credential").pipe( Argument.withDescription("Credential ID or label (opens an account picker when omitted)"), Argument.optional, ), @@ -185,7 +187,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME description: "print stored credentials, including secrets, as JSON", params: { ...ServerParams, - target: Argument.string("target").pipe( + target: Argument.String("target").pipe( Argument.withDescription("Integration ID or name (exports every integration when omitted)"), Argument.optional, ), @@ -195,7 +197,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME description: "import credentials exported by auth export", params: { ...ServerParams, - file: Argument.string("file").pipe( + file: Argument.String("file").pipe( Argument.withDescription("JSON file to import (reads stdin when omitted)"), Argument.optional, ), @@ -205,11 +207,11 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME description: "switch the active account for an integration", params: { ...ServerParams, - target: Argument.string("target").pipe( + target: Argument.String("target").pipe( Argument.withDescription("Integration ID or name"), Argument.optional, ), - credential: Argument.string("credential").pipe( + credential: Argument.String("credential").pipe( Argument.withDescription("Credential ID or label (opens an account picker when omitted)"), Argument.optional, ), @@ -224,21 +226,21 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME Spec.make("add", { description: "Add an MCP server to your configuration", params: { - name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server")), - command: Argument.string("command").pipe( + name: Argument.String("name").pipe(Argument.withDescription("Name of the MCP server")), + command: Argument.String("command").pipe( Argument.withDescription("Command and arguments for a local server, passed after --"), Argument.variadic({ min: 0 }), ), - url: Flag.string("url").pipe(Flag.withDescription("URL for a remote MCP server"), Flag.optional), - header: Flag.keyValuePair("header").pipe( + url: Flag.String("url").pipe(Flag.withDescription("URL for a remote MCP server"), Flag.optional), + header: Flag.KeyValuePair("header").pipe( Flag.withDescription("HTTP header for a remote server, as name=value"), Flag.optional, ), - env: Flag.keyValuePair("env").pipe( + env: Flag.KeyValuePair("env").pipe( Flag.withDescription("Environment variable for a local server, as name=value"), Flag.optional, ), - global: Flag.boolean("global").pipe( + global: Flag.Boolean("global").pipe( Flag.withDescription("Write to the global config instead of the project config"), Flag.withDefault(false), ), @@ -247,12 +249,12 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME Spec.make("auth", { description: "Authenticate with an OAuth-capable remote MCP server", params: { - name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server"), Argument.optional), + name: Argument.String("name").pipe(Argument.withDescription("Name of the MCP server"), Argument.optional), }, }), Spec.make("logout", { description: "Remove stored OAuth credentials for an MCP server", - params: { name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server")) }, + params: { name: Argument.String("name").pipe(Argument.withDescription("Name of the MCP server")) }, }), ], }), @@ -262,7 +264,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME Spec.make("list", { description: "List plugins", params: { - builtin: Flag.boolean("builtin").pipe( + builtin: Flag.Boolean("builtin").pipe( Flag.withDescription("Include built-in server plugins"), Flag.withDefault(false), ), @@ -271,13 +273,13 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME Spec.make("add", { description: "Install a plugin and add it to the global configuration", params: { - package: Argument.string("package").pipe(Argument.withDescription("npm registry or Git package specifier")), + package: Argument.String("package").pipe(Argument.withDescription("npm registry or Git package specifier")), }, }), Spec.make("check", { description: "Check package plugins for updates", params: { - target: Argument.string("target").pipe( + target: Argument.String("target").pipe( Argument.withDescription("Configured package target"), Argument.optional, ), @@ -286,7 +288,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME Spec.make("update", { description: "Update package plugins", params: { - target: Argument.string("target").pipe( + target: Argument.String("target").pipe( Argument.withDescription("Configured package target; omit to update all outdated plugins"), Argument.optional, ), @@ -295,7 +297,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME Spec.make("remove", { description: "Remove a plugin from global configuration", params: { - package: Argument.string("package").pipe(Argument.withDescription("configured package specifier")), + package: Argument.String("package").pipe(Argument.withDescription("configured package specifier")), }, }), ], @@ -308,108 +310,108 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME description: "Show shareable usage statistics", params: { ...ServerParams, - days: Flag.integer("days").pipe( + days: Flag.Int("days").pipe( Flag.withSchema(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))), Flag.withDescription("Show the last N days; 0 means today"), Flag.optional, ), - year: Flag.integer("year").pipe( + year: Flag.Int("year").pipe( Flag.withSchema(Schema.Int.check(Schema.isBetween({ minimum: 1970, maximum: 9_999 }))), Flag.withDescription("Show a calendar year"), Flag.optional, ), - all: Flag.boolean("all").pipe(Flag.withDescription("Show lifetime statistics"), Flag.withDefault(false)), - project: Flag.string("project").pipe( + all: Flag.Boolean("all").pipe(Flag.withDescription("Show lifetime statistics"), Flag.withDefault(false)), + project: Flag.String("project").pipe( Flag.withDescription('Filter by project ID, or use "." for the current project'), Flag.optional, ), - models: Flag.boolean("models").pipe(Flag.withDescription("Show model usage"), Flag.withDefault(false)), - tools: Flag.boolean("tools").pipe(Flag.withDescription("Show tool reliability"), Flag.withDefault(false)), - cost: Flag.boolean("cost").pipe(Flag.withDescription("Show cost and token details"), Flag.withDefault(false)), - full: Flag.boolean("full").pipe(Flag.withDescription("Show every detailed section"), Flag.withDefault(false)), - limit: Flag.integer("limit").pipe( + models: Flag.Boolean("models").pipe(Flag.withDescription("Show model usage"), Flag.withDefault(false)), + tools: Flag.Boolean("tools").pipe(Flag.withDescription("Show tool reliability"), Flag.withDefault(false)), + cost: Flag.Boolean("cost").pipe(Flag.withDescription("Show cost and token details"), Flag.withDefault(false)), + full: Flag.Boolean("full").pipe(Flag.withDescription("Show every detailed section"), Flag.withDefault(false)), + limit: Flag.Int("limit").pipe( Flag.withSchema(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))), Flag.withDescription("Number of rows in detailed sections"), Flag.withDefault(5), ), - json: Flag.boolean("json").pipe(Flag.withDescription("Output statistics as JSON"), Flag.withDefault(false)), + json: Flag.Boolean("json").pipe(Flag.withDescription("Output statistics as JSON"), Flag.withDefault(false)), }, }), Spec.make("mini", { description: "Start the minimal interactive interface", params: { ...ServerParams, - continue: Flag.boolean("continue").pipe( + continue: Flag.Boolean("continue").pipe( Flag.withAlias("c"), Flag.withDescription("Continue the last session"), Flag.withDefault(false), ), - session: Flag.string("session").pipe( + session: Flag.String("session").pipe( Flag.withAlias("s"), Flag.withDescription("Session ID to continue, or to create if it does not exist"), Flag.optional, ), - fork: Flag.boolean("fork").pipe( + fork: Flag.Boolean("fork").pipe( Flag.withDescription("Fork the session when continuing"), Flag.withDefault(false), ), - replay: Flag.boolean("replay").pipe( + replay: Flag.Boolean("replay").pipe( Flag.withDescription("Restore session history on resume and resize (disable with --no-replay)"), Flag.optional, ), - replayLimit: Flag.integer("replay-limit").pipe( + replayLimit: Flag.Int("replay-limit").pipe( Flag.withDescription("Limit replay to the newest N messages (default: 200)"), Flag.optional, ), - model: Flag.string("model").pipe( + model: Flag.String("model").pipe( Flag.withAlias("m"), Flag.withDescription("Model to use in the format provider/model"), Flag.optional, ), - agent: Flag.string("agent").pipe(Flag.withDescription("Agent to use"), Flag.optional), - prompt: Flag.string("prompt").pipe(Flag.withDescription("Prompt to use"), Flag.optional), - demo: Flag.boolean("demo").pipe(Flag.withDefault(false), Flag.withHidden), + agent: Flag.String("agent").pipe(Flag.withDescription("Agent to use"), Flag.optional), + prompt: Flag.String("prompt").pipe(Flag.withDescription("Prompt to use"), Flag.optional), + demo: Flag.Boolean("demo").pipe(Flag.withDefault(false), Flag.withHidden), }, }), Spec.make("run", { description: "Run OpenCode with a message", params: { ...ServerParams, - message: Argument.string("message").pipe( + message: Argument.String("message").pipe( Argument.withDescription("Message to send"), Argument.variadic({ min: 0 }), ), - continue: Flag.boolean("continue").pipe( + continue: Flag.Boolean("continue").pipe( Flag.withAlias("c"), Flag.withDescription("Continue the last session"), Flag.withDefault(false), ), - session: Flag.string("session").pipe( + session: Flag.String("session").pipe( Flag.withAlias("s"), Flag.withDescription("Session ID to continue, or to create if it does not exist"), Flag.optional, ), - fork: Flag.boolean("fork").pipe( + fork: Flag.Boolean("fork").pipe( Flag.withDescription("Fork the session before continuing"), Flag.withDefault(false), ), - model: Flag.string("model").pipe( + model: Flag.String("model").pipe( Flag.withAlias("m"), Flag.withDescription("Model to use in the format provider/model#variant"), Flag.optional, ), - agent: Flag.string("agent").pipe(Flag.withDescription("Agent to use"), Flag.optional), - format: Flag.choice("format", ["default", "json"]).pipe( + agent: Flag.String("agent").pipe(Flag.withDescription("Agent to use"), Flag.optional), + format: Flag.Literals("format", ["default", "json"]).pipe( Flag.withDescription("Output format"), Flag.withDefault("default"), ), - file: Flag.string("file").pipe( + file: Flag.String("file").pipe( Flag.withAlias("f"), Flag.withDescription("File to attach to the message"), Flag.atMost(100), ), - title: Flag.string("title").pipe(Flag.withDescription("Session title"), Flag.optional), - thinking: Flag.boolean("thinking").pipe(Flag.withDescription("Show thinking blocks"), Flag.withDefault(false)), + title: Flag.String("title").pipe(Flag.withDescription("Session title"), Flag.optional), + thinking: Flag.Boolean("thinking").pipe(Flag.withDescription("Show thinking blocks"), Flag.withDefault(false)), ...PermissionParams, }, }), @@ -420,13 +422,13 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME description: "List top-level sessions in the current project, newest first", params: { ...ServerParams, - maxCount: Flag.integer("max-count").pipe( + maxCount: Flag.Int("max-count").pipe( Flag.withAlias("n"), Flag.withSchema(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))), Flag.withDescription("Limit to N most recent sessions (default: 100)"), Flag.optional, ), - format: Flag.choice("format", ["table", "json"]).pipe( + format: Flag.Literals("format", ["table", "json"]).pipe( Flag.withDescription("Output format"), Flag.withDefault("table"), ), @@ -436,18 +438,18 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME description: "Delete a session and its child sessions", params: { ...ServerParams, - sessionID: Argument.string("sessionID").pipe(Argument.withDescription("Session ID to delete")), + sessionID: Argument.String("sessionID").pipe(Argument.withDescription("Session ID to delete")), }, }), Spec.make("export", { description: "Export session data as JSON", params: { ...ServerParams, - session: Argument.string("session").pipe( + session: Argument.String("session").pipe( Argument.withDescription("Session ID to export"), Argument.optional, ), - sanitize: Flag.boolean("sanitize").pipe( + sanitize: Flag.Boolean("sanitize").pipe( Flag.withDescription("Redact sensitive transcript and file data"), Flag.withDefault(false), ), @@ -457,8 +459,8 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME description: "Import session data from a JSON file or URL", params: { ...ServerParams, - file: Argument.string("file").pipe(Argument.withDescription("JSON file or URL to import")), - directory: Flag.string("directory").pipe( + file: Argument.String("file").pipe(Argument.withDescription("JSON file or URL to import")), + directory: Flag.String("directory").pipe( Flag.withDescription("Directory in which to import the session"), Flag.optional, ), @@ -476,8 +478,8 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME Spec.make("get", { description: "Get service configuration", params: { - key: Argument.string("key").pipe(Argument.withDescription("Service setting or env"), Argument.optional), - name: Argument.string("name").pipe( + key: Argument.String("key").pipe(Argument.withDescription("Service setting or env"), Argument.optional), + name: Argument.String("name").pipe( Argument.withDescription("Environment variable name"), Argument.optional, ), @@ -486,11 +488,11 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME Spec.make("set", { description: "Set service configuration", params: { - key: Argument.string("key").pipe(Argument.withDescription("Service setting or env")), - value: Argument.string("value").pipe( + key: Argument.String("key").pipe(Argument.withDescription("Service setting or env")), + value: Argument.String("value").pipe( Argument.withDescription("Setting value or environment variable name"), ), - nestedValue: Argument.string("env-value").pipe( + nestedValue: Argument.String("env-value").pipe( Argument.withDescription("Environment variable value"), Argument.optional, ), @@ -499,8 +501,8 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME Spec.make("unset", { description: "Unset service configuration", params: { - key: Argument.string("key").pipe(Argument.withDescription("Service setting or env")), - name: Argument.string("name").pipe( + key: Argument.String("key").pipe(Argument.withDescription("Service setting or env")), + name: Argument.String("name").pipe( Argument.withDescription("Environment variable name"), Argument.optional, ), @@ -517,7 +519,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME Spec.make("pair", { description: "Print one-time links to connect a browser or app", params: { - url: Flag.string("url").pipe( + url: Flag.String("url").pipe( Flag.withDescription("Use an external HTTP(S) server URL in pairing links"), Flag.mapTryCatch( (value) => { @@ -530,7 +532,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME ), Flag.optional, ), - remote: Flag.boolean("remote").pipe( + remote: Flag.Boolean("remote").pipe( Flag.withDescription("Pair through the OpenTunnel remote address, enabling remote access if needed"), Flag.withDefault(false), ), @@ -539,15 +541,15 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME Spec.make("serve", { description: "Start the v2 API and web server", params: { - hostname: Flag.string("hostname").pipe(Flag.optional), - port: Flag.integer("port").pipe(Flag.optional), - cors: Flag.string("cors").pipe( + hostname: Flag.String("hostname").pipe(Flag.optional), + port: Flag.Int("port").pipe(Flag.optional), + cors: Flag.String("cors").pipe( Flag.withSchema(Schema.NonEmptyString), Flag.withDescription("Additional allowed CORS origin (repeat for multiple origins)"), Flag.atLeast(0), ), - service: Flag.boolean("service").pipe(Flag.withDefault(false)), - stdio: Flag.boolean("stdio").pipe(Flag.withDefault(false)), + service: Flag.Boolean("service").pipe(Flag.withDefault(false)), + stdio: Flag.Boolean("stdio").pipe(Flag.withDefault(false)), }, }), ], diff --git a/packages/cli/src/commands/handlers/acp.ts b/packages/cli/src/commands/handlers/acp.ts index 6323a6651b6b..5dbe25850b8e 100644 --- a/packages/cli/src/commands/handlers/acp.ts +++ b/packages/cli/src/commands/handlers/acp.ts @@ -3,7 +3,7 @@ import { OpenCode } from "@opencode/client/effect" import { Service } from "@opencode/client/effect/service" import { CrossSpawnSpawner } from "@opencode/util/cross-spawn-spawner" import { Effect, Option } from "effect" -import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http" import { Writable } from "node:stream" import { ACP } from "../../acp/agent" import { Commands } from "../commands" diff --git a/packages/cli/src/commands/handlers/session/list.ts b/packages/cli/src/commands/handlers/session/list.ts index 5589070c4dad..83f71782aaed 100644 --- a/packages/cli/src/commands/handlers/session/list.ts +++ b/packages/cli/src/commands/handlers/session/list.ts @@ -61,7 +61,7 @@ const handler = Effect.fn("cli.session.list")(function* ( const { AppProcess } = yield* Effect.promise(() => import("@opencode/util/process")) const { LayerNode } = yield* Effect.promise(() => import("@opencode/util/effect/layer-node")) - const { ChildProcess } = yield* Effect.promise(() => import("effect/unstable/process")) + const { ChildProcess } = yield* Effect.promise(() => import("effect/process")) yield* Effect.gen(function* () { const processService = yield* AppProcess.Service const pager = yield* processService diff --git a/packages/cli/src/env.ts b/packages/cli/src/env.ts index 45340c1e0c29..8361bdb18520 100644 --- a/packages/cli/src/env.ts +++ b/packages/cli/src/env.ts @@ -7,8 +7,8 @@ import { Config } from "effect" // The opencode server password: sent by clients connecting to an explicit // --server, and adopted by a manually run or standalone server. The legacy // name is still honored. -export const password = Config.redacted("OPENCODE_PASSWORD").pipe( - Config.orElse(() => Config.redacted("OPENCODE_SERVER_PASSWORD")), +export const password = Config.Redacted("OPENCODE_PASSWORD").pipe( + Config.orElse(() => Config.Redacted("OPENCODE_SERVER_PASSWORD")), Config.withDefault(undefined), ) diff --git a/packages/cli/src/framework/runtime.ts b/packages/cli/src/framework/runtime.ts index 76c99ab9df5e..3df1f074965a 100644 --- a/packages/cli/src/framework/runtime.ts +++ b/packages/cli/src/framework/runtime.ts @@ -1,5 +1,5 @@ import { Effect, FileSystem, Scope } from "effect" -import { Command } from "effect/unstable/cli" +import { Command } from "effect/cli" import { PrintLogs } from "../commands/commands" import { Spec } from "./spec" import { Global } from "@opencode/util/global" diff --git a/packages/cli/src/framework/spec.ts b/packages/cli/src/framework/spec.ts index 587993299740..b57e46b849f5 100644 --- a/packages/cli/src/framework/spec.ts +++ b/packages/cli/src/framework/spec.ts @@ -1,4 +1,4 @@ -import { Command } from "effect/unstable/cli" +import { Command } from "effect/cli" type Options> = { readonly description?: string diff --git a/packages/cli/src/server-process.ts b/packages/cli/src/server-process.ts index 28ae68224a43..76f1ef4ccaa8 100644 --- a/packages/cli/src/server-process.ts +++ b/packages/cli/src/server-process.ts @@ -9,7 +9,8 @@ import { AppProcess } from "@opencode/util/process" import { randomBytes, randomUUID } from "node:crypto" import { Effect, Option, Redacted, Schema } from "effect" import { PersistentPty } from "@opencode/schema/persistent-pty" -import { HttpServer } from "effect/unstable/http" +import { HttpServer } from "effect/http" +import { NetAddress } from "effect/net" import { Env } from "./env" import { ServiceConfig } from "./services/service-config" import { RetainedImage } from "./services/retained-image" @@ -173,14 +174,17 @@ const processEffect = Effect.fnUntraced(function* (options: Options) { }), ) if (server === undefined) return - if (serviceOptions !== undefined && config.remote !== undefined && server.address._tag === "TcpAddress") { - const bound = server.address.hostname + if (serviceOptions !== undefined && config.remote !== undefined && NetAddress.isInetAddress(server.address)) { + const bound = NetAddress.formatIp(server.address.address) // A wildcard bind also listens on loopback, which is all the tunnel needs to reach. - const host = bound === "0.0.0.0" || bound === "::" ? "127.0.0.1" : bound.includes(":") ? `[${bound}]` : bound + const target = + bound === "0.0.0.0" || bound === "::" + ? `127.0.0.1:${server.address.port}` + : NetAddress.formatInet(server.address) yield* Effect.forkScoped( RemoteTunnel.run({ route: config.remote.route, - target: `${host}:${server.address.port}`, + target, onURL: (url) => { remote.urls = url === undefined ? [] : [url] }, diff --git a/packages/cli/src/services/service-registration.ts b/packages/cli/src/services/service-registration.ts index 68e3e307d458..69a00b284093 100644 --- a/packages/cli/src/services/service-registration.ts +++ b/packages/cli/src/services/service-registration.ts @@ -3,7 +3,8 @@ export * as ServiceRegistration from "./service-registration" import { Service, type Info } from "@opencode/client/effect/service" import path from "node:path" import { Effect, FileSystem, Schedule, Schema } from "effect" -import { HttpServer } from "effect/unstable/http" +import { HttpServer } from "effect/http" +import { NetAddress } from "effect/net" import { OPENCODE_VERSION } from "../version" const infoJson = Schema.fromJsonString(Service.Info) @@ -11,7 +12,7 @@ const encodeInfo = Schema.encodeEffect(infoJson) const decodeInfo = Schema.decodeUnknownEffect(infoJson) export const register = Effect.fnUntraced(function* (options: { - readonly address: HttpServer.Address + readonly address: NetAddress.SocketAddress readonly password: string readonly id: string readonly file: string diff --git a/packages/cli/src/services/standalone.ts b/packages/cli/src/services/standalone.ts index ba7d9f87965f..2e4cbc1c309e 100644 --- a/packages/cli/src/services/standalone.ts +++ b/packages/cli/src/services/standalone.ts @@ -1,7 +1,7 @@ import { CrossSpawnSpawner } from "@opencode/util/cross-spawn-spawner" import { LayerNode } from "@opencode/util/effect/layer-node" import { Deferred, Effect, Schema, Stream } from "effect" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import { ChildProcess, ChildProcessSpawner } from "effect/process" import { randomBytes } from "node:crypto" import { selfCommand } from "../util/process" diff --git a/packages/cli/src/services/updater.ts b/packages/cli/src/services/updater.ts index f74b4a64c2e2..c681f44b8a64 100644 --- a/packages/cli/src/services/updater.ts +++ b/packages/cli/src/services/updater.ts @@ -3,7 +3,7 @@ import { AppProcess } from "@opencode/util/process" import { EffectFlock } from "@opencode/util/effect-flock" import { OPENCODE_ARTIFACT, OPENCODE_CHANNEL, OPENCODE_LOCAL, OPENCODE_VERSION } from "../version" import { Context, Duration, Effect, FileSystem, Layer, Option, Ref, Schema } from "effect" -import { ChildProcess } from "effect/unstable/process" +import { ChildProcess } from "effect/process" import { parse, type ParseError } from "jsonc-parser" import path from "node:path" import { stripVTControlCharacters } from "node:util" diff --git a/packages/cli/src/services/web-ui.ts b/packages/cli/src/services/web-ui.ts index c00448a0b8ff..edda5d733dbb 100644 --- a/packages/cli/src/services/web-ui.ts +++ b/packages/cli/src/services/web-ui.ts @@ -1,6 +1,6 @@ import { FSUtil } from "@opencode/util/fs-util" import { Effect, FileSystem } from "effect" -import { HttpServerError, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" +import { HttpServerError, HttpServerRequest, HttpServerResponse } from "effect/http" import { createHash } from "node:crypto" import { load, type AssetMap, type BrotliMap } from "../app-assets" diff --git a/packages/cli/src/ssh-askpass.test.ts b/packages/cli/src/ssh-askpass.test.ts index 4ae5b7740ec2..d20e4d32aedc 100644 --- a/packages/cli/src/ssh-askpass.test.ts +++ b/packages/cli/src/ssh-askpass.test.ts @@ -34,3 +34,30 @@ test("the executable askpass branch returns only the response, without CLI outpu await new Promise((resolve) => server.close(() => resolve())) } }, 30_000) + +test("a response split inside a multi-byte character is written intact", async () => { + const response = Buffer.from(JSON.stringify({ value: "pässwörd" })) + // Split between the two bytes of "ä" so the helper reads them separately. + const split = response.indexOf(0xc3) + 1 + const server = createServer((socket) => { + socket.setNoDelay(true) + socket.once("data", () => { + socket.write(response.subarray(0, split)) + setTimeout(() => socket.end(response.subarray(split)), 50) + }) + }) + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)) + const address = server.address() + if (!address || typeof address === "string") throw new Error("missing listener") + try { + const child = Bun.spawn([process.execPath, path.join(import.meta.dir, "index.ts"), "Enter passphrase:"], { + env: { ...process.env, OPENCODE_SSH_ASKPASS_PORT: String(address.port), OPENCODE_SSH_ASKPASS_TOKEN: "fixture" }, + stdout: "pipe", + stderr: "pipe", + }) + expect(await new Response(child.stdout).text()).toBe("pässwörd\n") + expect(await child.exited).toBe(0) + } finally { + await new Promise((resolve) => server.close(() => resolve())) + } +}, 30_000) diff --git a/packages/cli/src/ssh-askpass.ts b/packages/cli/src/ssh-askpass.ts index b8db12912074..c8c26b82b8c2 100644 --- a/packages/cli/src/ssh-askpass.ts +++ b/packages/cli/src/ssh-askpass.ts @@ -1,5 +1,6 @@ import { NodeSocket } from "@effect/platform-node" import { Effect, Schema, Stdio, Stream } from "effect" +import { Socket } from "effect/socket" const Response = Schema.fromJsonString(Schema.Struct({ value: Schema.NullOr(Schema.String) })) const Port = Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(65535)) @@ -10,25 +11,21 @@ export const askpass = Effect.gen(function* () { const port = yield* Schema.decodeUnknownEffect(Port)(process.env.OPENCODE_SSH_ASKPASS_PORT) const stdio = yield* Stdio.Stdio const socket = yield* NodeSocket.makeNet({ host: "127.0.0.1", port }) - const write = yield* socket.writer - const response = { text: "" } - yield* Effect.all( - [ - socket.runString((text) => - Effect.sync(() => { - response.text += text - }), - ), - write( - JSON.stringify({ - token: process.env.OPENCODE_SSH_ASKPASS_TOKEN, - text: process.argv.slice(2).join(" "), - confirm: process.env.SSH_ASKPASS_PROMPT === "confirm", - }) + "\n", - ), - ], - { concurrency: "unbounded", discard: true }, + const pull = yield* Socket.readerBytes(socket) + const writer = yield* socket.writer + yield* writer.write( + JSON.stringify({ + token: process.env.OPENCODE_SSH_ASKPASS_TOKEN, + text: process.argv.slice(2).join(" "), + confirm: process.env.SSH_ASKPASS_PROMPT === "confirm", + }) + "\n", ) + // One streaming decoder, so a character split across reads decodes intact. + const decoder = new TextDecoder() + const response = { text: "" } + yield* Effect.gen(function* () { + while (true) for (const chunk of yield* pull) response.text += decoder.decode(chunk, { stream: true }) + }).pipe(Effect.catchReason("SocketError", "SocketCloseError", () => Effect.void)) const result = yield* Schema.decodeUnknownEffect(Response)(response.text) if (result.value === null) return 1 yield* Stream.make(result.value + "\n").pipe(Stream.run(stdio.stdout({ endOnDone: false }))) diff --git a/packages/cli/test/acp/wire-fixture.ts b/packages/cli/test/acp/wire-fixture.ts index 99c6be0a61b9..67ad107231bb 100644 --- a/packages/cli/test/acp/wire-fixture.ts +++ b/packages/cli/test/acp/wire-fixture.ts @@ -31,7 +31,7 @@ import type { TokenUsage } from "@opencode/schema/token-usage" import type { BunRequest } from "bun" import { createTwoFilesPatch } from "diff" import { Duration, Effect, Exit, Logger, Option, Schema, Scope } from "effect" -import { FetchHttpClient } from "effect/unstable/http" +import { FetchHttpClient } from "effect/http" import { ACP } from "../../src/acp/agent" import { ACPTurn } from "../../src/acp/turn" diff --git a/packages/cli/test/cors.test.ts b/packages/cli/test/cors.test.ts index d87589ef9a6e..92e5de1fb8b3 100644 --- a/packages/cli/test/cors.test.ts +++ b/packages/cli/test/cors.test.ts @@ -2,7 +2,7 @@ import { NodeServices } from "@effect/platform-node" import { Global } from "@opencode/util/global" import { expect, test } from "bun:test" import { Effect, Exit, FileSystem } from "effect" -import { Command } from "effect/unstable/cli" +import { Command } from "effect/cli" import path from "node:path" import { Commands } from "../src/commands/commands" import { ServiceConfig } from "../src/services/service-config" diff --git a/packages/cli/test/fixture/upgrade.ts b/packages/cli/test/fixture/upgrade.ts index 504862517933..149433d9465b 100644 --- a/packages/cli/test/fixture/upgrade.ts +++ b/packages/cli/test/fixture/upgrade.ts @@ -1,6 +1,6 @@ import { NodeServices } from "@effect/platform-node" import { Effect } from "effect" -import { Command } from "effect/unstable/cli" +import { Command } from "effect/cli" import { Commands } from "../../src/commands/commands" import upgrade from "../../src/commands/handlers/upgrade" import { Updater } from "../../src/services/updater" diff --git a/packages/cli/test/service.test.ts b/packages/cli/test/service.test.ts index 1facda409419..1f735ea3dee7 100644 --- a/packages/cli/test/service.test.ts +++ b/packages/cli/test/service.test.ts @@ -5,6 +5,7 @@ import { Global } from "@opencode/util/global" import { OPENCODE_VERSION } from "../src/version" import { expect, test } from "bun:test" import { Deferred, Effect, FileSystem, Schedule, Schema } from "effect" +import { NetAddress } from "effect/net" import { TestClock } from "effect/testing" import fs from "node:fs/promises" import os from "node:os" @@ -632,7 +633,7 @@ test("service registration replaces a stale owner with the bound address", async try { const cleanup = await Effect.runPromise( ServiceRegistration.register({ - address: { _tag: "TcpAddress", hostname: "127.0.0.1", port: 4321 }, + address: NetAddress.inetAddressFromIpStringUnsafe("127.0.0.1", 4321), password: "secret", id: "owner", file: registration, @@ -727,7 +728,7 @@ const registered = Effect.fnUntraced(function* () { const file = path.join(yield* fileSystem.makeTempDirectoryScoped(), "service-local.json") const stopped = yield* Deferred.make() const cleanup = yield* ServiceRegistration.register({ - address: { _tag: "TcpAddress", hostname: "127.0.0.1", port: 4321 }, + address: NetAddress.inetAddressFromIpStringUnsafe("127.0.0.1", 4321), password: "secret", id: "owner", file, diff --git a/packages/cli/test/updater-install.test.ts b/packages/cli/test/updater-install.test.ts index 55c1ce82bfa8..7707ef8693c9 100644 --- a/packages/cli/test/updater-install.test.ts +++ b/packages/cli/test/updater-install.test.ts @@ -5,7 +5,7 @@ import { Global } from "@opencode/util/global" import { AppProcess } from "@opencode/util/process" import { expect, spyOn, test } from "bun:test" import { Effect, FileSystem, Layer, PlatformError, Stream } from "effect" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import { ChildProcess, ChildProcessSpawner } from "effect/process" import { existsSync, readdirSync, readFileSync } from "node:fs" import path from "node:path" import { Updater } from "../src/services/updater" diff --git a/packages/cli/test/web-ui.test.ts b/packages/cli/test/web-ui.test.ts index 92d48ad8bc69..081516d3a899 100644 --- a/packages/cli/test/web-ui.test.ts +++ b/packages/cli/test/web-ui.test.ts @@ -2,7 +2,7 @@ import { NodeFileSystem, NodeHttpServer } from "@effect/platform-node" import { ServerProcess } from "@opencode/server/process" import { afterAll, describe, expect, test } from "bun:test" import { Effect } from "effect" -import { HttpServer, HttpServerError, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" +import { HttpServer, HttpServerError, HttpServerRequest, HttpServerResponse } from "effect/http" import { createServer } from "node:http" import { mkdtemp, rm, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" diff --git a/packages/client/package.json b/packages/client/package.json index bb36c09adbd6..511903edd205 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -37,7 +37,7 @@ "@opencode/protocol": "workspace:*" }, "peerDependencies": { - "effect": "4.0.0-rc.112", + "effect": "4.0.0-rc.118", "solid-js": ">=1.9.0" }, "peerDependenciesMeta": { diff --git a/packages/client/src/effect/client.ts b/packages/client/src/effect/client.ts index 7eba0c3d2ef3..c757acc80279 100644 --- a/packages/client/src/effect/client.ts +++ b/packages/client/src/effect/client.ts @@ -1,7 +1,7 @@ export * as OpenCode from "./client.js" import { Cause, Context, Effect, Stream } from "effect" -import { HttpClient, HttpClientRequest } from "effect/unstable/http" +import { HttpClient, HttpClientRequest } from "effect/http" import { SharedEvents } from "../shared-events.js" import { ClientError, OpenCode } from "./generated/index.js" import { RpcClientRuntime } from "./rpc.js" diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index 9d8e50d7c4c6..9ee87ea36c0d 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -1,8 +1,8 @@ // Generated by @opencode/httpapi-codegen. Do not edit. import { Effect, Stream, Schema } from "effect" -import { Sse } from "effect/unstable/encoding" -import { HttpClientError } from "effect/unstable/http" -import { HttpApiClient } from "effect/unstable/httpapi" +import { Sse } from "effect/encoding" +import { HttpClientError } from "effect/http" +import { HttpApiClient } from "effect/http-api" import { ClientApi } from "../../contract" import type { ServerInfoOutput, diff --git a/packages/client/test/effect.test.ts b/packages/client/test/effect.test.ts index 1e5e5b326ae8..5c10fe6b46a3 100644 --- a/packages/client/test/effect.test.ts +++ b/packages/client/test/effect.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test" import { Context, DateTime, Effect, Stream } from "effect" -import { HttpClient, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientResponse } from "effect/http" import { AbsolutePath, Agent, diff --git a/packages/codemode/src/openapi/index.ts b/packages/codemode/src/openapi/index.ts index bdf000718483..c44708febfba 100644 --- a/packages/codemode/src/openapi/index.ts +++ b/packages/codemode/src/openapi/index.ts @@ -1,4 +1,4 @@ -import { HttpClient } from "effect/unstable/http" +import { HttpClient } from "effect/http" import { make, type Tool } from "../tool.js" import { invoke } from "./runtime.js" import { diff --git a/packages/codemode/src/openapi/runtime.ts b/packages/codemode/src/openapi/runtime.ts index 8548a42557f8..f2a0cfc3cdf9 100644 --- a/packages/codemode/src/openapi/runtime.ts +++ b/packages/codemode/src/openapi/runtime.ts @@ -1,5 +1,5 @@ import { Effect, Option, Schema, Stream } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse, type HttpMethod } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse, type HttpMethod } from "effect/http" import { ToolError, toolError } from "../tool-error.js" import { isRecord, own } from "./spec.js" import type { AppliedAuth, Credential, Plan, SecurityScheme } from "./types.js" diff --git a/packages/codemode/src/openapi/types.ts b/packages/codemode/src/openapi/types.ts index 79fdb7fbff08..2a4df8c44f91 100644 --- a/packages/codemode/src/openapi/types.ts +++ b/packages/codemode/src/openapi/types.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { HttpClient } from "effect/unstable/http" +import { HttpClient } from "effect/http" import type { Tool, JsonSchema } from "../tool.js" /** A parsed OpenAPI 3.x document. YAML must be parsed by the ctx. */ diff --git a/packages/codemode/src/tool-schema.ts b/packages/codemode/src/tool-schema.ts index 80b3bc97ab03..900556191ca4 100644 --- a/packages/codemode/src/tool-schema.ts +++ b/packages/codemode/src/tool-schema.ts @@ -9,11 +9,27 @@ export const identifierSegment = /^[A-Za-z_$][A-Za-z0-9_$]*$/ const renderKey = (name: string): string => (identifierSegment.test(name) ? name : JSON.stringify(name)) +const definitionName = (ref: string): string | undefined => { + const tokens = JsonPointer.parseUriFragment(ref) + return tokens?.length === 2 && (tokens[0] === "$defs" || tokens[0] === "definitions") ? tokens[1] : undefined +} + +// Effect encodes an unchecked `number` as `number | "Infinity" | "-Infinity" | "NaN"`, and older generators emit one +// singleton enum per value. Rendering them as plain `number` only narrows what the model is told to send. const effectNumberSentinel = (schema: JsonSchema) => schema.type === "string" && Array.isArray(schema.enum) && - schema.enum.length === 1 && - (schema.enum[0] === "NaN" || schema.enum[0] === "Infinity" || schema.enum[0] === "-Infinity") + schema.enum.length > 0 && + schema.enum.every((value) => value === "NaN" || value === "Infinity" || value === "-Infinity") + +// Effect emits `{ not: { type: "null" } }` only for a struct with no properties or index signatures. With nothing but +// annotations beside it, it is TypeScript's `{}`. Mirrors `emptyInputJsonSchema` in `@opencode/ai`, which this +// standalone package cannot import. +const annotationKeywords = new Set(["title", "description", "default", "examples", "readOnly", "writeOnly"]) +const isEffectEmptyStruct = (schema: JsonSchema) => + schema.not?.type === "null" && + Object.keys(schema.not).length === 1 && + Object.keys(schema).every((key) => key === "not" || annotationKeywords.has(key)) const intersection = (members: ReadonlyArray): string => { const concrete = members.filter((member) => member !== "unknown") @@ -38,8 +54,7 @@ const hasUnresolvedRef = ( if (visited.has(schema)) return false const nextVisited = new Set([...visited, schema]) if (schema.$ref !== undefined) { - const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1] - const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment) + const name = definitionName(schema.$ref) if (name === undefined || definitions[name] === undefined || seen.has(name)) return true if (hasUnresolvedRef(definitions[name], definitions, new Set([...seen, name]), nextVisited)) return true } @@ -130,8 +145,7 @@ const renderSchema = ( ? ctx : { ...ctx, definitions: { ...ctx.definitions, ...(schema.definitions ?? {}), ...(schema.$defs ?? {}) } } if (schema.$ref) { - const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1] - const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment) + const name = definitionName(schema.$ref) if (!name || !nested.definitions[name] || seen.has(name)) return "unknown" return intersection([ renderSchema(nested.definitions[name], nested, depth, new Set([...seen, name])), @@ -147,6 +161,7 @@ const renderSchema = ( alternatives.every((item) => item.type === "number" || effectNumberSentinel(item)) ) return "number" + // Older Effect releases emitted this pair for an empty struct, and raw schemas from their generators still can. if ( alternatives.length === 2 && alternatives[0]?.type === "object" && @@ -197,17 +212,19 @@ const renderSchema = ( if (indexType !== undefined) lines.push(`${pad}[key: string]: ${indexType},`) return `{\n${lines.join("\n")}\n${" ".repeat(depth)}}` } + if (isEffectEmptyStruct(schema)) return "{}" return "unknown" } export const toTypeScript = (schema: Schema.Top, decoded = false, pretty = false): string => { try { - const visible = decoded ? Schema.toType(schema) : schema - const document = Schema.toJsonSchemaDocument(visible) as { + const document = Schema.toJsonSchemaDocument(decoded ? Schema.toType(schema) : schema, { + onExcessProperty: "error", + }) as { readonly schema: JsonSchema - readonly definitions?: Readonly> + readonly definitions: Readonly> } - return renderSchema(document.schema, { definitions: document.definitions ?? {}, pretty }) + return renderSchema(document.schema, { definitions: document.definitions, pretty }) } catch { return "unknown" } @@ -230,20 +247,18 @@ export type InputProperty = { export const inputProperties = (tool: Tool): Array => { try { const document = isEffectSchema(tool.input) - ? (Schema.toJsonSchemaDocument(tool.input) as { + ? (Schema.toJsonSchemaDocument(tool.input, { onExcessProperty: "error" }) as { readonly schema: JsonSchema - readonly definitions?: Readonly> + readonly definitions: Readonly> }) : { schema: tool.input, definitions: { ...(tool.input.definitions ?? {}), ...(tool.input.$defs ?? {}) }, } - const definitions = document.definitions ?? {} let schema = document.schema if (schema.$ref !== undefined) { - const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1] - const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment) - const resolved = name === undefined ? undefined : definitions[name] + const name = definitionName(schema.$ref) + const resolved = name === undefined ? undefined : document.definitions[name] if (resolved === undefined) return [] schema = resolved } @@ -261,10 +276,14 @@ export const inputProperties = (tool: Tool): Array => { export const inputTypeScript = (tool: Tool, pretty = false): string => isEffectSchema(tool.input) ? toTypeScript(tool.input, false, pretty) : jsonSchemaToTypeScript(tool.input, pretty) -// Empty object schemas render as `{}` in compact form; anything with properties, -// an index signature, or union members renders differently, so equality is a -// conservative emptiness test for both Effect and JSON Schema inputs. -export const isEmptyInput = (tool: Tool): boolean => inputTypeScript(tool) === "{}" +// Effect inputs are empty only when their encoded root is Effect's empty-struct marker, which records and checked +// structs never are. Raw JSON Schema inputs keep the `{}` rendering check, which treats `{ type: "object" }` as empty. +export const isEmptyInput = (tool: Tool): boolean => + isEffectSchema(tool.input) + ? isEffectEmptyStruct( + Schema.toJsonSchemaDocument(tool.input, { referencePolicy: () => undefined }).schema as JsonSchema, + ) + : inputTypeScript(tool) === "{}" export const outputTypeScript = (tool: Tool, pretty = false): string => tool.output === undefined diff --git a/packages/codemode/src/tool.ts b/packages/codemode/src/tool.ts index 1b23d20c3683..e39138fd595d 100644 --- a/packages/codemode/src/tool.ts +++ b/packages/codemode/src/tool.ts @@ -13,6 +13,7 @@ export type JsonSchema = { readonly anyOf?: ReadonlyArray readonly oneOf?: ReadonlyArray readonly allOf?: ReadonlyArray + readonly not?: JsonSchema readonly properties?: Readonly> readonly required?: ReadonlyArray readonly items?: JsonSchema diff --git a/packages/codemode/test/openapi.test.ts b/packages/codemode/test/openapi.test.ts index 86b194c00139..5a70ae71e726 100644 --- a/packages/codemode/test/openapi.test.ts +++ b/packages/codemode/test/openapi.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { Effect, Layer, Option } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import { CodeMode, OpenAPI, Tool } from "../src/index.js" import { inputTypeScript, outputTypeScript } from "../src/tool-schema.js" diff --git a/packages/codemode/test/signature.test.ts b/packages/codemode/test/signature.test.ts index 5b9c98d1a6d8..ed41e9b02c21 100644 --- a/packages/codemode/test/signature.test.ts +++ b/packages/codemode/test/signature.test.ts @@ -1,7 +1,13 @@ import { describe, expect, test } from "bun:test" -import { Effect, Schema } from "effect" +import { Effect, Schema, SchemaGetter } from "effect" import { CodeMode, Tool } from "../src/index.js" -import { inputTypeScript, jsonSchemaToTypeScript, outputTypeScript } from "../src/tool-schema.js" +import { + decodeInput, + inputProperties, + inputTypeScript, + jsonSchemaToTypeScript, + outputTypeScript, +} from "../src/tool-schema.js" // A raw JSON Schema tool in the shape an MCP adapter produces: render-only input schema // whose property descriptions and constraints must surface as JSDoc in pretty signatures. @@ -525,6 +531,34 @@ describe("pretty signature rendering", () => { }) describe("JSON Schema definition scope", () => { + test.each(["Lookup Input", "café", "A/B~C% D#E"])( + "resolves Effect definition %s in signatures and input metadata", + (identifier) => { + const tool = Tool.make({ + description: "Named tool", + input: Schema.Struct({ city: Schema.String }).annotate({ identifier }), + output: Schema.Struct({ city: Schema.String }).annotate({ identifier }), + execute: (input) => Effect.succeed(input), + }) + expect(inputTypeScript(tool)).toBe("{ city: string }") + expect(outputTypeScript(tool)).toBe("{ city: string }") + expect(inputProperties(tool)).toEqual([{ name: "city", description: undefined, required: true }]) + }, + ) + + test.each(["#/$defs/%", "#/$defs/A~2B", "#/$defs/Missing", "#/$defs/Loop", "https://example.test/schema"])( + "keeps malformed, unresolved, and recursive reference %s unknown", + ($ref) => { + const tool = Tool.make({ + description: "Unresolved tool", + input: { $ref, $defs: { Loop: { $ref: "#/$defs/Loop" } } }, + execute: () => Effect.succeed(null), + }) + expect(inputTypeScript(tool)).toBe("unknown") + expect(inputProperties(tool)).toEqual([]) + }, + ) + test.each(["definitions", "$defs"])("resolves root %s and lets $defs take precedence", (key) => { const schema = { $ref: `#/${key}/Value`, [key]: { Value: { type: "string" } } } expect(jsonSchemaToTypeScript(schema)).toBe("string") @@ -612,14 +646,102 @@ describe("non-identifier property names render as quoted keys", () => { input: Schema.Struct({ "foo-bar": Schema.String, plain: Schema.optionalKey(Schema.Number) }), execute: () => Effect.succeed(null), }) - expect(inputTypeScript(tool)).toBe('{ "foo-bar": string; plain?: number | "Infinity" | "-Infinity" | "NaN" }') - expect(inputTypeScript(tool, true)).toBe( - ["{", ' "foo-bar": string,', ' plain?: number | "Infinity" | "-Infinity" | "NaN",', "}"].join("\n"), - ) + expect(inputTypeScript(tool)).toBe('{ "foo-bar": string; plain?: number }') + expect(inputTypeScript(tool, true)).toBe(["{", ' "foo-bar": string,', " plain?: number,", "}"].join("\n")) }) }) describe("union schemas render every alternative", () => { + test("Effect numbers advertise the numeric values accepted by the input decoder", () => { + const tool = Tool.make({ + description: "Numeric tool", + input: Schema.Struct({ amount: Schema.Number }), + output: Schema.Number, + execute: (input) => Effect.succeed(input.amount), + }) + expect(inputTypeScript(tool)).toBe("{ amount: number }") + expect(inputTypeScript(tool, true)).toBe("{\n amount: number,\n}") + expect(outputTypeScript(tool)).toBe("number") + expect(outputTypeScript(tool, true)).toBe("number") + expect(decodeInput(tool, { amount: 42 })).toEqual({ amount: 42 }) + for (const amount of ["NaN", "Infinity", "-Infinity"]) { + expect(() => decodeInput(tool, { amount })).toThrow("Expected number") + } + }) + + test("Effect number encodings render as number wherever they appear", () => { + const Amount = Schema.Number.annotate({ identifier: "Amount" }) + const schema = Schema.Struct({ + plain: Schema.Number, + count: Schema.Int, + bounded: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 10 })), + positive: Schema.Number.check(Schema.isGreaterThan(0)), + maybe: Schema.optionalKey(Schema.Number), + nullable: Schema.NullOr(Schema.Number), + list: Schema.Array(Schema.Number), + scores: Schema.Record(Schema.String, Schema.Number), + nested: Schema.Struct({ first: Amount, second: Amount }), + }) + const tool = Tool.make({ description: "Numbers", input: schema, output: schema, execute: Effect.succeed }) + const compact = [ + "{ plain: number", + "count: number", + "bounded: number", + "positive: number", + "maybe?: number", + "nullable: number | null", + "list: Array", + "scores: { [key: string]: number }", + "nested: { first: number; second: number } }", + ].join("; ") + expect(inputTypeScript(tool)).toBe(compact) + expect(outputTypeScript(tool)).toBe(compact) + for (const rendered of [inputTypeScript(tool, true), outputTypeScript(tool, true)]) { + expect(rendered).toContain(" /** @integer */\n count: number,") + expect(rendered).toContain(" /** @integer @minimum 1 @maximum 10 */\n bounded: number,") + expect(rendered).not.toContain("Infinity") + } + const named = Tool.make({ description: "n", input: Amount, output: Amount, execute: Effect.succeed }) + expect(inputTypeScript(named)).toBe("number") + expect(outputTypeScript(named)).toBe("number") + }) + + test("strings that only spell non-finite numbers render as number", () => { + const literals = ["NaN", "Infinity", "-Infinity"] as const + const union = Schema.Union([Schema.Finite, Schema.Literals(literals)]) + const tool = Tool.make({ description: "n", input: union, output: union, execute: Effect.succeed }) + expect(inputTypeScript(tool)).toBe("number") + expect(outputTypeScript(tool)).toBe("number") + // The signature only narrows what the model is told; the strings still decode. + for (const value of literals) expect(decodeInput(tool, value)).toBe(value) + + expect(jsonSchemaToTypeScript({ anyOf: [{ type: "number" }, { type: "string", enum: [...literals] }] })).toBe( + "number", + ) + expect( + jsonSchemaToTypeScript({ + anyOf: [{ type: "number" }, ...literals.map((value) => ({ type: "string", enum: [value] }))], + }), + ).toBe("number") + }) + + test("keeps string enums with other values alongside numbers", () => { + expect( + jsonSchemaToTypeScript({ + anyOf: [{ type: "number" }, { type: "string", enum: ["NaN", "Infinity", "unknown"] }], + }), + ).toBe('number | "NaN" | "Infinity" | "unknown"') + const tool = Tool.make({ + description: "Number or status", + input: Schema.Union([Schema.Number, Schema.Literal("unknown")]), + output: Schema.Union([Schema.Number, Schema.Literal("unknown")]), + execute: (input) => Effect.succeed(input), + }) + expect(inputTypeScript(tool)).toContain('"unknown"') + expect(outputTypeScript(tool)).toContain('"unknown"') + expect(decodeInput(tool, "unknown")).toBe("unknown") + }) + test("anyOf with a number branch keeps sibling alternatives", () => { const schema = { anyOf: [{ type: "string" }, { type: "number" }], @@ -703,7 +825,7 @@ describe("JSDoc signatures in catalogs and search results", () => { source: "Effect", schema: Schema.Struct({ count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(10)), - name: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(20), Schema.isPattern(/^[a-z]+$/)), + name: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(20), Schema.isPattern(/^[a-z]+$/u)), labels: Schema.Array(Schema.String.check(Schema.isMinLength(1))).check( Schema.isMinLength(1), Schema.isMaxLength(5), @@ -807,6 +929,128 @@ describe("JSDoc signatures in catalogs and search results", () => { }) }) +describe("empty input signatures agree with decoding", () => { + test.each([ + { name: "a plain empty struct", input: Schema.Struct({}) }, + { + name: "an annotated empty struct", + input: Schema.Struct({}).annotate({ identifier: "Empty", description: "No input" }), + }, + { name: "a checked empty struct", input: Schema.Struct({}).check(Schema.makeFilter(() => true)) }, + { + name: "an empty encoded struct transformed to a non-empty struct", + input: Schema.Struct({}).pipe( + Schema.decodeTo(Schema.Struct({ token: Schema.String }), { + decode: SchemaGetter.transform(() => ({ token: "x" })), + encode: SchemaGetter.transform(() => ({})), + }), + ), + }, + { name: "a raw JSON Schema empty object", input: { type: "object", properties: {} } }, + ])("$name advertises () and runs with zero arguments", async ({ input }) => { + const runtime = CodeMode.make({ + tools: { + ping: Tool.make({ description: "Ping", input, output: Schema.String, execute: () => Effect.succeed("pong") }), + }, + }) + const signature = "tools.ping(): Promise" + expect(runtime.catalog[0]?.signature).toBe(signature) + const result = await Effect.runPromise(runtime.execute('return search({ query: "tools.ping" })')) + expect(result.ok).toBe(true) + if (!result.ok) throw new Error("search failed") + expect(result.value).toMatchObject({ items: [{ signature }] }) + expect(await Effect.runPromise(runtime.execute("return await tools.ping()"))).toMatchObject({ + ok: true, + value: "pong", + }) + }) + + test.each([ + { name: "a record of JSON values", input: Schema.Record(Schema.String, Schema.Json), call: "{ a: 1 }" }, + { name: "a record of unknown values", input: Schema.Record(Schema.String, Schema.Unknown), call: "{ a: 1 }" }, + { + name: "a pattern-keyed record", + input: Schema.Record(Schema.TemplateLiteral(["x-", Schema.String]), Schema.String), + call: '{ "x-a": "1" }', + }, + { name: "a constrained empty struct", input: Schema.Struct({}).check(Schema.isMinProperties(1)), call: "{ a: 1 }" }, + ])("$name is not advertised as a zero-argument call", async ({ input, call }) => { + const runtime = CodeMode.make({ + tools: { + take: Tool.make({ description: "Take", input, output: Schema.String, execute: () => Effect.succeed("taken") }), + }, + }) + expect(runtime.catalog[0]?.signature).not.toBe("tools.take(): Promise") + expect(await Effect.runPromise(runtime.execute(`return await tools.take(${call})`))).toMatchObject({ + ok: true, + value: "taken", + }) + }) + + test("a required encoded input transformed to an empty struct keeps its required fields", async () => { + const consume = Tool.make({ + description: "Consume token", + input: Schema.Struct({ token: Schema.String }).pipe( + Schema.decodeTo(Schema.Struct({}), { + decode: SchemaGetter.transform(() => ({})), + encode: SchemaGetter.transform(() => ({ token: "x" })), + }), + ), + output: Schema.String, + execute: () => Effect.succeed("consumed"), + }) + const runtime = CodeMode.make({ tools: { consume } }) + const signature = "tools.consume({\n token: string,\n}): Promise" + expect(() => decodeInput(consume, {})).toThrow() + expect(runtime.catalog[0]?.signature).toBe(signature) + const result = await Effect.runPromise(runtime.execute('return search({ query: "tools.consume" })')) + expect(result.ok).toBe(true) + if (!result.ok) throw new Error("search failed") + expect(result.value).toMatchObject({ items: [{ signature }] }) + expect(await Effect.runPromise(runtime.execute("return await tools.consume()"))).toMatchObject({ + ok: false, + error: { kind: "InvalidToolInput" }, + }) + expect(await Effect.runPromise(runtime.execute('return await tools.consume({ token: "t" })'))).toMatchObject({ + ok: true, + value: "consumed", + }) + }) +}) + +describe("empty object signatures", () => { + const output = (schema: Schema.Decoder) => + Tool.make({ description: "Inspect", input: Schema.Struct({}), output: schema, execute: () => Effect.succeed({}) }) + + test("an empty struct output renders as {}", () => { + const tool = output(Schema.Struct({})) + expect(outputTypeScript(tool)).toBe("{}") + expect(outputTypeScript(tool, true)).toBe("{}") + expect(CodeMode.make({ tools: { inspect: tool } }).catalog[0]?.signature).toBe("tools.inspect(): Promise<{}>") + }) + + test("nested empty structs render as {}", () => { + const tool = output( + Schema.Struct({ + meta: Schema.Struct({}), + extra: Schema.optionalKey(Schema.Struct({}).annotate({ description: "Extra data" })), + }), + ) + expect(outputTypeScript(tool)).toBe("{ meta: {}; extra?: {} }") + expect(outputTypeScript(tool, true)).toBe( + ["{", " meta: {},", " /** Extra data */", " extra?: {},", "}"].join("\n"), + ) + }) + + test("raw schemas render only the exact empty-struct shapes as {}", () => { + expect(jsonSchemaToTypeScript({ not: { type: "null" } })).toBe("{}") + expect(jsonSchemaToTypeScript({ anyOf: [{ type: "object" }, { type: "array" }] })).toBe("{}") + expect(jsonSchemaToTypeScript({ type: "string", not: { type: "null" } })).toBe("string") + expect(jsonSchemaToTypeScript({ not: { type: "string" } })).toBe("unknown") + expect(jsonSchemaToTypeScript({ not: { type: "null", description: "Not null" } })).toBe("unknown") + }) +}) + describe("non-identifier tool paths", () => { const resolveLibrary = Tool.make({ description: "Resolve a Context7 library ID", diff --git a/packages/core/package.json b/packages/core/package.json index f3b7fb973891..c528e08e7583 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -92,7 +92,8 @@ "@types/bun": "catalog:", "@types/node": "catalog:", "@types/which": "3.0.4", - "drizzle-kit": "catalog:" + "drizzle-kit": "catalog:", + "fast-check": "catalog:" }, "dependencies": { "@ai-sdk/cohere": "3.0.27", diff --git a/packages/core/script/benchmark-location-memory.ts b/packages/core/script/benchmark-location-memory.ts index 416589c935f5..5d6e674feaeb 100644 --- a/packages/core/script/benchmark-location-memory.ts +++ b/packages/core/script/benchmark-location-memory.ts @@ -13,7 +13,7 @@ import os from "os" import path from "path" import { heapStats } from "bun:jsc" import { Effect, Layer, Logger, Scope } from "effect" -import { FetchHttpClient } from "effect/unstable/http" +import { FetchHttpClient } from "effect/http" import { Global } from "@opencode/util/global" import { LayerNode } from "@opencode/util/effect/layer-node" import { AppNodeBuilder } from "../src/effect/app-node-builder" diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index 9f14aaa91572..a392273dad70 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -33,8 +33,8 @@ import { import { Auth, Endpoint, RequestExecutor, type AnyRoute, type HttpMiddleware } from "@opencode/ai/route" import { ProviderShared } from "@opencode/ai/protocols/shared" import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect" -import { makeParser } from "effect/unstable/encoding/Sse" -import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { makeParser } from "effect/encoding/Sse" +import { HttpClientRequest, HttpClientResponse } from "effect/http" import { AsyncLocalStorage } from "node:async_hooks" import type { ID, RuntimeInfo } from "./model.js" import { Provider } from "./provider.js" diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index fce18f08aecb..0f1ce308524d 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -99,7 +99,7 @@ export const layer = (options?: Options) => const wellknown = yield* WellKnown.Service const reloadLock = Semaphore.makeUnsafe(1) const updateLock = Semaphore.makeUnsafe(1) - const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const + const decodeOptions = { errors: "all", onExcessProperty: "ignore" } as const const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions) const parseInfo = Effect.fn("Config.parseInfo")(function* (text: string, source: string) { const errors: ParseError[] = [] @@ -330,7 +330,8 @@ export const layer = (options?: Options) => function* (patch: Patch) { const directory = initial.global ?? AbsolutePath.make(globalService.config) const candidates = ConfigDiscovery.names.map((name) => path.join(directory, name)) - const filepath = (yield* Effect.filter(candidates, fs.isFile)).at(-1) ?? path.join(directory, "opencode.jsonc") + const filepath = + (yield* Effect.filter(candidates, fs.isFile)).at(-1) ?? path.join(directory, "opencode.jsonc") const text = (yield* fs.readFileStringSafe(filepath)) ?? "{}\n" const updated = yield* Effect.try({ try: () => diff --git a/packages/core/src/config/normalize.ts b/packages/core/src/config/normalize.ts index f400b59ae2d8..dd3fb595542a 100644 --- a/packages/core/src/config/normalize.ts +++ b/packages/core/src/config/normalize.ts @@ -41,7 +41,7 @@ export type Result = } | { readonly type: "rejected"; readonly diagnostics: readonly Diagnostic[] } -const options = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const +const options = { errors: "all", onExcessProperty: "ignore" } as const const unsupportedTopLevel = ["logLevel", "server", "subagent_depth", "layout"] as const const unsupportedExperimental = [ "disable_paste_summary", diff --git a/packages/core/src/config/plugin/agent.ts b/packages/core/src/config/plugin/agent.ts index 7f96a3f22af5..1d3949d015ea 100644 --- a/packages/core/src/config/plugin/agent.ts +++ b/packages/core/src/config/plugin/agent.ts @@ -196,11 +196,11 @@ function decode(file: { directory: string; filepath: string; primary: boolean }, const agent = legacy ? Option.getOrUndefined( Option.map( - decodeLegacyAgent({ name, ...markdown.data, prompt: body }, { errors: "all", propertyOrder: "original" }), + decodeLegacyAgent({ name, ...markdown.data, prompt: body }, { errors: "all" }), ConfigMigrateV1.migrateAgent, ), ) - : Option.getOrUndefined(decodeAgent({ ...data, system: body }, { errors: "all", propertyOrder: "original" })) + : Option.getOrUndefined(decodeAgent({ ...data, system: body }, { errors: "all" })) if (!agent) return const info = Option.getOrUndefined( decodeConfig({ diff --git a/packages/core/src/config/plugin/command.ts b/packages/core/src/config/plugin/command.ts index cb90f1dd2acb..a029fb8adeff 100644 --- a/packages/core/src/config/plugin/command.ts +++ b/packages/core/src/config/plugin/command.ts @@ -8,7 +8,7 @@ import { Provider } from "@opencode/schema/provider" import { AppProcess } from "@opencode/util/process" import path from "path" import { Effect, Option, PubSub, Schema, Stream } from "effect" -import { ChildProcess } from "effect/unstable/process" +import { ChildProcess } from "effect/process" import { Agent } from "../../agent.js" import { Config } from "../../config.js" import { Location } from "../../location.js" diff --git a/packages/core/src/database/database.ts b/packages/core/src/database/database.ts index 249152333da1..93724052fc00 100644 --- a/packages/core/src/database/database.ts +++ b/packages/core/src/database/database.ts @@ -3,7 +3,7 @@ export * as Database from "./database.js" import { EffectDrizzleSqlite } from "./drizzle.js" import { sqliteLayer, supportsForeignKeyToggle, supportsTuningPragmas } from "#sqlite" import { Context, Effect, Layer, Schema, Semaphore } from "effect" -import type { SqlClient } from "effect/unstable/sql" +import type { SqlClient } from "effect/sql" import { Global } from "@opencode/util/global" import { closeSync, existsSync, openSync } from "node:fs" import { chmod } from "node:fs/promises" diff --git a/packages/core/src/database/drizzle/effect-sqlite/driver.ts b/packages/core/src/database/drizzle/effect-sqlite/driver.ts index 669aa0d211f4..e5579e366ead 100644 --- a/packages/core/src/database/drizzle/effect-sqlite/driver.ts +++ b/packages/core/src/database/drizzle/effect-sqlite/driver.ts @@ -1,7 +1,7 @@ /* oxlint-disable */ import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" -import { SqlClient } from "effect/unstable/sql/SqlClient" +import { SqlClient } from "effect/sql/SqlClient" import { EffectCache } from "drizzle-orm/cache/core/cache-effect" import { EffectLogger } from "drizzle-orm/effect-core" import { entityKind } from "drizzle-orm/entity" diff --git a/packages/core/src/database/drizzle/effect-sqlite/session.ts b/packages/core/src/database/drizzle/effect-sqlite/session.ts index 6b4ab20dd10e..9297bc7b6772 100644 --- a/packages/core/src/database/drizzle/effect-sqlite/session.ts +++ b/packages/core/src/database/drizzle/effect-sqlite/session.ts @@ -3,8 +3,8 @@ import * as Context from "effect/Context" import * as Effect from "effect/Effect" import * as Exit from "effect/Exit" import * as Scope from "effect/Scope" -import type { SqlClient } from "effect/unstable/sql/SqlClient" -import type { SqlError } from "effect/unstable/sql/SqlError" +import type { SqlClient } from "effect/sql/SqlClient" +import type { SqlError } from "effect/sql/SqlError" import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect" import type { WithCacheConfig } from "drizzle-orm/cache/core/types" import type { EffectDrizzleQueryError } from "drizzle-orm/effect-core/errors" diff --git a/packages/core/src/database/drizzle/sqlite-core/effect/db.ts b/packages/core/src/database/drizzle/sqlite-core/effect/db.ts index a1c44b392ad5..ccf96e5fb271 100644 --- a/packages/core/src/database/drizzle/sqlite-core/effect/db.ts +++ b/packages/core/src/database/drizzle/sqlite-core/effect/db.ts @@ -1,6 +1,6 @@ /* oxlint-disable */ import { Effect } from "effect" -import type { SqlError } from "effect/unstable/sql/SqlError" +import type { SqlError } from "effect/sql/SqlError" import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect" import type { MutationOption } from "drizzle-orm/cache/core/cache" import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" diff --git a/packages/core/src/database/drizzle/sqlite-core/effect/session.ts b/packages/core/src/database/drizzle/sqlite-core/effect/session.ts index 1ea0eca1b72d..d53f25447653 100644 --- a/packages/core/src/database/drizzle/sqlite-core/effect/session.ts +++ b/packages/core/src/database/drizzle/sqlite-core/effect/session.ts @@ -1,7 +1,7 @@ /* oxlint-disable */ import * as Cause from "effect/Cause" import * as Effect from "effect/Effect" -import type { SqlError } from "effect/unstable/sql/SqlError" +import type { SqlError } from "effect/sql/SqlError" import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect" import { NoopCache, strategyFor } from "drizzle-orm/cache/core/cache" import type { WithCacheConfig } from "drizzle-orm/cache/core/types" diff --git a/packages/core/src/database/sqlite.bun.ts b/packages/core/src/database/sqlite.bun.ts index ddf586d945d7..7728bc8f4e95 100644 --- a/packages/core/src/database/sqlite.bun.ts +++ b/packages/core/src/database/sqlite.bun.ts @@ -1,8 +1,8 @@ import { Database, type SQLQueryBindings } from "bun:sqlite" import { Context, Effect, Layer } from "effect" -import { Reactivity } from "effect/unstable/reactivity" -import { SqlClient } from "effect/unstable/sql" -import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError" +import { Reactivity } from "effect/reactivity" +import { SqlClient } from "effect/sql" +import { classifySqliteError, SqlError } from "effect/sql/SqlError" import { Sqlite } from "./sqlite.js" const TypeId = "~@opencode/core/database/SqliteBun" as const diff --git a/packages/core/src/database/sqlite.node.ts b/packages/core/src/database/sqlite.node.ts index 354e14076558..9053a0def663 100644 --- a/packages/core/src/database/sqlite.node.ts +++ b/packages/core/src/database/sqlite.node.ts @@ -1,8 +1,8 @@ import { DatabaseSync, type SQLInputValue } from "node:sqlite" import { Context, Effect, Layer } from "effect" -import { Reactivity } from "effect/unstable/reactivity" -import { SqlClient } from "effect/unstable/sql" -import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError" +import { Reactivity } from "effect/reactivity" +import { SqlClient } from "effect/sql" +import { classifySqliteError, SqlError } from "effect/sql/SqlError" import { Sqlite } from "./sqlite.js" const TypeId = "~@opencode/core/database/SqliteNode" as const diff --git a/packages/core/src/database/sqlite.ts b/packages/core/src/database/sqlite.ts index 884626d6ad2e..186274fe3e31 100644 --- a/packages/core/src/database/sqlite.ts +++ b/packages/core/src/database/sqlite.ts @@ -2,9 +2,9 @@ export * as Sqlite from "./sqlite.js" import { Context, Effect, Fiber, Scope, Semaphore, Stream } from "effect" import { identity } from "effect/Function" -import { SqlClient, Statement } from "effect/unstable/sql" -import type { Connection } from "effect/unstable/sql/SqlConnection" -import type { SqlError } from "effect/unstable/sql/SqlError" +import { SqlClient, Statement } from "effect/sql" +import type { Connection } from "effect/sql/SqlConnection" +import type { SqlError } from "effect/sql/SqlError" export class Native extends Context.Service()("@opencode/core/database/SqliteNative") {} diff --git a/packages/core/src/database/sqlite.workerd.ts b/packages/core/src/database/sqlite.workerd.ts index 86e38e3163cb..7c032507f0f0 100644 --- a/packages/core/src/database/sqlite.workerd.ts +++ b/packages/core/src/database/sqlite.workerd.ts @@ -1,8 +1,8 @@ import { Context, Effect, Exit, Fiber, Layer, Scope, Semaphore } from "effect" -import { Reactivity } from "effect/unstable/reactivity" -import { SqlClient, Statement } from "effect/unstable/sql" -import type { Connection } from "effect/unstable/sql/SqlConnection" -import { classifySqliteError, SqlError, UnknownError } from "effect/unstable/sql/SqlError" +import { Reactivity } from "effect/reactivity" +import { SqlClient, Statement } from "effect/sql" +import type { Connection } from "effect/sql/SqlConnection" +import { classifySqliteError, SqlError, UnknownError } from "effect/sql/SqlError" import type { NativeTransactionSqlClient } from "./drizzle/effect-sqlite/session.js" import { Sqlite } from "./sqlite.js" diff --git a/packages/core/src/effect/app-node-platform.ts b/packages/core/src/effect/app-node-platform.ts index 718c6456c7b9..2f0070cfb316 100644 --- a/packages/core/src/effect/app-node-platform.ts +++ b/packages/core/src/effect/app-node-platform.ts @@ -1,5 +1,5 @@ import { LLMClient, RequestExecutor } from "@opencode/ai/route" -import { Socket } from "effect/unstable/socket" +import { Socket } from "effect/socket" import { makeGlobalNode } from "@opencode/util/effect/app-node" import { httpClient } from "@opencode/util/effect/app-node-platform" import { WebSocketConstructor } from "./websocket-constructor.js" diff --git a/packages/core/src/effect/websocket-constructor.ts b/packages/core/src/effect/websocket-constructor.ts index 4d1031902866..444ff1d8bad7 100644 --- a/packages/core/src/effect/websocket-constructor.ts +++ b/packages/core/src/effect/websocket-constructor.ts @@ -3,11 +3,9 @@ import { NodeWS } from "@effect/platform-node/NodeSocket" import { HttpProxyAgent } from "http-proxy-agent" import { HttpsProxyAgent } from "https-proxy-agent" import { Layer } from "effect" -import { Headers } from "effect/unstable/http" -import { Socket } from "effect/unstable/socket" +import { Socket } from "effect/socket" -interface WebSocketOptions { - readonly headers?: Headers.Headers +interface WebSocketOptions extends Socket.WebSocketClientOptions { readonly protocols?: string | Array } @@ -48,11 +46,8 @@ const proxy = (value: string, environment: Environment = process.env) => { ) } -const constructorOptions = (input: string | Array | undefined): WebSocketOptions => { - if (typeof input === "string" || Array.isArray(input)) return { protocols: input } - // AI routes pass handshake options through Effect's browser-shaped constructor. - return (input ?? {}) as WebSocketOptions -} +const constructorOptions = (input: Socket.WebSocketConstructorOptions | undefined): WebSocketOptions => + typeof input === "string" || Array.isArray(input) ? { protocols: input } : { headers: input?.headers } const proxyAgent = (url: string, selectedProxy: string | undefined) => { if (!selectedProxy) return undefined @@ -80,11 +75,7 @@ const layer = Layer.succeed(Socket.WebSocketConstructor, (url, input) => { // Reject redirects before headers can cross an origin boundary; the caller safely falls back to HTTP. followRedirects: false, } - const socket = config.protocols - ? new NodeWS.WebSocket(url, config.protocols, native) - : new NodeWS.WebSocket(url, native) - // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- ws implements the WebSocket surface consumed by the AI transport. - return socket as unknown as globalThis.WebSocket + return config.protocols ? new NodeWS.WebSocket(url, config.protocols, native) : new NodeWS.WebSocket(url, native) }) export const WebSocketConstructor = { layer, proxy } as const diff --git a/packages/core/src/environment/driver.ts b/packages/core/src/environment/driver.ts index 39c0d2c74559..85162fd123b3 100644 --- a/packages/core/src/environment/driver.ts +++ b/packages/core/src/environment/driver.ts @@ -1,4 +1,4 @@ -import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" +import type { ChildProcessSpawner } from "effect/process/ChildProcessSpawner" import type { FilesImpl } from "./files.js" export interface Driver { diff --git a/packages/core/src/environment/environment.ts b/packages/core/src/environment/environment.ts index 0dc997fda93f..416c0d08064d 100644 --- a/packages/core/src/environment/environment.ts +++ b/packages/core/src/environment/environment.ts @@ -1,7 +1,7 @@ import { CrossSpawnSpawner } from "@opencode/util/cross-spawn-spawner" import { makeLocationNode } from "@opencode/util/effect/app-node" import { Context, Effect, Layer } from "effect" -import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" +import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner" import type { Files } from "./files.js" import { makeFiles } from "./index.js" import { makeLocalDriver } from "./local.js" diff --git a/packages/core/src/environment/exec-defaults.ts b/packages/core/src/environment/exec-defaults.ts index d2350fb75884..d315fb26e647 100644 --- a/packages/core/src/environment/exec-defaults.ts +++ b/packages/core/src/environment/exec-defaults.ts @@ -1,6 +1,6 @@ import { Effect, Stream } from "effect" -import { ChildProcess } from "effect/unstable/process" -import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" +import { ChildProcess } from "effect/process" +import type { ChildProcessSpawner } from "effect/process/ChildProcessSpawner" import { collectStream } from "@opencode/util/process" import { Failed, NotFound, WrongKind, type FileInfo, type FileType, type FilesImpl } from "./files.js" diff --git a/packages/core/src/environment/local.ts b/packages/core/src/environment/local.ts index a2a044523a08..24b237855ea6 100644 --- a/packages/core/src/environment/local.ts +++ b/packages/core/src/environment/local.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises" import path from "node:path" import { Effect } from "effect" -import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" +import type { ChildProcessSpawner } from "effect/process/ChildProcessSpawner" import type { Driver } from "./driver.js" import { Failed, NotFound, WrongKind, type FileInfo, type FilesImpl, type FileType } from "./files.js" diff --git a/packages/core/src/environment/memory.ts b/packages/core/src/environment/memory.ts index 0e8003a6e3d3..c7f23115645e 100644 --- a/packages/core/src/environment/memory.ts +++ b/packages/core/src/environment/memory.ts @@ -1,6 +1,6 @@ import path from "node:path" import { Effect, PlatformError } from "effect" -import { make } from "effect/unstable/process/ChildProcessSpawner" +import { make } from "effect/process/ChildProcessSpawner" import type { Driver } from "./driver.js" import { Failed, NotFound, WrongKind, type FileInfo, type FilesImpl, type FileType } from "./files.js" diff --git a/packages/core/src/environment/unavailable.ts b/packages/core/src/environment/unavailable.ts index 59db611a77fd..56e7b290444c 100644 --- a/packages/core/src/environment/unavailable.ts +++ b/packages/core/src/environment/unavailable.ts @@ -1,5 +1,5 @@ import { Effect, Layer, PlatformError } from "effect" -import { ChildProcessSpawner, make } from "effect/unstable/process/ChildProcessSpawner" +import { ChildProcessSpawner, make } from "effect/process/ChildProcessSpawner" export const spawner = make(() => Effect.fail( diff --git a/packages/core/src/formatter.ts b/packages/core/src/formatter.ts index eb85babf56ee..46bb4e6b2835 100644 --- a/packages/core/src/formatter.ts +++ b/packages/core/src/formatter.ts @@ -1,7 +1,7 @@ export * as Formatter from "./formatter.js" import { Context, Effect, Layer } from "effect" -import { ChildProcess } from "effect/unstable/process" +import { ChildProcess } from "effect/process" import path from "path" import { makeLocationNode } from "@opencode/util/effect/app-node" import { AppProcess } from "@opencode/util/process" diff --git a/packages/core/src/formatter/builtins.ts b/packages/core/src/formatter/builtins.ts index ba8354f807b4..86b3714add58 100644 --- a/packages/core/src/formatter/builtins.ts +++ b/packages/core/src/formatter/builtins.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { ChildProcess } from "effect/unstable/process" +import { ChildProcess } from "effect/process" import { FSUtil } from "@opencode/util/fs-util" import { Npm } from "@opencode/util/npm" import { AppProcess } from "@opencode/util/process" diff --git a/packages/core/src/git.ts b/packages/core/src/git.ts index 5144d6931b64..b633eed780e2 100644 --- a/packages/core/src/git.ts +++ b/packages/core/src/git.ts @@ -2,7 +2,7 @@ export * as Git from "./git.js" import path from "path" import { Cause, Context, Effect, Exit, Layer, Option, Schedule, Schema } from "effect" -import { ChildProcess } from "effect/unstable/process" +import { ChildProcess } from "effect/process" import { AbsolutePath, RelativePath } from "./schema.js" import { FSUtil } from "@opencode/util/fs-util" import { AppProcess } from "@opencode/util/process" diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 177666127b02..8480e4632453 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -24,7 +24,7 @@ import { State } from "./state.js" import { Bus } from "./bus.js" import { IntegrationConnection } from "./integration/connection.js" import { AppProcess } from "@opencode/util/process" -import { ChildProcess } from "effect/unstable/process" +import { ChildProcess } from "effect/process" import { Form } from "./form.js" export const ID = Integration.ID diff --git a/packages/core/src/mcp/stdio.ts b/packages/core/src/mcp/stdio.ts index 79faf9536e73..31dd7f224e14 100644 --- a/packages/core/src/mcp/stdio.ts +++ b/packages/core/src/mcp/stdio.ts @@ -2,8 +2,8 @@ export * as McpStdio from "./stdio.js" import { ReadBuffer, serializeMessage, type JSONRPCMessage, type Transport } from "@modelcontextprotocol/client" import { Cause, Duration, Effect, Fiber, Option, Queue, Scope, Stream } from "effect" -import { ChildProcess } from "effect/unstable/process" -import type { ChildProcessHandle } from "effect/unstable/process/ChildProcessSpawner" +import { ChildProcess } from "effect/process" +import type { ChildProcessHandle } from "effect/process/ChildProcessSpawner" import { Environment } from "../environment/index.js" /** Mirrors StdioClientTransport: wait this long for a graceful exit after stdin closes. */ diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index ce0c1963ca4a..84151f5554fb 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -1,5 +1,5 @@ import { Cause, Context, Duration, Effect, Layer, Option, Schedule, Schema, Semaphore } from "effect" -import { HttpClient, HttpClientRequest } from "effect/unstable/http" +import { HttpClient, HttpClientRequest } from "effect/http" import { ModelsDev } from "@opencode/schema/models-dev" import { Money } from "@opencode/schema/money" import { App } from "./app.js" diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index d805242c2ef9..f6239be4cbd1 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -6,7 +6,7 @@ import { LayerNode } from "@opencode/util/effect/layer-node" import { httpClient } from "@opencode/util/effect/app-node-platform" import { AppProcess } from "@opencode/util/process" import { Context, Effect, Scope } from "effect" -import { HttpClient } from "effect/unstable/http" +import { HttpClient } from "effect/http" import { Agent } from "../agent.js" import { Model } from "../model.js" import { Provider } from "../provider.js" diff --git a/packages/core/src/plugin/provider/azure.ts b/packages/core/src/plugin/provider/azure.ts index 15f44888e1c0..26e3cf76c8a7 100644 --- a/packages/core/src/plugin/provider/azure.ts +++ b/packages/core/src/plugin/provider/azure.ts @@ -1,6 +1,6 @@ import { Clock, Effect, FiberHandle, Option, Schema, Semaphore, Stream } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" -import { ChildProcess } from "effect/unstable/process" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" +import { ChildProcess } from "effect/process" import path from "node:path" import { define } from "@opencode/plugin/effect/plugin" import { Form } from "@opencode/schema/form" diff --git a/packages/core/src/plugin/provider/chatgpt.ts b/packages/core/src/plugin/provider/chatgpt.ts index 2ad40576efe9..6f1219240f91 100644 --- a/packages/core/src/plugin/provider/chatgpt.ts +++ b/packages/core/src/plugin/provider/chatgpt.ts @@ -3,7 +3,7 @@ import type { Context } from "@opencode/plugin/effect/plugin" import { define } from "@opencode/plugin/effect/plugin" // import { Deferred, Duration, Effect, Option, Schema, Semaphore, Stream } from "effect" import { Deferred, Effect, Option, Schema, Semaphore, Stream } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import type { Server, ServerResponse } from "node:http" import { App } from "../../app.js" import { Credential } from "../../credential.js" diff --git a/packages/core/src/plugin/provider/digitalocean.ts b/packages/core/src/plugin/provider/digitalocean.ts index 1d2a6358c18e..653d3243fbdd 100644 --- a/packages/core/src/plugin/provider/digitalocean.ts +++ b/packages/core/src/plugin/provider/digitalocean.ts @@ -1,7 +1,7 @@ import type { IntegrationOAuthMethodRegistration } from "@opencode/plugin/effect/integration" import { define } from "@opencode/plugin/effect/plugin" import { Clock, Deferred, Effect, Option, Schedule, Schema, Semaphore, Stream } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import type { Server } from "node:http" import { App } from "../../app.js" import { Bus } from "../../bus.js" diff --git a/packages/core/src/plugin/provider/gitlab.ts b/packages/core/src/plugin/provider/gitlab.ts index 45652ec88194..45891e8d6a16 100644 --- a/packages/core/src/plugin/provider/gitlab.ts +++ b/packages/core/src/plugin/provider/gitlab.ts @@ -1,7 +1,7 @@ import os from "os" import { App } from "../../app.js" import { Clock, Deferred, Effect, Exit, Option, Schema, Semaphore, Stream } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import type { Server, ServerResponse } from "node:http" import { define } from "@opencode/plugin/effect/plugin" import { Form } from "@opencode/schema/form" diff --git a/packages/core/src/plugin/provider/local.ts b/packages/core/src/plugin/provider/local.ts index 5b77dd8f5d95..c4eba8c74248 100644 --- a/packages/core/src/plugin/provider/local.ts +++ b/packages/core/src/plugin/provider/local.ts @@ -1,7 +1,7 @@ import { define } from "@opencode/plugin/effect/plugin" import type { Entry } from "@opencode/schema/config" import { Duration, Effect, Schedule, type Schema, Semaphore, Stream } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import { Config } from "../../config.js" import { Model } from "../../model.js" import type { PluginInternal } from "../internal.js" diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index 85b3d15efcfa..75de26333acf 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -2,7 +2,7 @@ import { Duration, Effect, Equal, Option, Schema, SchemaGetter, Scope, Semaphore import type { IntegrationOAuthMethodRegistration } from "@opencode/plugin/effect/integration" import { define } from "@opencode/plugin/effect/plugin" import type { SessionHttpResponse } from "@opencode/plugin/effect/session" -import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import { App } from "../../app.js" import { Bus } from "../../bus.js" import { Credential } from "../../credential.js" diff --git a/packages/core/src/plugin/provider/poe.ts b/packages/core/src/plugin/provider/poe.ts index f04c9b88839c..24edb14757f8 100644 --- a/packages/core/src/plugin/provider/poe.ts +++ b/packages/core/src/plugin/provider/poe.ts @@ -1,6 +1,6 @@ import { define } from "@opencode/plugin/effect/plugin" import { Clock, Deferred, Effect, Option, Schema } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import type { ServerResponse } from "node:http" import { Credential } from "../../credential.js" import { Integration } from "../../integration.js" diff --git a/packages/core/src/plugin/provider/snowflake-cortex.ts b/packages/core/src/plugin/provider/snowflake-cortex.ts index f36cab51892f..741f51a08c63 100644 --- a/packages/core/src/plugin/provider/snowflake-cortex.ts +++ b/packages/core/src/plugin/provider/snowflake-cortex.ts @@ -1,7 +1,7 @@ import { define } from "@opencode/plugin/effect/plugin" import { Form } from "@opencode/schema/form" import { Clock, Deferred, Effect, Option, Schema, Stream } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import { App } from "../../app.js" import { Bus } from "../../bus.js" import { Credential } from "../../credential.js" diff --git a/packages/core/src/plugin/vcs/git.ts b/packages/core/src/plugin/vcs/git.ts index 9efdc74d2982..d44ba2813527 100644 --- a/packages/core/src/plugin/vcs/git.ts +++ b/packages/core/src/plugin/vcs/git.ts @@ -6,7 +6,7 @@ import os from "os" import path from "path" import { define } from "@opencode/plugin/effect/plugin" import { Effect } from "effect" -import { ChildProcess } from "effect/unstable/process" +import { ChildProcess } from "effect/process" import { FileDiff } from "@opencode/schema/file-diff" import { Base, BranchList, FileStatus, Info, Mode } from "@opencode/schema/vcs" import { AppProcess } from "@opencode/util/process" diff --git a/packages/core/src/plugin/vcs/hg.ts b/packages/core/src/plugin/vcs/hg.ts index 7249253220c4..1d49dc49e321 100644 --- a/packages/core/src/plugin/vcs/hg.ts +++ b/packages/core/src/plugin/vcs/hg.ts @@ -2,7 +2,7 @@ export * as VcsHgPlugin from "./hg.js" import path from "path" import { Effect } from "effect" -import { ChildProcess } from "effect/unstable/process" +import { ChildProcess } from "effect/process" import { define } from "@opencode/plugin/effect/plugin" import { FileDiff } from "@opencode/schema/file-diff" import { FileStatus, Info, Mode } from "@opencode/schema/vcs" diff --git a/packages/core/src/plugin/websearch/exa.ts b/packages/core/src/plugin/websearch/exa.ts index 1dc3a26108b1..555780f4f2af 100644 --- a/packages/core/src/plugin/websearch/exa.ts +++ b/packages/core/src/plugin/websearch/exa.ts @@ -2,7 +2,7 @@ export * as WebSearchExa from "./exa.js" import { define } from "@opencode/plugin/effect/plugin" import { Effect, Schema, Scope } from "effect" -import { HttpClient } from "effect/unstable/http" +import { HttpClient } from "effect/http" import { WebSearchMcp } from "./mcp.js" export const endpoint = "https://mcp.exa.ai/mcp" diff --git a/packages/core/src/plugin/websearch/firecrawl.ts b/packages/core/src/plugin/websearch/firecrawl.ts index 1ea260e0c5da..39126ffcc776 100644 --- a/packages/core/src/plugin/websearch/firecrawl.ts +++ b/packages/core/src/plugin/websearch/firecrawl.ts @@ -2,7 +2,7 @@ export * as WebSearchFirecrawl from "./firecrawl.js" import { define } from "@opencode/plugin/effect/plugin" import { Effect, Option, Schema, Scope } from "effect" -import { HttpClient } from "effect/unstable/http" +import { HttpClient } from "effect/http" import { App } from "../../app.js" import { WebSearchMcp } from "./mcp.js" diff --git a/packages/core/src/plugin/websearch/mcp.ts b/packages/core/src/plugin/websearch/mcp.ts index 478d4feb5f8d..568d27b983b6 100644 --- a/packages/core/src/plugin/websearch/mcp.ts +++ b/packages/core/src/plugin/websearch/mcp.ts @@ -1,7 +1,7 @@ export * as WebSearchMcp from "./mcp.js" import { Duration, Effect, Schema } from "effect" -import { HttpClient, HttpClientRequest } from "effect/unstable/http" +import { HttpClient, HttpClientRequest } from "effect/http" import { collectBoundedResponseBody } from "../../tool/http-body.js" export const MAX_RESPONSE_BYTES = 256 * 1024 diff --git a/packages/core/src/plugin/websearch/parallel.ts b/packages/core/src/plugin/websearch/parallel.ts index 5184155482bd..5b858aff5260 100644 --- a/packages/core/src/plugin/websearch/parallel.ts +++ b/packages/core/src/plugin/websearch/parallel.ts @@ -2,7 +2,7 @@ export * as WebSearchParallel from "./parallel.js" import { define } from "@opencode/plugin/effect/plugin" import { Effect, Schema, Scope } from "effect" -import { HttpClient } from "effect/unstable/http" +import { HttpClient } from "effect/http" import { App } from "../../app.js" import { WebSearchMcp } from "./mcp.js" diff --git a/packages/core/src/plugin/websearch/tavily.ts b/packages/core/src/plugin/websearch/tavily.ts index 4b568a1fb0d2..54c047740133 100644 --- a/packages/core/src/plugin/websearch/tavily.ts +++ b/packages/core/src/plugin/websearch/tavily.ts @@ -2,7 +2,7 @@ export * as WebSearchTavily from "./tavily.js" import { define } from "@opencode/plugin/effect/plugin" import { Duration, Effect, Schema, Scope } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import { App } from "../../app.js" export const endpoint = "https://api.tavily.com/search" diff --git a/packages/core/src/plugin/websearch/tinyfish.ts b/packages/core/src/plugin/websearch/tinyfish.ts index 77d86acddc1d..604540f373ba 100644 --- a/packages/core/src/plugin/websearch/tinyfish.ts +++ b/packages/core/src/plugin/websearch/tinyfish.ts @@ -2,7 +2,7 @@ export * as WebSearchTinyFish from "./tinyfish.js" import { define } from "@opencode/plugin/effect/plugin" import { Effect, Option, Schema, Scope } from "effect" -import { HttpClient } from "effect/unstable/http" +import { HttpClient } from "effect/http" import { App } from "../../app.js" import { WebSearchMcp } from "./mcp.js" diff --git a/packages/core/src/project.ts b/packages/core/src/project.ts index bb51148330b8..6229abab030d 100644 --- a/packages/core/src/project.ts +++ b/packages/core/src/project.ts @@ -1,7 +1,7 @@ export * as Project from "./project.js" import { Context, Effect, Layer, Schema } from "effect" -import { ChildProcess } from "effect/unstable/process" +import { ChildProcess } from "effect/process" import { and, asc, desc, eq, gte, isNull, lte, sql } from "drizzle-orm" import path from "path" import { AbsolutePath } from "./schema.js" diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts index 4f2317e0d820..aa3b06f827dc 100644 --- a/packages/core/src/ripgrep.ts +++ b/packages/core/src/ripgrep.ts @@ -1,7 +1,7 @@ export * as Ripgrep from "./ripgrep.js" import { Context, Effect, Fiber, Layer, Schema, Stream } from "effect" -import { ChildProcess } from "effect/unstable/process" +import { ChildProcess } from "effect/process" import { Entry, Match } from "@opencode/schema/filesystem" import { makeLocationNode } from "@opencode/util/effect/app-node" import { collectStream, waitForAbort } from "@opencode/util/process" diff --git a/packages/core/src/ripgrep/binary.ts b/packages/core/src/ripgrep/binary.ts index 4ca73e8bb8da..2b013128b3a9 100644 --- a/packages/core/src/ripgrep/binary.ts +++ b/packages/core/src/ripgrep/binary.ts @@ -1,8 +1,8 @@ import path from "path" import { Context, Effect, Layer, Stream } from "effect" -import { HttpClient, HttpClientRequest } from "effect/unstable/http" -import { ChildProcess } from "effect/unstable/process" -import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" +import { HttpClient, HttpClientRequest } from "effect/http" +import { ChildProcess } from "effect/process" +import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner" import { CrossSpawnSpawner } from "@opencode/util/cross-spawn-spawner" import { makeGlobalNode } from "@opencode/util/effect/app-node" import { httpClient } from "@opencode/util/effect/app-node-platform" diff --git a/packages/core/src/session/model-request.ts b/packages/core/src/session/model-request.ts index 5e25326bf7b8..29d810149e13 100644 --- a/packages/core/src/session/model-request.ts +++ b/packages/core/src/session/model-request.ts @@ -24,7 +24,7 @@ import type { Agent } from "@opencode/schema/agent" import type { Model } from "@opencode/schema/model" import type { Content } from "@opencode/schema/tool" import { Cause, Context, Effect, Layer, Result, Stream } from "effect" -import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClientRequest, HttpClientResponse } from "effect/http" import { makeLocationNode } from "@opencode/util/effect/app-node" import { App } from "../app.js" import { Permission } from "../permission.js" diff --git a/packages/core/src/session/model-transport.ts b/packages/core/src/session/model-transport.ts index b42b07dc9c26..29ca97246d5c 100644 --- a/packages/core/src/session/model-transport.ts +++ b/packages/core/src/session/model-transport.ts @@ -13,8 +13,8 @@ import { import { AIError, AIErrorReason, TransportError, type TransportOperation } from "@opencode/ai" import { Hash } from "@opencode/util/hash" import { Cause, Clock, Context, Duration, Effect, Fiber, Layer, Metric, Queue, Scope, Semaphore, Stream } from "effect" -import { Headers } from "effect/unstable/http" -import { Socket } from "effect/unstable/socket" +import { Headers } from "effect/http" +import { Socket } from "effect/socket" import { makeGlobalNode } from "@opencode/util/effect/app-node" import { SessionSchema } from "./schema.js" import { webSocketConstructor } from "../effect/app-node-platform.js" diff --git a/packages/core/src/shell.ts b/packages/core/src/shell.ts index 393275517141..6953692dfbad 100644 --- a/packages/core/src/shell.ts +++ b/packages/core/src/shell.ts @@ -2,7 +2,7 @@ export * as Shell from "./shell.js" import path from "path" import { Context, Deferred, Duration, Effect, Fiber, Latch, Layer, Schema, Schedule, Stream } from "effect" -import { ChildProcess } from "effect/unstable/process" +import { ChildProcess } from "effect/process" import { produce } from "immer" import { Shell } from "@opencode/schema/shell" import { AppProcess } from "@opencode/util/process" diff --git a/packages/core/src/skill/discovery.ts b/packages/core/src/skill/discovery.ts index f3173184cecf..2d2dbe70c938 100644 --- a/packages/core/src/skill/discovery.ts +++ b/packages/core/src/skill/discovery.ts @@ -2,7 +2,7 @@ export * as SkillDiscovery from "./discovery.js" import path from "path" import { Context, Effect, Layer, Schedule, Schema } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import { FSUtil } from "@opencode/util/fs-util" import { Global } from "@opencode/util/global" import { makeGlobalNode } from "@opencode/util/effect/app-node" diff --git a/packages/core/src/tool/http-body.ts b/packages/core/src/tool/http-body.ts index 7cb534a444fc..beaad3851d66 100644 --- a/packages/core/src/tool/http-body.ts +++ b/packages/core/src/tool/http-body.ts @@ -1,5 +1,5 @@ import { Effect, Stream } from "effect" -import { HttpClientResponse } from "effect/unstable/http" +import { HttpClientResponse } from "effect/http" export const collectBoundedResponseBody = ( response: HttpClientResponse.HttpClientResponse, diff --git a/packages/core/src/tool/plugin/webfetch.ts b/packages/core/src/tool/plugin/webfetch.ts index ec445e469b54..7c40d5b5106c 100644 --- a/packages/core/src/tool/plugin/webfetch.ts +++ b/packages/core/src/tool/plugin/webfetch.ts @@ -3,7 +3,7 @@ export * as WebFetchTool from "./webfetch.js" import type { Context } from "@opencode/plugin/effect/plugin" import { ToolFailure } from "@opencode/ai" import { Duration, Effect, Schema } from "effect" -import { HttpClient, type HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, type HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http" import { Parser } from "htmlparser2" import { Permission } from "../../permission.js" import { convertHTMLToMarkdown, MAX_MARKDOWN_BYTES } from "../html-markdown.js" diff --git a/packages/core/src/tool/plugin/websearch.ts b/packages/core/src/tool/plugin/websearch.ts index 0338dfdb601e..e25084b0a406 100644 --- a/packages/core/src/tool/plugin/websearch.ts +++ b/packages/core/src/tool/plugin/websearch.ts @@ -4,7 +4,7 @@ import type { Context } from "@opencode/plugin/effect/plugin" import type { SessionHooks } from "@opencode/plugin/effect/session" import { ToolFailure } from "@opencode/ai" import { Effect, Schema, Semaphore } from "effect" -import { HttpClientError } from "effect/unstable/http" +import { HttpClientError } from "effect/http" import { Form } from "../../form.js" import { Permission } from "../../permission.js" import { WebSearch } from "../../websearch.js" diff --git a/packages/core/src/tool/runtime.ts b/packages/core/src/tool/runtime.ts index f21c67a533ed..d1c5e29caf15 100644 --- a/packages/core/src/tool/runtime.ts +++ b/packages/core/src/tool/runtime.ts @@ -1,7 +1,8 @@ import type { ToolDefinition } from "@opencode/ai" +import { emptyInputJsonSchema } from "@opencode/ai/tool" import { Tool } from "@opencode/schema/tool" import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec" -import { Cache, Effect, JsonSchema, Schema, SchemaIssue, SchemaRepresentation } from "effect" +import { Cache, Effect, JsonPointer, JsonSchema, Schema, SchemaIssue, SchemaRepresentation } from "effect" import { $ZodType, toJSONSchema } from "zod/v4/core" const formatEffectIssues = SchemaIssue.makeFormatterStandardSchemaV1() @@ -155,7 +156,8 @@ const validateStandard = ( const inputJsonSchema = (schema: Tool.ValueSchema): JsonSchema.JsonSchema => { if (schema === undefined || schema === null) return {} if (isStandardSchema(schema)) return standardJsonSchema(schema, "input") - return Schema.isSchema(schema) ? toJsonSchema(schema) : schema + if (!Schema.isSchema(schema)) return schema + return emptyInputJsonSchema(schema) ?? toJsonSchema(schema) } const outputJsonSchema = (schema: Tool.ValueSchema): JsonSchema.JsonSchema => { @@ -170,7 +172,7 @@ const standardJsonSchema = (schema: StandardSchemaV1, io: "input" | "o } const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => { - const document = Schema.toJsonSchemaDocument(schema) + const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: "error" }) // Effect emits valid JSON Schema that some inference providers handle poorly. Simplify it // without changing validation: `{ type: "integer", allOf: [{ minimum: 0 }] }` becomes // `{ type: "integer", minimum: 0 }` only when no keyword would be overwritten. Named schemas @@ -220,8 +222,8 @@ const inlineLocalReferences = ( const localDefinitions = definitions ?? (isRecord(value.$defs) ? value.$defs : undefined) if (typeof value.$ref === "string" && localDefinitions) { - const segment = value.$ref.match(/^#\/\$defs\/([^/]+)$/)?.[1] - const name = segment?.replaceAll("~1", "/").replaceAll("~0", "~") + const tokens = JsonPointer.parseUriFragment(value.$ref) + const name = tokens?.length === 2 && tokens[0] === "$defs" ? tokens[1] : undefined if (name && !seen.has(name)) { const target = localDefinitions[name] if (target) { diff --git a/packages/core/src/v1/config/agent.ts b/packages/core/src/v1/config/agent.ts index f8e959cfa212..b3f600c54953 100644 --- a/packages/core/src/v1/config/agent.ts +++ b/packages/core/src/v1/config/agent.ts @@ -18,7 +18,7 @@ const AgentSchema = Schema.StructWithRest( temperature: Schema.optional(Schema.Finite), top_p: Schema.optional(Schema.Finite), prompt: Schema.optional(Schema.String), - tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({ + tools: Schema.optional(ConfigPermissionV1.Entries(Schema.Boolean)).annotate({ description: "@deprecated Use 'permission' field instead", }), disable: Schema.optional(Schema.Boolean), @@ -48,23 +48,20 @@ const normalize = (agent: Schema.Schema.Type): Schema.Schema if (!KNOWN_KEYS.has(key)) options[key] = value } - const permission: ConfigPermissionV1.Info = {} - for (const [tool, enabled] of Object.entries(agent.tools ?? {})) { + // A Map keeps each key's first position when a later entry overrides it, like Object.assign. + const permission = new Map() + for (const [tool, enabled] of agent.tools ?? []) { const action = enabled ? "allow" : "deny" - if (tool === "write" || tool === "edit" || tool === "patch") { - permission.edit = action - continue - } - permission[tool] = action + permission.set(tool === "write" || tool === "patch" ? "edit" : tool, action) } - globalThis.Object.assign(permission, agent.permission) + for (const [key, rule] of agent.permission ?? []) permission.set(key, rule) const steps = agent.steps ?? agent.maxSteps - return { ...agent, options, permission, ...(steps !== undefined ? { steps } : {}) } + return { ...agent, options, permission: [...permission], ...(steps !== undefined ? { steps } : {}) } } export const Info = AgentSchema.pipe( - Schema.decodeTo(AgentSchema, { + Schema.decodeTo(Schema.toType(AgentSchema), { decode: SchemaGetter.transform(normalize), encode: SchemaGetter.passthrough({ strict: false }), }), diff --git a/packages/core/src/v1/config/migrate.ts b/packages/core/src/v1/config/migrate.ts index ec62425e31e0..5c645b2240ab 100644 --- a/packages/core/src/v1/config/migrate.ts +++ b/packages/core/src/v1/config/migrate.ts @@ -11,16 +11,15 @@ import { ConfigProviderOptionsV1 } from "./provider-options.js" import { Provider } from "../../provider.js" import { Model } from "../../model.js" -const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const +const decodeOptions = { errors: "all", onExcessProperty: "ignore" } as const const decodeAgent = Schema.decodeUnknownSync(Schema.fromJsonString(ConfigAgent.Info), decodeOptions) const encodeAgent = Schema.encodeSync(ConfigAgent.Info) function permissions(info?: ConfigPermissionV1.Info) { - const rules = Object.entries(info ?? {}).flatMap(([key, rule]) => { - if (!rule) return [] + const rules = (info ?? []).flatMap(([key, rule]) => { const action = normalizeAction(key) if (typeof rule === "string") return [{ action, resource: "*", effect: rule }] - return Object.entries(rule).map(([resource, effect]) => ({ action, resource, effect })) + return rule.map(([resource, effect]) => ({ action, resource, effect })) }) return rules.length ? rules : undefined } diff --git a/packages/core/src/v1/config/permission.ts b/packages/core/src/v1/config/permission.ts index 450ae5daa40f..fe761a54b040 100644 --- a/packages/core/src/v1/config/permission.ts +++ b/packages/core/src/v1/config/permission.ts @@ -5,45 +5,38 @@ import { Schema, SchemaGetter } from "effect" export const Action = Schema.Literals(["ask", "allow", "deny"]).annotate({ identifier: "PermissionActionConfig" }) export type Action = Schema.Schema.Type -export const Object = Schema.Record(Schema.String, Action).annotate({ identifier: "PermissionObjectConfig" }) +// Permission rules are last-match-wins, so the order an author writes keys in is policy. Effect +// leaves the key order of decoded objects unspecified but keeps array order, so these objects +// decode to entries read directly from the input. +export const Entries = (value: Schema.Codec) => + Schema.ObjectKeyword.check( + Schema.makeFilter((input) => !globalThis.Array.isArray(input), { expected: "an object" }), + ).pipe( + Schema.decodeTo(Schema.Array(Schema.Tuple([Schema.String, value])), { + decode: SchemaGetter.transform((input) => globalThis.Object.entries(input)), + encode: SchemaGetter.transform((entries) => globalThis.Object.fromEntries(entries)), + }), + ) + +export const Object = Entries(Action).annotate({ identifier: "PermissionObjectConfig" }) export type Object = Schema.Schema.Type export const Rule = Schema.Union([Action, Object]).annotate({ identifier: "PermissionRuleConfig" }) export type Rule = Schema.Schema.Type -// Known permission keys get explicit types in the Effect schema for generated -// docs/types. Runtime config parsing uses Effect's `propertyOrder: "original"` -// parse option so user key order is preserved for permission precedence. -const InputObject = Schema.StructWithRest( - Schema.Struct({ - read: Schema.optional(Rule), - edit: Schema.optional(Rule), - glob: Schema.optional(Rule), - grep: Schema.optional(Rule), - list: Schema.optional(Rule), - bash: Schema.optional(Rule), - task: Schema.optional(Rule), - external_directory: Schema.optional(Rule), - question: Schema.optional(Action), - webfetch: Schema.optional(Rule), - websearch: Schema.optional(Action), - lsp: Schema.optional(Rule), - doom_loop: Schema.optional(Action), - skill: Schema.optional(Rule), - }), - [Schema.Record(Schema.String, Rule)], -) +const actionOnly = new Set(["question", "websearch", "doom_loop"]) -const InputSchema = Schema.Union([Action, InputObject]) - -const normalizeInput = (input: Schema.Schema.Type): Schema.Schema.Type => - typeof input === "string" ? { "*": input } : input - -export const Info = InputSchema.pipe( - Schema.decodeTo(InputObject, { - decode: SchemaGetter.transform(normalizeInput), - encode: SchemaGetter.passthrough({ strict: false }), - }), -).annotate({ identifier: "PermissionConfig" }) -type _Info = Schema.Schema.Type -export type Info = { -readonly [K in keyof _Info]: _Info[K] } +export const Info = Schema.Union([Action, Schema.ObjectKeyword]) + .pipe( + Schema.decodeTo(Entries(Rule), { + decode: SchemaGetter.transform((input) => (typeof input === "string" ? { "*": input } : input)), + encode: SchemaGetter.passthrough({ strict: false }), + }), + ) + .check( + Schema.makeFilter((rules) => rules.every(([key, rule]) => !actionOnly.has(key) || typeof rule === "string"), { + expected: "question, websearch, and doom_loop to be ask, allow, or deny", + }), + ) + .annotate({ identifier: "PermissionConfig" }) +export type Info = Schema.Schema.Type diff --git a/packages/core/src/websearch.ts b/packages/core/src/websearch.ts index 7ec63bb7b970..dc37dd45c05b 100644 --- a/packages/core/src/websearch.ts +++ b/packages/core/src/websearch.ts @@ -4,7 +4,7 @@ import { WebSearch } from "@opencode/schema/websearch" import type { Session } from "@opencode/schema/session" import { SessionEvent } from "@opencode/schema/session-event" import { Clock, Context, Effect, Layer, Option, Schema, Stream } from "effect" -import { HttpClientError } from "effect/unstable/http" +import { HttpClientError } from "effect/http" import { makeLocationNode } from "@opencode/util/effect/app-node" import { Bus } from "./bus.js" import { KV } from "./kv.js" diff --git a/packages/core/src/wellknown.ts b/packages/core/src/wellknown.ts index 34b651296e62..27436d35a26c 100644 --- a/packages/core/src/wellknown.ts +++ b/packages/core/src/wellknown.ts @@ -2,7 +2,7 @@ export * as WellKnown from "./wellknown.js" import { Integration } from "@opencode/schema/integration" import { Context, Effect, Layer, Ref, Schema, Semaphore } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import { isDeepStrictEqual } from "node:util" import { makeGlobalNode } from "@opencode/util/effect/app-node" import { httpClient } from "@opencode/util/effect/app-node-platform" diff --git a/packages/core/src/workspace.ts b/packages/core/src/workspace.ts index daad50ceab3c..04626e74a373 100644 --- a/packages/core/src/workspace.ts +++ b/packages/core/src/workspace.ts @@ -5,7 +5,7 @@ import { makeGlobalNode } from "@opencode/util/effect/app-node" import { eq } from "drizzle-orm" import { Clock, Context, Deferred, Duration, Effect, Exit, FiberSet, Layer, Ref, Schedule, Schema, Scope } from "effect" import { systemError } from "effect/PlatformError" -import { make } from "effect/unstable/process/ChildProcessSpawner" +import { make } from "effect/process/ChildProcessSpawner" import type { EnvironmentDriver } from "./environment/driver.js" import { execDefaults } from "./environment/exec-defaults.js" import { Failed, type Files, type FilesImpl } from "./environment/files.js" diff --git a/packages/core/src/worktree.ts b/packages/core/src/worktree.ts index d6d2685e8cfb..12a390adcd08 100644 --- a/packages/core/src/worktree.ts +++ b/packages/core/src/worktree.ts @@ -21,7 +21,7 @@ import { canonical, DirectoryUnavailableError } from "./worktree/directory.js" import type { EffectDrizzleSqlite } from "./database/drizzle.js" import { ProjectTable } from "./project/sql.js" import { AppProcess } from "@opencode/util/process" -import { ChildProcess } from "effect/unstable/process" +import { ChildProcess } from "effect/process" import { WorktreeStrategies } from "./worktree/strategies.js" export type { Strategy, Editor } from "./worktree/strategies.js" diff --git a/packages/core/test/aisdk.test.ts b/packages/core/test/aisdk.test.ts index b88a8eaaf257..c422716a8201 100644 --- a/packages/core/test/aisdk.test.ts +++ b/packages/core/test/aisdk.test.ts @@ -25,7 +25,7 @@ import { LLMClient, RequestExecutor } from "@opencode/ai/route" import { compileRequest } from "@opencode/ai/route/client" import { expect } from "bun:test" import { Effect, Layer } from "effect" -import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClientRequest, HttpClientResponse } from "effect/http" import { testEffect } from "./lib/effect" const it = testEffect(AISDK.locationLayer) diff --git a/packages/core/test/codemode.test.ts b/packages/core/test/codemode.test.ts index 59a26dbb08a9..dda8eb982cf5 100644 --- a/packages/core/test/codemode.test.ts +++ b/packages/core/test/codemode.test.ts @@ -4,8 +4,13 @@ import { Location } from "@opencode/core/location" import { AbsolutePath } from "@opencode/core/schema" import { Tool } from "@opencode/core/tool" import { Effect, Schema } from "effect" +import type { Info } from "@opencode/schema/tool" import { it } from "./lib/effect" +const toolNode = AppNodeBuilder.build(Tool.node, [ + Location.node.replace(Location.boundNode({ directory: AbsolutePath.make("/project") })), +]) + describe("CodeMode", () => { it.effect("owns registrations, execute, and catalog materialization", () => Effect.gen(function* () { @@ -41,13 +46,50 @@ describe("CodeMode", () => { }, ], }) - }).pipe( - Effect.scoped, - Effect.provide( - AppNodeBuilder.build(Tool.node, [ - Location.node.replace(Location.boundNode({ directory: AbsolutePath.make("/project") })), - ]), - ), - ), + }).pipe(Effect.scoped, Effect.provide(toolNode)), + ) + + it.effect("renders Effect numbers in built-in tool signatures as number", () => + Effect.gen(function* () { + const tools = yield* Tool.Service + const ref = Schema.String.annotate({ identifier: "Ref", description: "Element ref" }) + const sentinel = Schema.Literals(["NaN", "Infinity", "-Infinity"]) + const Measure = { + name: "measure", + description: "Measure", + input: Schema.Struct({ ref, amount: Schema.Number, sentinel }), + output: Schema.Struct({ exit: Schema.optionalKey(Schema.Number), sentinel }), + execute: () => Effect.succeed({ output: { exit: 0, sentinel: "NaN" as const } }), + } satisfies Info + yield* tools.transform((editor) => { + editor.add(Measure) + editor.add({ ...Measure, name: "direct", options: { codemode: false } }) + }) + + const snapshot = yield* tools.snapshot() + expect(snapshot.codeModeCatalog?.tools).toEqual([ + { + type: "tool", + name: "measure", + description: "Measure", + signature: [ + "tools.measure({", + " /** Element ref */", + " ref: string,", + " amount: number,", + ' sentinel: "NaN" | "Infinity" | "-Infinity",', + "}): Promise<{", + " exit?: number,", + ' sentinel: "NaN" | "Infinity" | "-Infinity",', + "}>", + ].join("\n"), + pinned: false, + }, + ]) + // Provider definitions keep Effect's plain JSON Schema encoding. + const direct = JSON.stringify(snapshot.definitions.find((tool) => tool.name === "direct")) + expect(direct).toContain('{"anyOf":[{"type":"number"},{"type":"string","enum":["Infinity","-Infinity","NaN"]}]}') + expect(direct).not.toContain("$ref") + }).pipe(Effect.scoped, Effect.provide(toolNode)), ) }) diff --git a/packages/core/test/config/agent.test.ts b/packages/core/test/config/agent.test.ts index a0389f481e28..7cd1758b3ecf 100644 --- a/packages/core/test/config/agent.test.ts +++ b/packages/core/test/config/agent.test.ts @@ -119,6 +119,15 @@ permissions: } } + it.live("keeps Markdown legacy permission precedence in source order", () => + Effect.gen(function* () { + const agent = yield* loadMarkdownAgent('permission:\n "*": allow\n bash: ask\n edit: deny') + expect(Permission.evaluate("shell", "ls", agent.permissions).effect).toBe("ask") + expect(Permission.evaluate("edit", "example.txt", agent.permissions).effect).toBe("deny") + expect(Permission.evaluate("webfetch", "*", agent.permissions).effect).toBe("allow") + }), + ) + for (const variant of [undefined, "high"]) { it.live(`loads Markdown legacy temperature ${variant ? "with" : "without"} a separate variant`, () => Effect.gen(function* () { diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 64baf92cc7a6..832e69d791e1 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -1,8 +1,7 @@ import path from "path" import fs from "fs/promises" import { describe, expect, test } from "bun:test" -import { Effect, Fiber, Layer, Logger, Schema, Stream } from "effect" -import { FastCheck } from "effect/testing" +import { Arbitrary, Effect, Fiber, Layer, Logger, Schema, Stream } from "effect" import { Config } from "@opencode/core/config" import { Directory, Document, type Entry, Event, Info } from "@opencode/schema/config" import { ConfigModel } from "@opencode/schema/config/model" @@ -12,6 +11,7 @@ import { makeGlobalNode } from "@opencode/util/effect/app-node" import { LayerNode } from "@opencode/util/effect/layer-node" import { Credential } from "@opencode/core/credential" import { ConfigV1 } from "../fixture/v1-config/config" +import { ConfigPermissionV1 } from "@opencode/core/v1/config/permission" import { ConfigNormalize } from "@opencode/core/config/normalize" import { Watcher } from "@opencode/core/filesystem/watcher" import { Bus } from "@opencode/core/bus" @@ -30,11 +30,7 @@ import { testEffect } from "../lib/effect" const it = testEffect(Layer.empty) const selection = Schema.decodeUnknownSync(ConfigModel.Selection) -const decodeInfo = Schema.decodeUnknownSync(Info, { - errors: "all", - onExcessProperty: "ignore", - propertyOrder: "original", -}) +const decodeInfo = Schema.decodeUnknownSync(Info, { errors: "all", onExcessProperty: "ignore" }) const encodeInfo = Schema.encodeSync(Info) function migrateV1(input: unknown) { @@ -785,14 +781,59 @@ describe("Config", () => { }) test("migrates arbitrary v1 configuration into valid v2 configuration", () => { - FastCheck.assert( - FastCheck.property(Schema.toArbitrary(ConfigV1.Info)(FastCheck), (info) => { - migrateV1(JSON.parse(JSON.stringify(info))) - }), - { numRuns: 100 }, + const json = Schema.fromJsonString(Schema.toCodecJson(ConfigV1.Info)) + const result = Effect.runSync( + Arbitrary.checkEffect( + Arbitrary.schema(ConfigV1.Info), + (info) => { + migrateV1(JSON.parse(Schema.encodeUnknownSync(json)(info))) + return true + }, + { runs: 100 }, + ), ) + expect(Arbitrary.formatCheckFailure(result)).toBeUndefined() }, 30_000) + // Effect leaves decoded key order unspecified, but these maps' order is user-visible: agents cycle + // in this order, the first matching formatter wins, and providers fold aliases in sequence. + test("decodes configuration maps in source order", () => { + const keys = ["zeta", "build", "alpha", "plan", "mid"] + const map = (value: A) => Object.fromEntries(keys.map((key) => [key, value])) + const info = Schema.decodeUnknownSync(Info)( + migrateV1({ + agents: map({}), + formatter: map({ command: ["fmt"], extensions: [".ts"] }), + providers: map({}), + commands: map({ template: "run" }), + }), + { errors: "all", onExcessProperty: "ignore" }, + ) + expect(Object.keys(info.agents ?? {})).toEqual(keys) + expect(Object.keys(typeof info.formatter === "object" ? info.formatter : {})).toEqual(keys) + expect(Object.keys(info.providers ?? {})).toEqual(keys) + expect(Object.keys(info.commands ?? {})).toEqual(keys) + }) + + test("decodes v1 permissions in source order at every level", () => { + const decode = Schema.decodeUnknownSync(ConfigPermissionV1.Info) + expect(decode({ "*": "allow", custom: "deny", bash: { "git *": "ask", "*": "deny" }, edit: "deny" })).toEqual([ + ["*", "allow"], + ["custom", "deny"], + [ + "bash", + [ + ["git *", "ask"], + ["*", "deny"], + ], + ], + ["edit", "deny"], + ]) + expect(decode("ask")).toEqual([["*", "ask"]]) + for (const invalid of [["allow"], { bash: ["allow"] }, { question: { "*": "allow" } }, { read: null }]) + expect(() => decode(invalid)).toThrow() + }) + test("migrates the v1 experimental subagent depth", () => { expect(migrateV1({ experimental: { subagent_depth: 2 } }).experimental?.subagent_depth).toBe(2) }) diff --git a/packages/core/test/config/normalization.test.ts b/packages/core/test/config/normalization.test.ts index 38b45e594542..78d59cf146d0 100644 --- a/packages/core/test/config/normalization.test.ts +++ b/packages/core/test/config/normalization.test.ts @@ -1,10 +1,9 @@ import { describe, expect, test } from "bun:test" -import { Duration, Schema } from "effect" -import { FastCheck } from "effect/testing" +import { Arbitrary, Duration, Effect, Schema } from "effect" import { ConfigNormalize } from "@opencode/core/config/normalize" import { Info } from "@opencode/schema/config" -const options = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const +const options = { errors: "all", onExcessProperty: "ignore" } as const function normalized(input: unknown) { const result = ConfigNormalize.normalize(input) @@ -109,16 +108,21 @@ describe("ConfigNormalize", () => { }) test("preserves arbitrary JSON-round-tripped native configuration", () => { - FastCheck.assert( - FastCheck.property(Schema.toArbitrary(Info)(FastCheck), (info) => { - const source = JSON.parse(JSON.stringify(Schema.encodeSync(Info)(info))) - const result = normalized(source) - expect(Schema.decodeUnknownSync(Info)(result.encoded)).toEqual( - Schema.decodeUnknownSync(Info)(withoutEmptyCompatibilityContainers(source)), - ) - }), - { numRuns: 100 }, + const result = Effect.runSync( + Arbitrary.checkEffect( + Arbitrary.schema(Info), + (info) => { + const source = JSON.parse(JSON.stringify(Schema.encodeSync(Info)(info))) + const result = normalized(source) + expect(Schema.decodeUnknownSync(Info)(result.encoded)).toEqual( + Schema.decodeUnknownSync(Info)(withoutEmptyCompatibilityContainers(source)), + ) + return true + }, + { runs: 100 }, + ), ) + expect(Arbitrary.formatCheckFailure(result)).toBeUndefined() }) test("merges named maps by entry and gives valid native entries precedence", () => { @@ -291,6 +295,25 @@ describe("ConfigNormalize", () => { ]) }) + test.each(["agent", "mode"])("preserves legacy %s permission source order", (key) => { + const build = { + tools: { write: false, read: true }, + permission: { "*": "allow", bash: "ask", custom: "deny", edit: "allow" }, + } + // An agent's permission overrides its tools in place, like Object.assign. + expect(normalized({ [key]: { build } }).encoded.agents).toMatchObject({ + build: { + permissions: [ + { action: "edit", resource: "*", effect: "allow" }, + { action: "read", resource: "*", effect: "allow" }, + { action: "*", resource: "*", effect: "allow" }, + { action: "shell", resource: "*", effect: "ask" }, + { action: "custom", resource: "*", effect: "deny" }, + ], + }, + }) + }) + test("redacts permission resource keys from invalid diagnostics", () => { const result = normalized({ permission: { bash: { "curl -H Authorization:Bearer TOPSECRET *": "bogus" } }, diff --git a/packages/core/test/database-drizzle.test.ts b/packages/core/test/database-drizzle.test.ts index d2c0958dcefc..e863b7f6b588 100644 --- a/packages/core/test/database-drizzle.test.ts +++ b/packages/core/test/database-drizzle.test.ts @@ -7,8 +7,8 @@ import { SqliteClient } from "@effect/sql-sqlite-bun" import { eq, sql } from "drizzle-orm" import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" import { Cause, Effect, Tracer } from "effect" -import type { SqlClient } from "effect/unstable/sql/SqlClient" -import { isSqlError } from "effect/unstable/sql/SqlError" +import type { SqlClient } from "effect/sql/SqlClient" +import { isSqlError } from "effect/sql/SqlError" import { EffectDrizzleSqlite } from "@opencode/core/database/drizzle" import { EffectDrizzleQueryError } from "drizzle-orm/effect-core/errors" diff --git a/packages/core/test/database-migration.test.ts b/packages/core/test/database-migration.test.ts index b5c088bbd1df..41f450ea0f52 100644 --- a/packages/core/test/database-migration.test.ts +++ b/packages/core/test/database-migration.test.ts @@ -5,8 +5,8 @@ import path from "path" import { SqliteClient } from "@effect/sql-sqlite-bun" import { EffectDrizzleSqlite } from "@opencode/core/database/drizzle" import { Deferred, Effect, Fiber, Layer } from "effect" -import { Reactivity } from "effect/unstable/reactivity" -import { SqlClient, Statement } from "effect/unstable/sql" +import { Reactivity } from "effect/reactivity" +import { SqlClient, Statement } from "effect/sql" import { sql } from "drizzle-orm" import { DatabaseMigration } from "@opencode/core/database/migration" import { migrations } from "@opencode/core/database/migration.gen" diff --git a/packages/core/test/effect-app-node-platform.test.ts b/packages/core/test/effect-app-node-platform.test.ts index 380852721cf9..8c52f824190c 100644 --- a/packages/core/test/effect-app-node-platform.test.ts +++ b/packages/core/test/effect-app-node-platform.test.ts @@ -2,8 +2,8 @@ import { describe, expect, test } from "bun:test" import { WebSocketTransport } from "@opencode/ai/route" import { WebSocketConstructor } from "@opencode/core/effect/websocket-constructor" import { Effect } from "effect" -import { Headers } from "effect/unstable/http" -import { Socket } from "effect/unstable/socket" +import { Headers } from "effect/http" +import { Socket } from "effect/socket" const makeServer = (fetch: (request: Request, server: Bun.Server) => Response | undefined) => Effect.acquireRelease( diff --git a/packages/core/test/effect/cross-spawn-spawner.test.ts b/packages/core/test/effect/cross-spawn-spawner.test.ts index a7887642723e..bd265a5a7112 100644 --- a/packages/core/test/effect/cross-spawn-spawner.test.ts +++ b/packages/core/test/effect/cross-spawn-spawner.test.ts @@ -3,7 +3,7 @@ import fs from "node:fs/promises" import os from "node:os" import path from "node:path" import { Deferred, Effect, Exit, PlatformError, Stream } from "effect" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import { ChildProcess, ChildProcessSpawner } from "effect/process" import { CrossSpawnSpawner } from "@opencode/util/cross-spawn-spawner" import { LayerNode } from "@opencode/util/effect/layer-node" import { testEffect } from "../lib/effect" diff --git a/packages/core/test/environment.test.ts b/packages/core/test/environment.test.ts index a093eab9ec7c..f1a238f06028 100644 --- a/packages/core/test/environment.test.ts +++ b/packages/core/test/environment.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises" import { describe, expect } from "bun:test" import { Effect } from "effect" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import { ChildProcess, ChildProcessSpawner } from "effect/process" import { CrossSpawnSpawner } from "@opencode/util/cross-spawn-spawner" import { LayerNode } from "@opencode/util/effect/layer-node" import { EnvironmentUnavailable } from "../src/environment/unavailable" diff --git a/packages/core/test/fixture/environment.ts b/packages/core/test/fixture/environment.ts index 31c31535d0fc..272a86c7f886 100644 --- a/packages/core/test/fixture/environment.ts +++ b/packages/core/test/fixture/environment.ts @@ -2,7 +2,7 @@ import { Environment } from "@opencode/core/environment/index" import { CrossSpawnSpawner } from "@opencode/util/cross-spawn-spawner" import { LayerNode } from "@opencode/util/effect/layer-node" import { Effect, Layer } from "effect" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import { ChildProcess, ChildProcessSpawner } from "effect/process" /** * The host environment, without the workspace machinery: what a location with no `workspaceID` diff --git a/packages/core/test/fixture/v1-config/config.ts b/packages/core/test/fixture/v1-config/config.ts index b38676e8cd1d..d45f0eecd4f1 100644 --- a/packages/core/test/fixture/v1-config/config.ts +++ b/packages/core/test/fixture/v1-config/config.ts @@ -118,7 +118,7 @@ export const Info = Schema.Struct({ }), layout: Schema.optional(ConfigLayoutV1.Layout).annotate({ description: "@deprecated Always uses stretch layout." }), permission: Schema.optional(ConfigPermissionV1.Info), - tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)), + tools: Schema.optional(ConfigPermissionV1.Entries(Schema.Boolean)), attachment: Schema.optional(ConfigAttachmentV1.Info).annotate({ description: "Attachment processing configuration, including image size limits and resizing behavior", }), diff --git a/packages/core/test/generate.test.ts b/packages/core/test/generate.test.ts index 0a9728d54d56..627aeeb98365 100644 --- a/packages/core/test/generate.test.ts +++ b/packages/core/test/generate.test.ts @@ -11,7 +11,7 @@ import { ID, Info, Model, Ref } from "@opencode/core/model" import { Provider } from "@opencode/core/provider" import { Npm } from "@opencode/util/npm" import { Effect, Layer } from "effect" -import { HttpClient, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientResponse } from "effect/http" import { testEffect } from "./lib/effect" const selected = Info.make({ diff --git a/packages/core/test/lib/effect.ts b/packages/core/test/lib/effect.ts index 8cc23621b54a..8d67db19f5e9 100644 --- a/packages/core/test/lib/effect.ts +++ b/packages/core/test/lib/effect.ts @@ -1,7 +1,7 @@ import { test, type TestOptions } from "bun:test" import { Cause, Effect, Exit, Layer, type Scope } from "effect" import { TestClock, TestConsole } from "effect/testing" -import { FetchHttpClient } from "effect/unstable/http" +import { FetchHttpClient } from "effect/http" type Body = Effect.Effect | (() => Effect.Effect) diff --git a/packages/core/test/lib/websearch.ts b/packages/core/test/lib/websearch.ts index defe86af230b..3af091a3f33e 100644 --- a/packages/core/test/lib/websearch.ts +++ b/packages/core/test/lib/websearch.ts @@ -1,7 +1,7 @@ export * as TestWebSearch from "./websearch" import { Context, Deferred, Effect, Layer } from "effect" -import { HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http" import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder" import { LayerNode } from "@opencode/util/effect/layer-node" import { Bus } from "@opencode/core/bus" diff --git a/packages/core/test/mcp.test.ts b/packages/core/test/mcp.test.ts index 10972c8b0290..2549d45ec3eb 100644 --- a/packages/core/test/mcp.test.ts +++ b/packages/core/test/mcp.test.ts @@ -53,8 +53,8 @@ import { Stream, } from "effect" import { TestClock } from "effect/testing" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import { ExitCode, makeHandle, ProcessId } from "effect/unstable/process/ChildProcessSpawner" +import { ChildProcess, ChildProcessSpawner } from "effect/process" +import { ExitCode, makeHandle, ProcessId } from "effect/process/ChildProcessSpawner" import { Image } from "@opencode/core/image" import { advance, drain } from "./lib/clock" import { testEffect } from "./lib/effect" diff --git a/packages/core/test/model-resolver.test.ts b/packages/core/test/model-resolver.test.ts index 686dc4dc3344..d556c9e84e74 100644 --- a/packages/core/test/model-resolver.test.ts +++ b/packages/core/test/model-resolver.test.ts @@ -3,7 +3,7 @@ import { LLM, LanguageModel, Message } from "@opencode/ai" import { OpenAIChat } from "@opencode/ai/protocols" import { compileRequest } from "@opencode/ai/route/client" import { ConfigProvider, Effect, Layer } from "effect" -import { Headers } from "effect/unstable/http" +import { Headers } from "effect/http" import { AISDKNative } from "@opencode/core/aisdk-native" import { Credential } from "@opencode/core/credential" import { Integration } from "@opencode/core/integration" diff --git a/packages/core/test/models.test.ts b/packages/core/test/models.test.ts index d1caf7c0c2fa..da42abb7dfa8 100644 --- a/packages/core/test/models.test.ts +++ b/packages/core/test/models.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import { Money } from "@opencode/schema/money" import { Effect, Fiber, Layer, Ref, Scope, Stream } from "effect" -import { HttpClient, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientResponse } from "effect/http" import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder" import { LayerNodePlatform } from "@opencode/util/effect/app-node-platform" import { LayerNode } from "@opencode/util/effect/layer-node" diff --git a/packages/core/test/network-guard.test.ts b/packages/core/test/network-guard.test.ts index c4b8481afb76..0efc867fc9e7 100644 --- a/packages/core/test/network-guard.test.ts +++ b/packages/core/test/network-guard.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { Effect, Layer } from "effect" -import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http" +import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/http" import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder" import { LayerNodePlatform } from "@opencode/util/effect/app-node-platform" import { it, refuseNetwork } from "./lib/effect" diff --git a/packages/core/test/plugin/module.test.ts b/packages/core/test/plugin/module.test.ts index f19d8e398123..63d51ac5efe0 100644 --- a/packages/core/test/plugin/module.test.ts +++ b/packages/core/test/plugin/module.test.ts @@ -320,7 +320,6 @@ it.live("discovers exported specifiers from the resolved tree even when dist/ ex Effect.gen(function* () { const directory = yield* tmpdirScoped() const consumerDir = path.join(directory.path, "consumer") - const jitFixtureDir = path.join(directory.path, "jit-fixture") const asyncModPath = path.join(directory.path, "async-mod.ts") yield* Effect.promise(() => @@ -341,8 +340,6 @@ it.live("discovers exported specifiers from the resolved tree even when dist/ ex "consumer/node_modules/@opencode/plugin/dist/promise/index.js": "export const root = 'dist'", "consumer/node_modules/@opencode/plugin/dist/effect/index.js": "export const index = 'dist'", "consumer/node_modules/@opencode/plugin/dist/effect/plugin.js": "export const leaf = 'dist'", - "jit-fixture/SchemaJITCompiler.js": "export const enable = () => 'fn'", - "jit-fixture/enable.js": "export {}", "async-mod.ts": "await new Promise((r) => setTimeout(r, 20)); export const nonce = Math.random()", }), ) @@ -353,13 +350,17 @@ it.live("discovers exported specifiers from the resolved tree even when dist/ ex expect(discovered.get("@opencode/plugin/effect/plugin")?.replaceAll("\\", "/")).toEndWith("src/effect/plugin.ts") expect(discovered.has("@opencode/plugin/unexported")).toBe(false) - const hostDiscovered = new Map(discoverPluginRuntimeSpecifiers()) - hostDiscovered.set("effect/schema/SchemaJITCompiler", path.join(jitFixtureDir, "SchemaJITCompiler.js")) - hostDiscovered.set("effect/schema/SchemaJITCompiler/enable", path.join(jitFixtureDir, "enable.js")) + const hostDiscovered = discoverPluginRuntimeSpecifiers() + expect(hostDiscovered.has("effect/schema/SchemaJITCompiler")).toBe(true) + expect(hostDiscovered.has("effect/schema/SchemaJITCompiler/enable")).toBe(true) expect(pluginRuntimeLoaderCode("effect/Option", hostDiscovered)).toBe('() => require("effect")["Option"]') expect(pluginRuntimeLoaderCode("effect/testing", hostDiscovered)).toBe('() => require("effect/testing")') - expect(pluginRuntimeLoaderCode("effect/unstable/http/MultipartParser/HeadersParser", hostDiscovered)).toBe( - '() => require("effect/unstable/http/MultipartParser/HeadersParser")', + expect(hostDiscovered.has("effect/http/MultipartParser/HeadersParser")).toBe(true) + expect(pluginRuntimeLoaderCode("effect/http/MultipartParser/HeadersParser", hostDiscovered)).toBe( + '() => require("effect/http/MultipartParser/HeadersParser")', + ) + expect(pluginRuntimeLoaderCode("effect/schema/SchemaJITCompiler", hostDiscovered)).toBe( + '() => require("effect/schema")["SchemaJITCompiler"]', ) expect(pluginRuntimeLoaderCode("effect/schema/SchemaJITCompiler/enable", hostDiscovered)).toBe( '() => require("effect/schema/SchemaJITCompiler/enable")', diff --git a/packages/core/test/plugin/provider-chatgpt.test.ts b/packages/core/test/plugin/provider-chatgpt.test.ts index f44cdb7ca1ef..ff2bdced4afa 100644 --- a/packages/core/test/plugin/provider-chatgpt.test.ts +++ b/packages/core/test/plugin/provider-chatgpt.test.ts @@ -8,7 +8,7 @@ import { compileRequest } from "@opencode/ai/route/client" import { describe, expect } from "bun:test" // import { DateTime, Deferred, Effect, Schedule } from "effect" import { DateTime, Effect, Schedule } from "effect" -import { HttpClient, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientResponse } from "effect/http" import { exportJWK, generateKeyPair, SignJWT } from "jose" import { App } from "@opencode/core/app" import { Credential } from "@opencode/core/credential" diff --git a/packages/core/test/plugin/provider-digitalocean.test.ts b/packages/core/test/plugin/provider-digitalocean.test.ts index c62517606996..5386fa6a9eb3 100644 --- a/packages/core/test/plugin/provider-digitalocean.test.ts +++ b/packages/core/test/plugin/provider-digitalocean.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import { Clock, Effect, Schedule } from "effect" import { TestClock } from "effect/testing" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import { Credential } from "@opencode/core/credential" import { Integration } from "@opencode/core/integration" import { Model } from "@opencode/core/model" diff --git a/packages/core/test/plugin/provider-gitlab-oauth.test.ts b/packages/core/test/plugin/provider-gitlab-oauth.test.ts index 6699a077f49e..7abed9c5ba3e 100644 --- a/packages/core/test/plugin/provider-gitlab-oauth.test.ts +++ b/packages/core/test/plugin/provider-gitlab-oauth.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Clock, Effect, Schedule } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import { Credential } from "@opencode/core/credential" import { Integration } from "@opencode/core/integration" import { Plugin } from "@opencode/core/plugin" diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index 152099b000c0..a5be7ad5e5a0 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -5,7 +5,7 @@ import { ConfigPolicy } from "@opencode/schema/config/policy" import { Money } from "@opencode/schema/money" import { Effect, Layer, Stream } from "effect" import { TestClock } from "effect/testing" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import { Agent } from "@opencode/core/agent" import { App } from "@opencode/core/app" import { Config } from "@opencode/core/config" diff --git a/packages/core/test/plugin/provider-poe.test.ts b/packages/core/test/plugin/provider-poe.test.ts index c7afed50f19e..bc53389c390d 100644 --- a/packages/core/test/plugin/provider-poe.test.ts +++ b/packages/core/test/plugin/provider-poe.test.ts @@ -12,7 +12,7 @@ import { Provider } from "@opencode/core/provider" import { expect } from "bun:test" import { Clock, Deferred, Effect, Fiber, Layer, Schedule, Stream } from "effect" import { TestClock } from "effect/testing" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" diff --git a/packages/core/test/plugin/provider-snowflake-cortex.test.ts b/packages/core/test/plugin/provider-snowflake-cortex.test.ts index 5651dcac311b..36959af196f0 100644 --- a/packages/core/test/plugin/provider-snowflake-cortex.test.ts +++ b/packages/core/test/plugin/provider-snowflake-cortex.test.ts @@ -16,8 +16,8 @@ import { SessionModelRequest } from "@opencode/core/session/model-request" import { SessionModelTransport } from "@opencode/core/session/model-transport" import { expect } from "bun:test" import { Effect, Layer, Schedule, Stream } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" -import { Socket } from "effect/unstable/socket" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" +import { Socket } from "effect/socket" import { withEnv } from "../fixture/env" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" diff --git a/packages/core/test/plugin/websearch-fixture.ts b/packages/core/test/plugin/websearch-fixture.ts index 3933adcde16e..21ed89ba9cbe 100644 --- a/packages/core/test/plugin/websearch-fixture.ts +++ b/packages/core/test/plugin/websearch-fixture.ts @@ -1,5 +1,5 @@ import { Effect, Layer } from "effect" -import { HttpClient, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientResponse } from "effect/http" import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder" import { LayerNode } from "@opencode/util/effect/layer-node" import { Credential } from "@opencode/core/credential" diff --git a/packages/core/test/process/process.test.ts b/packages/core/test/process/process.test.ts index 1ee13c4cbb76..e644aafe2760 100644 --- a/packages/core/test/process/process.test.ts +++ b/packages/core/test/process/process.test.ts @@ -5,7 +5,7 @@ import { realpathSync } from "node:fs" import { tmpdir } from "node:os" import path from "node:path" import { Effect, Exit, Fiber, Stream } from "effect" -import { ChildProcess } from "effect/unstable/process" +import { ChildProcess } from "effect/process" import { LayerNode } from "@opencode/util/effect/layer-node" import { AppProcess } from "@opencode/util/process" import { testEffect } from "../lib/effect" diff --git a/packages/core/test/session-checkpoint-transport.test.ts b/packages/core/test/session-checkpoint-transport.test.ts index 16726a4ad43d..4c23590db1ab 100644 --- a/packages/core/test/session-checkpoint-transport.test.ts +++ b/packages/core/test/session-checkpoint-transport.test.ts @@ -6,7 +6,7 @@ import { SessionModelTransport } from "../src/session/model-transport" import { WebSocketConstructor } from "../src/effect/websocket-constructor" import { Session } from "@opencode/schema/session" import { Effect, Layer, Schema } from "effect" -import { FetchHttpClient } from "effect/unstable/http" +import { FetchHttpClient } from "effect/http" import { testEffect } from "./lib/effect" import { makeWebSocketServer } from "./lib/websocket-server" diff --git a/packages/core/test/session-compaction-transport.test.ts b/packages/core/test/session-compaction-transport.test.ts index 96b01be66851..938397b5b33b 100644 --- a/packages/core/test/session-compaction-transport.test.ts +++ b/packages/core/test/session-compaction-transport.test.ts @@ -6,8 +6,8 @@ import { SessionModelTransport } from "@opencode/core/session/model-transport" import { WebSocketConstructor } from "@opencode/core/effect/websocket-constructor" import { Session } from "@opencode/schema/session" import { Effect, Latch, Layer, Schema } from "effect" -import { FetchHttpClient } from "effect/unstable/http" -import { Socket } from "effect/unstable/socket" +import { FetchHttpClient } from "effect/http" +import { Socket } from "effect/socket" import { testEffect } from "./lib/effect" const decodeBody = Schema.decodeSync(Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown))) diff --git a/packages/core/test/session-model-request-hooks.test.ts b/packages/core/test/session-model-request-hooks.test.ts index 3b7b1d45f45b..8e7612066344 100644 --- a/packages/core/test/session-model-request-hooks.test.ts +++ b/packages/core/test/session-model-request-hooks.test.ts @@ -12,7 +12,7 @@ import { SessionModelRequest } from "@opencode/core/session/model-request" import { SessionModelTransport } from "@opencode/core/session/model-transport" import { SessionRunnerModel } from "@opencode/core/session/runner/model" import { DateTime, Effect, Stream } from "effect" -import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClientRequest, HttpClientResponse } from "effect/http" import { testEffect } from "./lib/effect" import { PluginTestLayer } from "./plugin/fixture" diff --git a/packages/core/test/session-model-transport-live.test.ts b/packages/core/test/session-model-transport-live.test.ts index 3ca601baceb6..e14177e97249 100644 --- a/packages/core/test/session-model-transport-live.test.ts +++ b/packages/core/test/session-model-transport-live.test.ts @@ -12,8 +12,8 @@ import { SessionModelTransport } from "@opencode/core/session/model-transport" import { WebSocketConstructor } from "@opencode/core/effect/websocket-constructor" import { Session } from "@opencode/schema/session" import { Effect, Fiber, Layer, Stream } from "effect" -import { Headers } from "effect/unstable/http" -import { Socket } from "effect/unstable/socket" +import { Headers } from "effect/http" +import { Socket } from "effect/socket" import { makeWebSocketServer, type WebSocketServerFixture, type WebSocketServerOptions } from "./lib/websocket-server" const sessionID = Session.ID.make("ses_live_websocket") diff --git a/packages/core/test/session-model-transport.test.ts b/packages/core/test/session-model-transport.test.ts index 929079e7b3e2..391568462119 100644 --- a/packages/core/test/session-model-transport.test.ts +++ b/packages/core/test/session-model-transport.test.ts @@ -10,7 +10,7 @@ import { SessionModelTransport } from "@opencode/core/session/model-transport" import { Session } from "@opencode/schema/session" import { Cause, Deferred, Effect, Fiber, Metric, Queue, Stream } from "effect" import { TestClock } from "effect/testing" -import { Headers } from "effect/unstable/http" +import { Headers } from "effect/http" const session = Session.ID.make("ses_transport") const otherSession = Session.ID.make("ses_transport_other") diff --git a/packages/core/test/shell-retention.test.ts b/packages/core/test/shell-retention.test.ts index d0a400d8230b..36879be3d1ac 100644 --- a/packages/core/test/shell-retention.test.ts +++ b/packages/core/test/shell-retention.test.ts @@ -1,7 +1,7 @@ import { expect } from "bun:test" import { Deferred, Effect, Fiber, Layer, Queue, Scope, Sink, Stream } from "effect" -import { ChildProcessSpawner } from "effect/unstable/process" -import { ExitCode, makeHandle, ProcessId } from "effect/unstable/process/ChildProcessSpawner" +import { ChildProcessSpawner } from "effect/process" +import { ExitCode, makeHandle, ProcessId } from "effect/process/ChildProcessSpawner" import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder" import { Config } from "@opencode/core/config" import { Environment } from "@opencode/core/environment/index" diff --git a/packages/core/test/skill-discovery.test.ts b/packages/core/test/skill-discovery.test.ts index fb60bf9014c6..ce9eb980a46a 100644 --- a/packages/core/test/skill-discovery.test.ts +++ b/packages/core/test/skill-discovery.test.ts @@ -4,7 +4,7 @@ import { createServer } from "node:http" import { describe, expect } from "bun:test" import { NodeHttpServer } from "@effect/platform-node" import { Effect } from "effect" -import { HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" +import { HttpServer, HttpServerRequest, HttpServerResponse } from "effect/http" import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder" import { Global } from "@opencode/util/global" import { SkillDiscovery } from "@opencode/core/skill/discovery" diff --git a/packages/core/test/sqlite-workerd.test.ts b/packages/core/test/sqlite-workerd.test.ts index 8bfdaf3663c8..ae5ef2601144 100644 --- a/packages/core/test/sqlite-workerd.test.ts +++ b/packages/core/test/sqlite-workerd.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import { Effect, Layer } from "effect" -import { SqlClient } from "effect/unstable/sql" -import { SqlError } from "effect/unstable/sql/SqlError" +import { SqlClient } from "effect/sql" +import { SqlError } from "effect/sql/SqlError" import { sqliteLayer } from "@opencode/core/database/sqlite.workerd" import type { DurableObjectStorage } from "@opencode/core/database/sqlite.workerd" import { makeDurableObjectStorage } from "./fixture/durable-object-storage" diff --git a/packages/core/test/state-replay.test.ts b/packages/core/test/state-replay.test.ts index 299f42255f39..91e46fb2ae22 100644 --- a/packages/core/test/state-replay.test.ts +++ b/packages/core/test/state-replay.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import { State } from "@opencode/core/state" import { Effect } from "effect" -import { FastCheck } from "effect/testing" +import FastCheck from "fast-check" type Operation = { multiply: number; add: number } type Value = { value: number; order: number[] } diff --git a/packages/core/test/tool-read-filesystem.test.ts b/packages/core/test/tool-read-filesystem.test.ts index 184e0448a5f2..1410f92de873 100644 --- a/packages/core/test/tool-read-filesystem.test.ts +++ b/packages/core/test/tool-read-filesystem.test.ts @@ -9,7 +9,7 @@ import { CrossSpawnSpawner } from "@opencode/util/cross-spawn-spawner" import { LayerNodePlatform } from "@opencode/util/effect/app-node-platform" import { LayerNode } from "@opencode/util/effect/layer-node" import { Effect, FileSystem } from "effect" -import { ChildProcessSpawner } from "effect/unstable/process" +import { ChildProcessSpawner } from "effect/process" import { testEffect } from "./lib/effect" const it = testEffect(LayerNode.compile(LayerNode.group([CrossSpawnSpawner.node, LayerNodePlatform.filesystem]))) diff --git a/packages/core/test/tool-registry.test.ts b/packages/core/test/tool-registry.test.ts index ebda546cbc65..22c7c0d77a04 100644 --- a/packages/core/test/tool-registry.test.ts +++ b/packages/core/test/tool-registry.test.ts @@ -1041,7 +1041,7 @@ describe("Tool", () => { value: Schema.Boolean.pipe( Schema.decodeTo(Schema.String, { decode: SchemaGetter.transform((value) => String(value)), - encode: SchemaGetter.transformOrFail((value) => + encode: SchemaGetter.transformEffect((value) => value === "valid" ? Effect.succeed(true) : Effect.fail(new SchemaIssue.InvalidValue({ message: "invalid output" }, value)), diff --git a/packages/core/test/tool-schema.test.ts b/packages/core/test/tool-schema.test.ts index 7e90941eddeb..c70bef8df5f3 100644 --- a/packages/core/test/tool-schema.test.ts +++ b/packages/core/test/tool-schema.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "bun:test" -import { Effect, Schema } from "effect" +import { Effect, Schema, SchemaGetter } from "effect" import { z } from "zod" +import { emptyInputJsonSchema } from "@opencode/ai/tool" import { Agent } from "@opencode/schema/agent" import { Session } from "@opencode/schema/session" import { SessionMessage } from "@opencode/schema/session-message" @@ -51,7 +52,7 @@ test("Effect tool schemas use exact optional keys and flatten compatible constra description: "Constraints", input: Schema.Struct({ offset: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))), - code: Schema.String.check(Schema.isPattern(/^a/), Schema.isPattern(/z$/)), + code: Schema.String.check(Schema.isPattern(/^a/u), Schema.isPattern(/z$/u)), }), execute: () => Effect.succeed({ content: "unused" }), } @@ -67,6 +68,92 @@ test("Effect tool schemas use exact optional keys and flatten compatible constra }) }) +test.each([ + { name: "plain", input: Schema.Struct({}), metadata: {} }, + { + name: "annotated", + input: Schema.Struct({}).annotate({ identifier: "Empty Input", description: "No parameters" }), + metadata: { description: "No parameters" }, + }, +])("empty $name Effect input structs describe callable object parameters", async ({ input, metadata }) => { + const tool: Info = { + name: "empty", + description: "Empty tool", + input, + output: input, + execute: (value) => Effect.succeed({ output: value }), + } + expect(definition(tool).inputSchema).toEqual({ + ...metadata, + type: "object", + properties: {}, + additionalProperties: false, + }) + expect(definition(tool).outputSchema).toEqual({ ...metadata, not: { type: "null" } }) + expect(await Effect.runPromise(execute(tool, {}, context))).toMatchObject({ output: {} }) +}) + +test("empty input normalization preserves explicit Effect overrides and raw JSON schemas", () => { + for (const override of [{ type: "object", additionalProperties: true }, { not: { type: "null" } }]) { + const tool: Info = { + name: "explicit", + description: "Explicit schema tool", + input: override, + execute: () => Effect.succeed({ content: "unused" }), + } + expect(definition(tool).inputSchema).toEqual(override) + const checked = Schema.Struct({}).check(Schema.makeFilter(() => true, { toJsonSchema: () => override })) + expect(definition({ ...tool, output: checked }).outputSchema).toEqual( + definition({ ...tool, input: checked }).inputSchema, + ) + } +}) + +// Projection rules are covered by @opencode/ai; core must delegate to them and execute the decoded input. +test("Effect inputs delegate empty-input projection to the shared helper", async () => { + // Callers send `{}`; the handler receives a token. + const defaulted = Schema.Struct({}).pipe( + Schema.decodeTo(Schema.Struct({ token: Schema.String }), { + decode: SchemaGetter.transform(() => ({ token: "default" })), + encode: SchemaGetter.transform(() => ({})), + }), + ) + const checked = Schema.Struct({}).check(Schema.isMinProperties(1)) + const tool = (input: Schema.Codec): Info => ({ + name: "delegated", + description: "Delegated input", + input, + execute: (value) => Effect.succeed({ content: JSON.stringify(value) }), + }) + + expect(emptyInputJsonSchema(defaulted)).toEqual(definition(tool(defaulted)).inputSchema) + expect(definition(tool(defaulted)).inputSchema).toEqual({ + type: "object", + properties: {}, + additionalProperties: false, + }) + expect(emptyInputJsonSchema(checked)).toBeUndefined() + expect(definition(tool(checked)).inputSchema).toEqual({ not: { type: "null" }, minProperties: 1 }) + expect(await Effect.runPromise(execute(tool(defaulted), {}, context))).toMatchObject({ + content: [{ type: "text", text: JSON.stringify({ token: "default" }) }], + }) +}) + +test("empty input normalization leaves nested empty structs unchanged", () => { + const tool: Info = { + name: "nested", + description: "Nested empty struct", + input: Schema.Struct({ value: Schema.Struct({}) }), + execute: () => Effect.succeed({ content: "unused" }), + } + expect(definition(tool).inputSchema).toEqual({ + type: "object", + properties: { value: { not: { type: "null" } } }, + required: ["value"], + additionalProperties: false, + }) +}) + test("Effect tool schemas inline named child schemas", () => { const Child = Schema.Struct({ value: Schema.String }).annotate({ identifier: "Child" }) const tool: Info = { @@ -106,6 +193,33 @@ test("Effect tool schemas resolve escaped definition names", () => { expect(JSON.stringify(definition(tool).inputSchema)).not.toContain("$defs") }) +test.each(["Lookup Input", "café", "A/B~C% D#E"])( + "Effect tool schemas inline URI-encoded definition %s", + (identifier) => { + const child = Schema.Struct({ city: Schema.String }).annotate({ identifier }) + const tool: Info = { + name: "encoded-reference", + description: "Encoded reference", + input: child, + output: Schema.Struct({ result: child }), + execute: () => Effect.succeed({ content: "unused" }), + } + const expected = { + type: "object", + properties: { city: { type: "string" } }, + required: ["city"], + additionalProperties: false, + } + expect(definition(tool).inputSchema).toEqual(expected) + expect(definition(tool).outputSchema).toEqual({ + type: "object", + properties: { result: expected }, + required: ["result"], + additionalProperties: false, + }) + }, +) + test("portable schemas validate and describe typed tools", async () => { const input = { "~standard": { diff --git a/packages/core/test/tool-webfetch.test.ts b/packages/core/test/tool-webfetch.test.ts index bcec31a6735e..557182723744 100644 --- a/packages/core/test/tool-webfetch.test.ts +++ b/packages/core/test/tool-webfetch.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import { Duration, Effect, Fiber, Layer, Schema } from "effect" import * as TestClock from "effect/testing/TestClock" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder" import { LayerNode } from "@opencode/util/effect/layer-node" import { LayerNodePlatform } from "@opencode/util/effect/app-node-platform" diff --git a/packages/core/test/tool-websearch.test.ts b/packages/core/test/tool-websearch.test.ts index 7e3f8e8dd723..c3f1fcf5d238 100644 --- a/packages/core/test/tool-websearch.test.ts +++ b/packages/core/test/tool-websearch.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Context, Effect, Layer } from "effect" -import type { HttpClientError } from "effect/unstable/http" +import type { HttpClientError } from "effect/http" import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder" import { LayerNode } from "@opencode/util/effect/layer-node" import { Permission } from "@opencode/core/permission" diff --git a/packages/core/test/v1-migration.test.ts b/packages/core/test/v1-migration.test.ts index f0201e6cb683..d9f5b86698db 100644 --- a/packages/core/test/v1-migration.test.ts +++ b/packages/core/test/v1-migration.test.ts @@ -13,7 +13,7 @@ import { AbsolutePath } from "@opencode/core/schema" import { Global } from "@opencode/util/global" import { Effect, Fiber, Layer, Logger, Schedule, Schema, Scope } from "effect" import { eq, sql } from "drizzle-orm" -import type { SqlClient } from "effect/unstable/sql/SqlClient" +import type { SqlClient } from "effect/sql/SqlClient" import { tmpdir } from "./fixture/tmpdir" import path from "path" diff --git a/packages/core/test/wellknown.test.ts b/packages/core/test/wellknown.test.ts index 1e49c90e8b81..0363735f427e 100644 --- a/packages/core/test/wellknown.test.ts +++ b/packages/core/test/wellknown.test.ts @@ -1,6 +1,6 @@ import { expect } from "bun:test" import { Effect, Fiber, Stream } from "effect" -import { FetchHttpClient } from "effect/unstable/http" +import { FetchHttpClient } from "effect/http" import { KV } from "@opencode/core/kv" import { LayerNode } from "@opencode/util/effect/layer-node" import { Bus } from "@opencode/core/bus" diff --git a/packages/core/test/workspace.test.ts b/packages/core/test/workspace.test.ts index 76d582752844..a09a7fa616ff 100644 --- a/packages/core/test/workspace.test.ts +++ b/packages/core/test/workspace.test.ts @@ -9,8 +9,8 @@ import { LayerNode } from "@opencode/util/effect/layer-node" import { eq } from "drizzle-orm" import { Deferred, Effect, Fiber, Sink, Stream } from "effect" import { TestClock } from "effect/testing" -import { ChildProcess } from "effect/unstable/process" -import { ExitCode, make as makeSpawner, makeHandle, ProcessId } from "effect/unstable/process/ChildProcessSpawner" +import { ChildProcess } from "effect/process" +import { ExitCode, make as makeSpawner, makeHandle, ProcessId } from "effect/process/ChildProcessSpawner" import { testEffect } from "./lib/effect" const calls: Array<{ readonly operation: string; readonly binding?: WorkspaceDriver.Binding | null }> = [] diff --git a/packages/desktop/src/main/files/attachment-picker.ts b/packages/desktop/src/main/files/attachment-picker.ts index 1419a55d4d03..99a5d1928f86 100644 --- a/packages/desktop/src/main/files/attachment-picker.ts +++ b/packages/desktop/src/main/files/attachment-picker.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto" -import { Effect, FileSystem } from "effect" +import { ByteSize, Effect, FileSystem } from "effect" import { nativeT } from "../native/translations" export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024 @@ -48,7 +48,7 @@ export function readAttachment(filePath: string, maxBytes = MAX_ATTACHMENT_BYTES const file = yield* fs.open(filePath, { flag: "r" }) const info = yield* file.stat - if (info.size > FileSystem.Size(maxBytes)) + if (info.size > ByteSize.bytes(maxBytes)) throw new Error(nativeT("desktop.picker.error.sizeLimit", { limit: MAX_ATTACHMENT_BYTES / 1024 / 1024 })) const bytes = new Uint8Array(Number(info.size)) diff --git a/packages/desktop/src/main/ipc-transport.test.ts b/packages/desktop/src/main/ipc-transport.test.ts index 754690389986..a4d29eb70a50 100644 --- a/packages/desktop/src/main/ipc-transport.test.ts +++ b/packages/desktop/src/main/ipc-transport.test.ts @@ -3,8 +3,8 @@ import { EventEmitter } from "node:events" import { MessageChannel } from "node:worker_threads" import type { MessagePortMain, WebContents } from "electron" import { Effect, Layer, ManagedRuntime, Predicate, Schema, Stream } from "effect" -import { Rpc, RpcGroup, RpcMessage, RpcServer } from "effect/unstable/rpc" -import { Transferable } from "effect/unstable/workers" +import { Rpc, RpcGroup, RpcMessage, RpcServer } from "effect/rpc" +import { Transferable } from "effect/workers" import { omitUndefined } from "../shared/ipc-transport" import { FilesOpenFilePicker } from "../shared/ipc-rpc/files" import { IpcPortHandoff, IpcServerProtocolLive } from "./ipc-transport" diff --git a/packages/desktop/src/main/ipc-transport.ts b/packages/desktop/src/main/ipc-transport.ts index b9e3bf3864c1..3287d2a99f1b 100644 --- a/packages/desktop/src/main/ipc-transport.ts +++ b/packages/desktop/src/main/ipc-transport.ts @@ -1,6 +1,6 @@ import type { MessagePortMain, WebContents } from "electron" import { Context, Effect, Layer, Option, Queue, Schema, Stream } from "effect" -import { RpcMessage, RpcServer } from "effect/unstable/rpc" +import { RpcMessage, RpcServer } from "effect/rpc" import { bindIpcEvents } from "./ipc-events" type PortBinding = { diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts index e09f6cdf0d15..824061a2af43 100644 --- a/packages/desktop/src/main/ipc.ts +++ b/packages/desktop/src/main/ipc.ts @@ -2,7 +2,7 @@ export * as Ipc from "./ipc" import { app, BrowserWindow, MessageChannelMain } from "electron" import { Effect, Layer } from "effect" -import { RpcServer } from "effect/unstable/rpc" +import { RpcServer } from "effect/rpc" import { DesktopRpcs } from "../shared/ipc-rpc" import { DragCancelEvent, IpcTransportPort } from "../shared/ipc-transport" import { Extensions } from "./extension" diff --git a/packages/desktop/src/renderer/ipc-client.ts b/packages/desktop/src/renderer/ipc-client.ts index d6cb754b0963..f7c10b34eac9 100644 --- a/packages/desktop/src/renderer/ipc-client.ts +++ b/packages/desktop/src/renderer/ipc-client.ts @@ -1,5 +1,5 @@ import type { Effect } from "effect" -import type { RpcMessage } from "effect/unstable/rpc" +import type { RpcMessage } from "effect/rpc" import type { DesktopRpcClient } from "../shared/ipc-rpc" import type { DesktopEvent } from "../shared/ipc-rpc/events" import { IpcTransportPort, omitUndefined } from "../shared/ipc-transport" diff --git a/packages/desktop/src/shared/ipc-rpc.ts b/packages/desktop/src/shared/ipc-rpc.ts index 5475342e97bf..b10551419017 100644 --- a/packages/desktop/src/shared/ipc-rpc.ts +++ b/packages/desktop/src/shared/ipc-rpc.ts @@ -1,4 +1,4 @@ -import { RpcClient, RpcClientError } from "effect/unstable/rpc" +import { RpcClient, RpcClientError } from "effect/rpc" import { AppRpcs } from "./ipc-rpc/app" import { EventRpcs } from "./ipc-rpc/events" import { ExtensionRpcs } from "./ipc-rpc/extensions" diff --git a/packages/desktop/src/shared/ipc-rpc/app.ts b/packages/desktop/src/shared/ipc-rpc/app.ts index ae1946841a4c..3e6de686cb1b 100644 --- a/packages/desktop/src/shared/ipc-rpc/app.ts +++ b/packages/desktop/src/shared/ipc-rpc/app.ts @@ -1,5 +1,5 @@ import { Schema } from "effect" -import { Rpc, RpcGroup } from "effect/unstable/rpc" +import { Rpc, RpcGroup } from "effect/rpc" const ServerReadyData = Schema.Struct({ url: Schema.String, diff --git a/packages/desktop/src/shared/ipc-rpc/events.ts b/packages/desktop/src/shared/ipc-rpc/events.ts index 3a1547d29210..2114695c3f98 100644 --- a/packages/desktop/src/shared/ipc-rpc/events.ts +++ b/packages/desktop/src/shared/ipc-rpc/events.ts @@ -1,5 +1,5 @@ import { Schema } from "effect" -import { Rpc, RpcGroup } from "effect/unstable/rpc" +import { Rpc, RpcGroup } from "effect/rpc" import { ExtensionInstalled, ExtensionMenubarItem, ExtensionPayload } from "./extensions" export class DeepLinksOpened extends Schema.TaggedClass()("DeepLinksOpened", { diff --git a/packages/desktop/src/shared/ipc-rpc/extensions.ts b/packages/desktop/src/shared/ipc-rpc/extensions.ts index ef131e6e317f..9c882ba13960 100644 --- a/packages/desktop/src/shared/ipc-rpc/extensions.ts +++ b/packages/desktop/src/shared/ipc-rpc/extensions.ts @@ -1,6 +1,6 @@ import { Schema } from "effect" -import { Rpc, RpcGroup } from "effect/unstable/rpc" -import { Transferable } from "effect/unstable/workers" +import { Rpc, RpcGroup } from "effect/rpc" +import { Transferable } from "effect/workers" const text = (maximum: number) => Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maximum)) diff --git a/packages/desktop/src/shared/ipc-rpc/files.ts b/packages/desktop/src/shared/ipc-rpc/files.ts index 51e826cff238..5d934b1ac24d 100644 --- a/packages/desktop/src/shared/ipc-rpc/files.ts +++ b/packages/desktop/src/shared/ipc-rpc/files.ts @@ -1,6 +1,6 @@ import { Schema } from "effect" -import { Rpc, RpcGroup } from "effect/unstable/rpc" -import { Transferable } from "effect/unstable/workers" +import { Rpc, RpcGroup } from "effect/rpc" +import { Transferable } from "effect/workers" const OptionalString = Schema.optional(Schema.String) diff --git a/packages/desktop/src/shared/ipc-rpc/menu.ts b/packages/desktop/src/shared/ipc-rpc/menu.ts index a83abbaef18d..9e0a0e0adecb 100644 --- a/packages/desktop/src/shared/ipc-rpc/menu.ts +++ b/packages/desktop/src/shared/ipc-rpc/menu.ts @@ -1,5 +1,5 @@ import { Schema } from "effect" -import { Rpc, RpcGroup } from "effect/unstable/rpc" +import { Rpc, RpcGroup } from "effect/rpc" const DesktopMenuAction = Schema.Literals([ "app.installCli", diff --git a/packages/desktop/src/shared/ipc-rpc/storage.ts b/packages/desktop/src/shared/ipc-rpc/storage.ts index b3438f8d93da..69299cba8fa0 100644 --- a/packages/desktop/src/shared/ipc-rpc/storage.ts +++ b/packages/desktop/src/shared/ipc-rpc/storage.ts @@ -1,6 +1,6 @@ import { Schema } from "effect" -import { Rpc, RpcGroup } from "effect/unstable/rpc" -import { Transferable } from "effect/unstable/workers" +import { Rpc, RpcGroup } from "effect/rpc" +import { Transferable } from "effect/workers" export const StorageItems = Rpc.make("StorageItems", { payload: { name: Schema.String }, diff --git a/packages/desktop/src/shared/ipc-rpc/window.ts b/packages/desktop/src/shared/ipc-rpc/window.ts index e8aca73d1a45..91bdd38b8ae9 100644 --- a/packages/desktop/src/shared/ipc-rpc/window.ts +++ b/packages/desktop/src/shared/ipc-rpc/window.ts @@ -1,5 +1,5 @@ import { Schema } from "effect" -import { Rpc, RpcGroup } from "effect/unstable/rpc" +import { Rpc, RpcGroup } from "effect/rpc" export const WindowThemeReady = Rpc.make("WindowThemeReady") diff --git a/packages/gui-extensions/src/browser/ipc.ts b/packages/gui-extensions/src/browser/ipc.ts index 73a0d5e38b9b..00e74407cc3f 100644 --- a/packages/gui-extensions/src/browser/ipc.ts +++ b/packages/gui-extensions/src/browser/ipc.ts @@ -47,7 +47,7 @@ export const PaneEvent = Schema.Union([ Schema.Struct({ type: Schema.Literal("page"), tabID: Browser.TabID, - icon: Schema.optionalKey(detail(MAX_ICON_URL).check(Schema.isStartsWith("data:image/"))), + icon: Schema.optionalKey(detail(MAX_ICON_URL).check(Schema.isStartingWith("data:image/"))), zoom: Schema.Finite.check(Schema.isBetween({ minimum: 0.25, maximum: 5 })), }), // The page asked for the address field, with the platform's address shortcut. @@ -67,7 +67,7 @@ export const BrowserPane = Ipc.define({ input: Schema.Struct({ binding, server: text(16_384), - session: text(256).check(Schema.isStartsWith("ses")), + session: text(256).check(Schema.isStartingWith("ses")), restore: Schema.optionalKey(Browser.State), }), }, diff --git a/packages/gui-extensions/src/browser/network.ts b/packages/gui-extensions/src/browser/network.ts index aa21479d6b42..82cb66eee2cf 100644 --- a/packages/gui-extensions/src/browser/network.ts +++ b/packages/gui-extensions/src/browser/network.ts @@ -3,7 +3,8 @@ import type { RpcClient } from "@opencode/client/effect/api" import type { Session } from "@opencode/schema/session" import { Browser } from "@opencode/plugin-browser/rpc" import { BrowserProxy } from "@opencode/plugin-browser/proxy" -import { Effect, Encoding } from "effect" +import { Effect } from "effect" +import { Base64 } from "effect/encoding" export type BrowserNetwork = Effect.Success> @@ -24,10 +25,7 @@ export const createBrowserNetwork = Effect.fn("BrowserNetwork.create")(function* Effect.runPromise(input.rpc["tunnel.read"]({ ...input.attachment, tunnelID }, options), { signal }), write: (tunnelID, data, end, signal) => Effect.runPromise( - input.rpc["tunnel.write"]( - { ...input.attachment, tunnelID, data: Encoding.encodeBase64(data), end }, - options, - ), + input.rpc["tunnel.write"]({ ...input.attachment, tunnelID, data: Base64.encode(data), end }, options), { signal }, ), close: (tunnelID) => diff --git a/packages/gui-extensions/src/browser/pane.ts b/packages/gui-extensions/src/browser/pane.ts index 8b1ddb18be53..91de480703e7 100644 --- a/packages/gui-extensions/src/browser/pane.ts +++ b/packages/gui-extensions/src/browser/pane.ts @@ -4,7 +4,7 @@ import { OpenCode } from "@opencode/client/effect" import { SessionID } from "@opencode/schema/session-id" import electron, { type BrowserWindow } from "electron" import { Deferred, Effect, ManagedRuntime, Queue, Schedule, Schema, Stream } from "effect" -import { HttpClient, HttpClientRequest } from "effect/unstable/http" +import { HttpClient, HttpClientRequest } from "effect/http" import type { Embeds, Persisted, ServerEndpoints, Storage, Windows } from "../sdk/main" import { createBrowserPage, type BrowserPage, type Shared } from "./chromium" import { browserFailure } from "./errors" diff --git a/packages/gui-extensions/src/ssh/askpass.test.ts b/packages/gui-extensions/src/ssh/askpass.test.ts index 312803385be3..4923495cab62 100644 --- a/packages/gui-extensions/src/ssh/askpass.test.ts +++ b/packages/gui-extensions/src/ssh/askpass.test.ts @@ -1,6 +1,8 @@ import { expect } from "bun:test" import { NodeSocket } from "@effect/platform-node" import { Deferred, Effect, Fiber, Layer, Queue, Scope, Exit } from "effect" +import net from "node:net" +import { Socket } from "effect/socket" import { testEffect } from "../../../core/test/lib/effect" import { createAskpass } from "./askpass" @@ -12,19 +14,14 @@ const request = Effect.fn("test.askpass.request")(function* ( confirm = false, ) { const socket = yield* NodeSocket.makeNet({ host: "127.0.0.1", port: Number(env.OPENCODE_SSH_ASKPASS_PORT) }) - const write = yield* socket.writer + const writer = yield* socket.writer const result = { text: "" } - yield* Effect.all( - [ - socket - .runString((text) => { - result.text += text - }) - .pipe(Effect.ignore), - write(JSON.stringify({ token: env.OPENCODE_SSH_ASKPASS_TOKEN, text, confirm }) + "\n").pipe(Effect.ignore), - ], - { concurrency: "unbounded" }, - ) + yield* Effect.gen(function* () { + const pull = yield* Socket.readerString(socket) + yield* writer.write(JSON.stringify({ token: env.OPENCODE_SSH_ASKPASS_TOKEN, text, confirm }) + "\n") + + while (true) result.text += (yield* pull).join("") + }).pipe(Effect.ignore) return result.text }, Effect.scoped) @@ -73,3 +70,61 @@ it.live( expect(yield* Fiber.join(reply)).toBe("") }), ) + +it.live( + "oversized requests never prompt; disconnecting a helper clears its prompt", + Effect.gen(function* () { + const prompts = yield* Queue.unbounded() + const cleared = yield* Deferred.make() + + const bridge = yield* createAskpass({ + binary: "unused", + prompt: (prompt) => Queue.offer(prompts, prompt.id).pipe(Effect.asVoid), + clear: (id) => Deferred.succeed(cleared, id).pipe(Effect.asVoid), + }) + + expect(yield* request(bridge.env, "x".repeat(16_384))).toBe("") + expect(yield* Queue.size(prompts)).toBe(0) + + const helper = yield* request(bridge.env, "Password:").pipe(Effect.forkScoped) + const id = yield* Queue.take(prompts) + yield* Fiber.interrupt(helper) + expect(yield* Deferred.await(cleared)).toBe(id) + }), +) + +it.live( + "a request split inside a multi-byte character arrives intact", + Effect.gen(function* () { + const prompts = yield* Queue.unbounded() + + const bridge = yield* createAskpass({ + binary: "unused", + prompt: (prompt) => Queue.offer(prompts, prompt.text).pipe(Effect.asVoid), + clear: () => Effect.void, + }) + + const payload = Buffer.from( + JSON.stringify({ token: bridge.env.OPENCODE_SSH_ASKPASS_TOKEN, text: "Passwort für host:", confirm: false }) + + "\n", + ) + + // Split between the two bytes of "ü" so they arrive as separate reads. + const split = payload.indexOf(0xc3) + 1 + + const client = yield* Effect.acquireRelease( + Effect.callback((resume) => { + const socket = net.createConnection(Number(bridge.env.OPENCODE_SSH_ASKPASS_PORT), "127.0.0.1", () => + resume(Effect.succeed(socket)), + ) + }), + (socket) => Effect.sync(() => socket.destroy()), + ) + + client.setNoDelay(true) + client.write(payload.subarray(0, split)) + yield* Effect.sleep("50 millis") + client.write(payload.subarray(split)) + expect(yield* Queue.take(prompts)).toBe("Passwort für host:") + }), +) diff --git a/packages/gui-extensions/src/ssh/askpass.ts b/packages/gui-extensions/src/ssh/askpass.ts index 6be836909119..75db66aa7ea4 100644 --- a/packages/gui-extensions/src/ssh/askpass.ts +++ b/packages/gui-extensions/src/ssh/askpass.ts @@ -1,5 +1,6 @@ import { NodeSocketServer } from "@effect/platform-node" import { Deferred, Effect, Fiber, Predicate, Schema, Semaphore } from "effect" +import { Socket } from "effect/socket" import { randomUUID } from "node:crypto" import { SshFailure } from "./command" @@ -17,27 +18,31 @@ export const createAskpass = Effect.fn("Ssh.askpass")(function* (input: { const prompts = yield* Semaphore.make(1) const server = yield* NodeSocketServer.make({ host: "127.0.0.1", port: 0 }).pipe(Effect.mapError(SshFailure.from)) - if (!Predicate.isTagged(server.address, "TcpAddress")) return yield* Effect.fail(new SshFailure("connection")) + if (Predicate.isTagged(server.address, "UnixPathAddress")) return yield* Effect.fail(new SshFailure("connection")) const serving = yield* server .run((socket) => Effect.gen(function* () { const request = yield* Deferred.make() - const state = { buffer: "", received: false } - const reader = yield* socket - .runString((chunk) => { - if (state.received) return Effect.fail(new SshFailure("connection")) - state.buffer += chunk + // A helper sends one newline-terminated request. Oversized input, later data, or a close fails it. + const reader = yield* Effect.gen(function* () { + const pull = yield* Socket.readerBytes(socket) + // One streaming decoder, so a character split across reads decodes intact. + const decoder = new TextDecoder() + let buffer = "" - if (state.buffer.length > 16_384) return Effect.fail(new SshFailure("connection")) + while (!buffer.includes("\n")) { + for (const chunk of yield* pull) buffer += decoder.decode(chunk, { stream: true }) - if (!state.buffer.includes("\n")) return Effect.void - state.received = true + if (buffer.length > 16_384) return yield* new SshFailure("connection") + } - return Deferred.succeed(request, state.buffer.trim()) - }) - .pipe(Effect.ensuring(Deferred.fail(request, new SshFailure("connection"))), Effect.forkScoped) + yield* Deferred.succeed(request, buffer.trim()) + yield* pull + + return yield* new SshFailure("connection") + }).pipe(Effect.ensuring(Deferred.fail(request, new SshFailure("connection"))), Effect.forkScoped) const message = yield* Deferred.await(request).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Request))) @@ -56,11 +61,11 @@ export const createAskpass = Effect.fn("Ssh.askpass")(function* (input: { ) yield* input.prompt({ id, text: message.text, confirm: message.confirm }) const value = yield* Deferred.await(response) - const write = yield* socket.writer - yield* write(JSON.stringify({ value })) + const writer = yield* socket.writer + yield* writer.write(JSON.stringify({ value })) }).pipe(Effect.scoped), ) - .pipe(Effect.raceFirst(Fiber.join(reader).pipe(Effect.andThen(Effect.fail(new SshFailure("connection")))))) + .pipe(Effect.raceFirst(Fiber.join(reader))) }).pipe( Effect.scoped, Effect.timeout("5 minutes"), diff --git a/packages/gui-extensions/src/ssh/bootstrap.ts b/packages/gui-extensions/src/ssh/bootstrap.ts index 899d0feaf53b..fee6c65838d0 100644 --- a/packages/gui-extensions/src/ssh/bootstrap.ts +++ b/packages/gui-extensions/src/ssh/bootstrap.ts @@ -1,5 +1,5 @@ import { Effect, Option, Schema } from "effect" -import { HttpClient } from "effect/unstable/http" +import { HttpClient } from "effect/http" import { parseTarget, quote, runSsh, sshArgs, SshFailure } from "./command" import { RemoteCli } from "./remote-cli" diff --git a/packages/gui-extensions/src/ssh/command.ts b/packages/gui-extensions/src/ssh/command.ts index 420ccb09b680..05a1f0869217 100644 --- a/packages/gui-extensions/src/ssh/command.ts +++ b/packages/gui-extensions/src/ssh/command.ts @@ -1,5 +1,5 @@ import { Effect, PlatformError, Predicate, Schema, Stream } from "effect" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import { ChildProcess, ChildProcessSpawner } from "effect/process" import { RemoteCli } from "./remote-cli" export class SshFailure extends Schema.TaggedError()("SshFailure", { diff --git a/packages/gui-extensions/src/ssh/controller.ts b/packages/gui-extensions/src/ssh/controller.ts index 0eb05b9927cf..0cf01096ab12 100644 --- a/packages/gui-extensions/src/ssh/controller.ts +++ b/packages/gui-extensions/src/ssh/controller.ts @@ -15,8 +15,8 @@ import { Scope, Stream, } from "effect" -import { HttpClient } from "effect/unstable/http" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import { HttpClient } from "effect/http" +import { ChildProcess, ChildProcessSpawner } from "effect/process" import type { SshConfig, SshHttp, SshItem, SshStart, SshState } from "./contract" import { createAskpass } from "./askpass" import { bootstrap } from "./bootstrap" @@ -391,7 +391,7 @@ export const createSshController = Effect.fn("Ssh.controller")(function* (input: const freePort = Effect.gen(function* () { const server = yield* NodeSocketServer.make({ host: "127.0.0.1", port: 0 }) - if (!Predicate.isTagged(server.address, "TcpAddress")) return yield* Effect.fail(new SshFailure("connection")) + if (Predicate.isTagged(server.address, "UnixPathAddress")) return yield* Effect.fail(new SshFailure("connection")) return server.address.port }).pipe(Effect.scoped) diff --git a/packages/gui-extensions/src/ssh/main.ts b/packages/gui-extensions/src/ssh/main.ts index 4be32c648fa0..fbb678bcca41 100644 --- a/packages/gui-extensions/src/ssh/main.ts +++ b/packages/gui-extensions/src/ssh/main.ts @@ -1,6 +1,6 @@ import { NodeServices } from "@effect/platform-node" import { Effect, Exit, Fiber, Layer, ManagedRuntime, Scope, Stream } from "effect" -import { FetchHttpClient } from "effect/unstable/http" +import { FetchHttpClient } from "effect/http" import type { MainSetup } from "../sdk/main" import { SshFailure } from "./command" import { Ssh } from "./contract" diff --git a/packages/gui-extensions/src/ssh/remote-cli.ts b/packages/gui-extensions/src/ssh/remote-cli.ts index a59326684725..97c95023c33f 100644 --- a/packages/gui-extensions/src/ssh/remote-cli.ts +++ b/packages/gui-extensions/src/ssh/remote-cli.ts @@ -1,7 +1,7 @@ export * as RemoteCli from "./remote-cli" import { Effect, Schema } from "effect" -import { HttpClient, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientResponse } from "effect/http" export class Failure extends Schema.TaggedError()("RemoteCliFailure", { code: Schema.Literals(["platform", "version", "install"]), diff --git a/packages/http-recorder/README.md b/packages/http-recorder/README.md index 05cde80c9637..905b41876867 100644 --- a/packages/http-recorder/README.md +++ b/packages/http-recorder/README.md @@ -30,7 +30,7 @@ Effect `4.0.0-beta.107` currently contains unresolved symbols in its published d ```ts import { assert, describe, it } from "@effect/vitest" import { Effect, Schema } from "effect" -import { HttpClient, HttpClientRequest } from "effect/unstable/http" +import { HttpClient, HttpClientRequest } from "effect/http" import { HttpRecorder } from "@opencode/http-recorder" const User = Schema.Struct({ @@ -111,26 +111,18 @@ Real applications often select WebSocket URLs inside domain services. Effect rep ```ts import { NodeSocket } from "@effect/platform-node" import { it } from "@effect/vitest" -import { Deferred, Effect, Layer } from "effect" -import { Socket } from "effect/unstable/socket" +import { Effect, Layer } from "effect" +import { Socket } from "effect/socket" import { HttpRecorder } from "@opencode/http-recorder" const roundTrip = Effect.fn("Echo.roundTrip")(function* (url: string, message: string) { - const socket = yield* Socket.makeWebSocket(url, { closeCodeIsError: () => false }) - const write = yield* socket.writer - const echoed = yield* Deferred.make() - - yield* socket.runString( - (response) => { - return Deferred.succeed(echoed, response).pipe( - Effect.andThen(write(new Socket.CloseEvent(1000, "done"))), - Effect.orDie, - ) - }, - { onOpen: write(message).pipe(Effect.orDie) }, - ) - - return yield* Deferred.await(echoed) + const socket = yield* Socket.makeWebSocket(url) + const writer = yield* socket.writer + const pull = yield* Socket.readerString(socket) + yield* writer.write(message) + const [echoed] = yield* pull + yield* writer.write(new Socket.CloseEvent(1000, "done")) + return echoed }) it.effect("round trips a message", () => @@ -147,17 +139,15 @@ it.effect("round trips a message", () => The production application supplies only `NodeSocket.layerWebSocketConstructor`. The recorder appears in test wiring and observes each call to `Socket.makeWebSocket`, including URLs selected at runtime. -`socket.runString` owns the receive loop and finishes when the connection closes or fails. Its optional `onOpen` effect is the safe place to send protocols whose client speaks first. The writer is scoped because sending is valid only while a connection run is active. +Acquiring a reader with `socket.reader` or `Socket.readerString` opens the connection within the current scope, and each `pull` returns the next batch of frames. Every close, clean or not, fails `pull` with a `SocketCloseError`. Write only while a reader is open. WebSocket cassettes preserve one ordered transcript of client and server text or binary frames. Replay releases recorded server frames until it reaches a client frame, waits for the application to write the matching frame, then continues. This preserves causal ordering without reproducing network timing. Client text frames containing JSON compare canonically, so object-key order does not matter. Changed fields, extra fields, non-JSON text, and binary frames must match exactly after redaction. There is intentionally no custom WebSocket matcher in this beta. -Incoming frame handlers start in recorded order and may run concurrently, matching Effect's socket abstraction. Replay waits for all handlers before the socket run completes, but handler completion order is not guaranteed. Use Effect synchronization such as `Queue`, `Ref`, or `Deferred` instead of unsynchronized mutable state. - A constructor cassette records the URL, requested protocols, frames, and terminal close for each connection. Replay validates the URL and protocols before opening the simulated socket. Closing before every recorded frame is consumed fails the test. -Use `layerSocket` when a protocol layer already consumes one application-provided `Socket.Socket`, including non-WebSocket transports. Because that lower-level abstraction has no URL or protocols, its cassettes use the cassette name and connection order as identity. +Use `layerSocket` when a protocol layer already consumes one application-provided `Socket.Socket`, including non-WebSocket transports. Because that lower-level abstraction has no URL or protocols, its cassettes use the cassette name and connection order as identity. A connection is recorded when its reader scope ends with a normal close (`1000`) or succeeds without a socket failure; abnormal closes, failures, and interruptions record nothing. Text frames use the same JSON-field and body redaction as HTTP bodies. Binary frames are stored losslessly as base64. Client and server frame kinds must match during replay. diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index bfa9438e23b1..f6a4c7a2c40b 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -52,7 +52,7 @@ "typescript": "catalog:" }, "dependencies": { - "@effect/platform-node-shared": "4.0.0-rc.112" + "@effect/platform-node-shared": "4.0.0-rc.118" }, "peerDependencies": { "effect": "catalog:" diff --git a/packages/http-recorder/script/verify-package.ts b/packages/http-recorder/script/verify-package.ts index bc4beb540429..3b89af635d9b 100644 --- a/packages/http-recorder/script/verify-package.ts +++ b/packages/http-recorder/script/verify-package.ts @@ -27,8 +27,8 @@ export const verifyPackage = async (archive: string) => { `import { HttpRecorder } from "@opencode/http-recorder" import { NodeSocket } from "@effect/platform-node" import { Layer } from "effect" -import { HttpClient } from "effect/unstable/http" -import { Socket } from "effect/unstable/socket" +import { HttpClient } from "effect/http" +import { Socket } from "effect/socket" const options: HttpRecorder.RecorderOptions = { match: () => true, redact: { jsonFields: ["access_token"] } } const socketOptions: HttpRecorder.SocketRecorderOptions = { redact: { jsonFields: ["access_token"] } } diff --git a/packages/http-recorder/src/http/recorder.ts b/packages/http-recorder/src/http/recorder.ts index 568b101743ad..53ad8801be8e 100644 --- a/packages/http-recorder/src/http/recorder.ts +++ b/packages/http-recorder/src/http/recorder.ts @@ -1,12 +1,6 @@ import { NodeFileSystem } from "@effect/platform-node-shared" import { Deferred, Effect, Layer, Ref } from "effect" -import { - FetchHttpClient, - HttpClient, - HttpClientError, - HttpClientRequest, - HttpClientResponse, -} from "effect/unstable/http" +import { FetchHttpClient, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http" import { fileSystem, Service } from "../cassette/store.js" import type { RecorderOptions } from "../options.js" import { make, redactUrl, type Redactor } from "../redaction/redactor.js" diff --git a/packages/http-recorder/src/index.ts b/packages/http-recorder/src/index.ts index 2dcba03e6fcb..37a26873af15 100644 --- a/packages/http-recorder/src/index.ts +++ b/packages/http-recorder/src/index.ts @@ -1,6 +1,6 @@ import { Layer } from "effect" -import { HttpClient } from "effect/unstable/http" -import { Socket } from "effect/unstable/socket" +import { HttpClient } from "effect/http" +import { Socket } from "effect/socket" import { Api } from "./api.js" import { hasCassetteSync, removeCassetteSync } from "./cassette/store.js" import { layer, layerFetch } from "./http/recorder.js" diff --git a/packages/http-recorder/src/websocket/recorder.ts b/packages/http-recorder/src/websocket/recorder.ts index e1900f729f83..11a5e7b05894 100644 --- a/packages/http-recorder/src/websocket/recorder.ts +++ b/packages/http-recorder/src/websocket/recorder.ts @@ -1,6 +1,6 @@ import { NodeFileSystem } from "@effect/platform-node-shared" -import { Deferred, Effect, Exit, FiberSet, Layer, Option, Ref, Scope, Semaphore } from "effect" -import { Socket } from "effect/unstable/socket" +import { Cause, Deferred, Effect, Exit, Layer, Ref, Scope, Semaphore } from "effect" +import { Socket } from "effect/socket" import { fileSystem, type Interface, Service } from "../cassette/store.js" import type { SocketRecorderOptions } from "../options.js" import { make, type Redactor } from "../redaction/redactor.js" @@ -23,12 +23,13 @@ interface ActiveRecording { readonly eventLock: Semaphore.Semaphore readonly accepting: Ref.Ref opened: boolean + clientClosed: boolean valid: boolean } interface PendingRecordings { readonly promises: Set> readonly errors: Array - readonly sockets: Set + readonly sockets: Set } type Frame = string | Uint8Array @@ -134,46 +135,11 @@ const assertEvent = (actual: WebSocketEvent, expected: WebSocketEvent | undefine if (expected && comparable(actual, asJson) === comparable(expected, asJson)) return throw new Error(`WebSocket event ${index + 1}: expected ${safeText(expected)}, received ${safeText(actual)}`) }) -const runHandler = (handler: (value: A) => Effect.Effect | void, value: A) => - Effect.suspend(() => { - const result = handler(value) - return Effect.isEffect(result) ? Effect.asVoid(result) : Effect.void - }) -const runReplay = ( - state: ActiveReplay, - handler: (value: A) => Effect.Effect | void, - decode: (event: WebSocketEvent) => A, - onOpen: Effect.Effect | undefined, -) => - Effect.scoped( - Effect.gen(function* () { - const handlers = yield* FiberSet.make() - const run = yield* FiberSet.runtime(handlers)() - if (onOpen) yield* onOpen - const drive = Effect.gen(function* () { - while (true) { - const current = yield* Ref.get(state.progress) - const event = state.interaction.events[current.position] - if (!event) return - if (yield* Ref.get(state.closed)) - return yield* Effect.die( - new Error( - `WebSocket closed with unconsumed events: used ${current.position} of ${state.interaction.events.length}`, - ), - ) - if (event.direction === "server") { - yield* Ref.set(state.progress, { position: current.position + 1, changed: yield* Deferred.make() }) - run(runHandler(handler, decode(event))) - continue - } - yield* Deferred.await(current.changed) - } - }) - yield* drive.pipe(Effect.raceFirst(FiberSet.join(handlers))) - yield* FiberSet.awaitEmpty(handlers).pipe(Effect.raceFirst(FiberSet.join(handlers))) - }), - ) - +const isNormalClose = (error: unknown) => + Socket.isSocketError(error) && error.reason._tag === "SocketCloseError" && error.reason.code === 1000 +const isNormalCause = (cause: Cause.Cause) => + cause.reasons.length > 0 && cause.reasons.every((reason) => Cause.isFailReason(reason) && isNormalClose(reason.error)) +const isNormalExit = (exit: Exit.Exit) => Exit.isSuccess(exit) || isNormalCause(exit.cause) const makeRecordingSocket = ( upstream: Socket.Socket, cassette: Interface, @@ -185,71 +151,97 @@ const makeRecordingSocket = ( const active = yield* Ref.make(undefined) const writeLock = yield* Semaphore.make(1) return Socket.make({ - runRaw: (handler, runOptions) => - Effect.gen(function* () { - const state: ActiveRecording = { - events: [], - eventLock: yield* Semaphore.make(1), - accepting: yield* Ref.make(true), - opened: false, - valid: true, - } - const occupied = yield* Ref.modify(active, (current) => [current !== undefined, current ?? state]) - if (occupied) return yield* Effect.die("Concurrent runs of a recorded WebSocket are not supported") - yield* upstream - .runRaw( - (message) => { - if (!Ref.getUnsafe(state.accepting)) throw new Error("WebSocket received a frame after closing") - state.events.push(redactEvent(encodeEvent("server", message), redactor)) - return handler(message) - }, - { - ...runOptions, - onOpen: Effect.gen(function* () { - state.opened = true - if (runOptions?.onOpen) yield* runOptions.onOpen + reader: Effect.gen(function* () { + const state: ActiveRecording = { + events: [], + eventLock: yield* Semaphore.make(1), + accepting: yield* Ref.make(true), + opened: false, + clientClosed: false, + valid: true, + } + const occupied = yield* Ref.modify(active, (current) => [current !== undefined, current ?? state]) + if (occupied) return yield* Effect.die("Concurrent runs of a recorded WebSocket are not supported") + yield* Effect.addFinalizer((exit) => + writeLock.withPermit( + state.eventLock.withPermit( + Effect.gen(function* () { + yield* Ref.set(state.accepting, false) + yield* Ref.set(active, undefined) + // Every close fails the pull, so a consumer may end its scope with that normal-close failure. + if (!state.opened || !state.valid || !isNormalExit(exit)) return + yield* cassette + .append( + name, + { + transport: "websocket", + events: [...state.events], + }, + options.metadata, + ) + .pipe(Effect.orDie) + }), + ), + ), + ) + const reader = yield* upstream.reader + state.opened = true + return { + ...reader, + pull: reader.pull.pipe( + Effect.tap((messages) => + state.eventLock.withPermit( + Effect.sync(() => { + if (!Ref.getUnsafe(state.accepting)) throw new Error("WebSocket received a frame after closing") + state.events.push(...messages.map((message) => redactEvent(encodeEvent("server", message), redactor))) }), - }, - ) - .pipe( - Effect.onExit((exit) => - writeLock.withPermit( - state.eventLock.withPermit( - Effect.gen(function* () { - yield* Ref.set(state.accepting, false) - yield* Ref.set(active, undefined) - if (!Exit.isSuccess(exit) || !state.opened || !state.valid) return - yield* cassette - .append( - name, - { - transport: "websocket", - events: [...state.events], - }, - options.metadata, - ) - .pipe(Effect.orDie) - }), - ), - ), ), - ) - }), + ), + Effect.onError((cause) => + Effect.sync(() => { + if (!isNormalCause(cause)) state.valid = false + }), + ), + ), + } + }), writer: upstream.writer.pipe( - Effect.map( - (write) => (message) => + Effect.map((writer): Socket.Writer => { + const record = (messages: ReadonlyArray, send: Effect.Effect) => writeLock.withPermit( Effect.gen(function* () { - if (Socket.isCloseEvent(message)) return yield* write(message) const state = yield* Ref.get(active) if (!state || !(yield* Ref.get(state.accepting))) return yield* Effect.die("WebSocket writer used without an active socket run") - const event = redactEvent(encodeEvent("client", message), redactor) - yield* state.eventLock.withPermit(Effect.sync(() => state.events.push(event))) - return yield* write(message).pipe(Effect.onError(() => Effect.sync(() => (state.valid = false)))) + // Replay ends the run at the client's close, so a later frame could never replay. + if (state.clientClosed) return yield* Effect.die("WebSocket writer used after the client closed") + yield* state.eventLock.withPermit( + Effect.sync(() => + state.events.push( + ...messages.map((message) => redactEvent(encodeEvent("client", message), redactor)), + ), + ), + ) + return yield* send.pipe(Effect.onError(() => Effect.sync(() => (state.valid = false)))) }), - ), - ), + ) + return { + write: (message) => + Socket.isCloseEvent(message) + ? writeLock.withPermit( + Ref.get(active).pipe( + Effect.tap((state) => + Effect.sync(() => { + if (state) state.clientClosed = true + }), + ), + Effect.andThen(writer.write(message)), + ), + ) + : record([message], writer.write(message)), + writeAll: (messages) => record(messages, writer.writeAll(messages)), + } + }), ), }) }) @@ -265,70 +257,80 @@ const makeReplaySocket = ( const active = yield* Ref.make(undefined) const runLock = yield* Semaphore.make(1) return Socket.make({ - runRaw: (handler, runOptions) => - runLock - .withPermitsIfAvailable(1)( - Effect.gen(function* () { - const claimed = yield* replay - .claim((interaction) => - interaction ? Effect.void : Effect.die("Missing recorded WebSocket interaction"), - ) - .pipe(Effect.orDie) - const state = { - interaction: claimed.interaction, - progress: yield* Ref.make({ position: 0, changed: yield* Deferred.make() }), - writeLock: yield* Semaphore.make(1), - closed: yield* Ref.make(false), - } - yield* Ref.set(active, state) - yield* runReplay(state, handler, decodeEvent, runOptions?.onOpen).pipe( - Effect.ensuring(Ref.set(active, undefined)), - ) - }), - ) - .pipe( - Effect.flatMap( - Option.match({ - onNone: () => Effect.die("Concurrent runs of a replayed WebSocket are not supported"), - onSome: () => Effect.void, - }), - ), - ), - writer: Effect.succeed((message) => - Ref.get(active).pipe( - Effect.flatMap((state) => - state - ? state.writeLock.withPermit( - Effect.gen(function* () { - const current = yield* Ref.get(state.progress) - if (Socket.isCloseEvent(message)) { - yield* Ref.set(state.closed, true) - yield* Deferred.succeed(current.changed, undefined) - if (current.position === state.interaction.events.length) return - return yield* Effect.die( - new Error( - `WebSocket closed with unconsumed events: used ${current.position} of ${state.interaction.events.length}`, - ), + reader: Effect.gen(function* () { + if (!(yield* runLock.takeIfAvailable(1))) + return yield* Effect.die("Concurrent runs of a replayed WebSocket are not supported") + yield* Effect.addFinalizer(() => + Effect.all([Ref.set(active, undefined), runLock.release(1)], { discard: true }), + ) + const claimed = yield* replay + .claim((interaction) => (interaction ? Effect.void : Effect.die("Missing recorded WebSocket interaction"))) + .pipe(Effect.orDie) + const state = { + interaction: claimed.interaction, + progress: yield* Ref.make({ position: 0, changed: yield* Deferred.make() }), + writeLock: yield* Semaphore.make(1), + closed: yield* Ref.make(false), + } + yield* Ref.set(active, state) + const pull: Socket.Reader["pull"] = Effect.gen(function* () { + while (true) { + const current = yield* Ref.get(state.progress) + const closed = yield* Ref.get(state.closed) + const remaining = state.interaction.events.slice(current.position) + const client = remaining.findIndex((event) => event.direction === "client") + const messages = client === -1 ? remaining : remaining.slice(0, client) + if (!closed && messages.length > 0) { + yield* Ref.set(state.progress, { + position: current.position + messages.length, + changed: yield* Deferred.make(), + }) + return messages.map(decodeEvent) as [Frame, ...Array] + } + if (closed || remaining.length === 0) + return yield* Effect.fail(new Socket.SocketError({ reason: new Socket.SocketCloseError({ code: 1000 }) })) + yield* Deferred.await(current.changed) + } + }) + return { pull, upgrade: Socket.SocketUpgradeError.unsupported } + }), + writer: Effect.sync(() => { + const write: Socket.Writer["write"] = (message) => + Ref.get(active).pipe( + Effect.flatMap((state) => + state + ? state.writeLock.withPermit( + Effect.gen(function* () { + const current = yield* Ref.get(state.progress) + if (Socket.isCloseEvent(message)) { + yield* Ref.set(state.closed, true) + yield* Deferred.succeed(current.changed, undefined) + if (current.position === state.interaction.events.length) return + return yield* Effect.die( + new Error( + `WebSocket closed with unconsumed events: used ${current.position} of ${state.interaction.events.length}`, + ), + ) + } + const actual = redactEvent(encodeEvent("client", message), redactor) + yield* assertEvent( + actual, + state.interaction.events[current.position], + current.position, + options.compareClientMessagesAsJson === true, ) - } - const actual = redactEvent(encodeEvent("client", message), redactor) - yield* assertEvent( - actual, - state.interaction.events[current.position], - current.position, - options.compareClientMessagesAsJson === true, - ) - yield* Ref.set(state.progress, { - position: current.position + 1, - changed: yield* Deferred.make(), - }) - yield* Deferred.succeed(current.changed, undefined) - }), - ) - : Effect.die("WebSocket writer used without an active socket run"), - ), - ), - ), + yield* Ref.set(state.progress, { + position: current.position + 1, + changed: yield* Deferred.make(), + }) + yield* Deferred.succeed(current.changed, undefined) + }), + ) + : Effect.die("WebSocket writer used without an active socket run"), + ), + ) + return { write, writeAll: (messages) => Effect.forEach(messages, write, { discard: true }) } + }), }) }) @@ -399,13 +401,13 @@ const makeRecordingWebSocketConstructor = ( const onOpen = () => { opened = true } - const onMessage = (event: MessageEvent) => { + const onMessage = (event: Socket.WebSocketEvent) => { appendEvent("server", event.data) } const onError = () => { failed = true } - const onClose = (event: CloseEvent) => { + const onClose = (event: Socket.WebSocketEvent) => { native.removeEventListener("open", onOpen) native.removeEventListener("message", onMessage) native.removeEventListener("error", onError) @@ -421,7 +423,7 @@ const makeRecordingWebSocketConstructor = ( sequence, url: request.url, protocols: requestedProtocols, - close: { code: event.code, reason: event.reason }, + close: { code: event.code ?? 1006, reason: event.reason ?? "" }, }, events: [...events], } diff --git a/packages/http-recorder/test/http.test.ts b/packages/http-recorder/test/http.test.ts index ebba965c7df1..1387cf2f369a 100644 --- a/packages/http-recorder/test/http.test.ts +++ b/packages/http-recorder/test/http.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { Effect, Exit } from "effect" -import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http" import { existsSync } from "node:fs" import { isHttpInteraction } from "../src/cassette/model" import { HttpRecorder } from "../src" diff --git a/packages/http-recorder/test/redaction.test.ts b/packages/http-recorder/test/redaction.test.ts index 64e139b01fa5..d3a8a1be20e1 100644 --- a/packages/http-recorder/test/redaction.test.ts +++ b/packages/http-recorder/test/redaction.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { HttpBody, HttpClientRequest } from "effect/unstable/http" +import { HttpBody, HttpClientRequest } from "effect/http" import { redactedErrorRequest } from "../src/http/recorder" import { make, redactHeaders, redactUrl } from "../src/redaction/redactor" import { secretFindings } from "../src/redaction/secrets" diff --git a/packages/http-recorder/test/support.ts b/packages/http-recorder/test/support.ts index 30fa8205f220..d2589ddd3b4d 100644 --- a/packages/http-recorder/test/support.ts +++ b/packages/http-recorder/test/support.ts @@ -1,6 +1,6 @@ import { NodeFileSystem } from "@effect/platform-node-shared" import { Cause, Effect, Exit } from "effect" -import { HttpBody, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { HttpBody, HttpClient, HttpClientRequest } from "effect/http" import { mkdtempSync, readFileSync, rmSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" diff --git a/packages/http-recorder/test/websocket.test.ts b/packages/http-recorder/test/websocket.test.ts index 2856d157cee3..02e3cc1e2fc8 100644 --- a/packages/http-recorder/test/websocket.test.ts +++ b/packages/http-recorder/test/websocket.test.ts @@ -1,16 +1,54 @@ import { describe, expect, test } from "bun:test" import { Deferred, Effect, Exit, Fiber, Layer } from "effect" -import { Socket } from "effect/unstable/socket" +import { Socket } from "effect/socket" import { existsSync } from "node:fs" import { HttpRecorder } from "../src" import { layerSocketWithMode } from "../src/websocket/recorder" import { failureText, readCassette, seedCassetteDirectory, tempDirectory, withEnvironment } from "./support" const unavailableSocket = Socket.make({ - runRaw: () => Effect.die(new Error("unexpected live WebSocket run")), - writer: Effect.succeed(() => Effect.die(new Error("unexpected live WebSocket write"))), + reader: Effect.die(new Error("unexpected live WebSocket run")), + writer: Effect.succeed({ + write: () => Effect.die(new Error("unexpected live WebSocket write")), + writeAll: () => Effect.die(new Error("unexpected live WebSocket write")), + }), }) +// Reads until the socket closes, handling each frame before pulling the next batch. +const runRaw = ( + socket: Socket.Socket, + handler: (message: string | Uint8Array) => Effect.Effect | void, + onOpen?: Effect.Effect, +) => + Effect.scoped( + Effect.gen(function* () { + const reader = yield* socket.reader + if (onOpen) yield* onOpen + while (true) { + for (const message of yield* reader.pull) { + const result = handler(message) + if (Effect.isEffect(result)) yield* result + } + } + }).pipe( + Effect.catchIf( + (error) => Socket.SocketError.is(error) && error.reason._tag === "SocketCloseError", + () => Effect.void, + ), + ), + ) + +const runString = ( + socket: Socket.Socket, + handler: (message: string) => Effect.Effect | void, + onOpen?: Effect.Effect, +) => + runRaw( + socket, + (message) => handler(typeof message === "string" ? message : new TextDecoder().decode(message)), + onOpen, + ) + class EchoWebSocket extends EventTarget { readonly protocol = "" readonly extensions = "" @@ -42,23 +80,20 @@ describe("WebSocket", () => { using directory = tempDirectory("http-recorder-websocket-constructor-") const recorder = HttpRecorder.layerWebSocketConstructor("websocket/constructor-record", { directory: directory.path, - }).pipe( - Layer.provide( - Layer.succeed(Socket.WebSocketConstructor, (url) => new EchoWebSocket(url) as unknown as globalThis.WebSocket), - ), - ) + }).pipe(Layer.provide(Layer.succeed(Socket.WebSocketConstructor, (url) => new EchoWebSocket(url)))) await withEnvironment("CI", undefined, () => Effect.runPromise( Effect.gen(function* () { const socket = yield* Socket.makeWebSocket("wss://echo.example.test/one", { protocols: ["echo.v1"], - closeCodeIsError: () => false, - }) - const write = yield* socket.writer - yield* socket.runString(() => write(new Socket.CloseEvent(1000, "complete")).pipe(Effect.orDie), { - onOpen: write("hello").pipe(Effect.orDie), }) + const writer = yield* socket.writer + yield* runString( + socket, + () => writer.write(new Socket.CloseEvent(1000, "complete")).pipe(Effect.orDie), + writer.write("hello").pipe(Effect.orDie), + ) }).pipe(Effect.scoped, Effect.provide(recorder)), ), ) @@ -89,8 +124,7 @@ describe("WebSocket", () => { Layer.provide( Layer.succeed(Socket.WebSocketConstructor, (url, options) => { received = options - // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- the fixture implements the WebSocket surface used by the recorder. - return new EchoWebSocket(url) as unknown as globalThis.WebSocket + return new EchoWebSocket(url) }), ), ) @@ -99,8 +133,7 @@ describe("WebSocket", () => { Effect.gen(function* () { const constructor = yield* Socket.WebSocketConstructor const options = { headers: { authorization: "Bearer fixture" } } - // oxlint-disable-next-line no-restricted-globals -- This test intentionally passes runtime constructor options absent from the public socket type. - const socket = Reflect.apply(constructor, undefined, ["wss://echo.example.test/options", options]) + const socket = constructor("wss://echo.example.test/options", options) yield* Effect.callback((resume) => { socket.addEventListener("open", () => { socket.close() @@ -141,17 +174,15 @@ describe("WebSocket", () => { Effect.gen(function* () { const socket = yield* Socket.makeWebSocket("wss://events.example.test/workspaces/one", { protocols: ["events.v1"], - closeCodeIsError: () => false, }) - const write = yield* socket.writer + const writer = yield* socket.writer const received: string[] = [] - yield* socket.runString( + yield* runString( + socket, (message) => { received.push(message) }, - { - onOpen: write('{"type":"subscribe"}').pipe(Effect.orDie), - }, + writer.write('{"type":"subscribe"}').pipe(Effect.orDie), ) return received }).pipe(Effect.scoped, Effect.provide(recorder)), @@ -187,7 +218,7 @@ describe("WebSocket", () => { const exit = await Effect.runPromise( Effect.gen(function* () { const socket = yield* Socket.makeWebSocket("wss://events.example.test/workspaces/two") - yield* socket.runString(() => {}) + yield* runString(socket, () => {}) }).pipe(Effect.scoped, Effect.exit, Effect.provide(recorder)), ) @@ -200,23 +231,28 @@ describe("WebSocket", () => { type: "response.completed", token: "server-secret", }) + let sent = false const upstream = Socket.make({ - runRaw: (handler, options) => - Effect.gen(function* () { - if (options?.onOpen) yield* options.onOpen - const result = handler(response) - if (Effect.isEffect(result)) yield* result + reader: Effect.succeed({ + pull: Effect.suspend(() => { + if (sent) return Effect.fail(new Socket.SocketError({ reason: new Socket.SocketCloseError({ code: 1000 }) })) + sent = true + return Effect.succeed([response]) }), - writer: Effect.succeed(() => Effect.void), + upgrade: Socket.SocketUpgradeError.unsupported, + }), + writer: Effect.succeed({ write: () => Effect.void, writeAll: () => Effect.void }), }) await Effect.runPromise( Effect.gen(function* () { const socket = yield* Socket.Socket - const write = yield* socket.writer - yield* socket.runRaw(() => {}, { - onOpen: write(JSON.stringify({ type: "response.create", token: "client-secret" })).pipe(Effect.orDie), - }) + const writer = yield* socket.writer + yield* runRaw( + socket, + () => {}, + writer.write(JSON.stringify({ type: "response.create", token: "client-secret" })).pipe(Effect.orDie), + ) }).pipe( Effect.scoped, Effect.provide( @@ -250,6 +286,163 @@ describe("WebSocket", () => { }) }) + test("records a connection whose reader scope ends with the socket close", async () => { + using directory = tempDirectory("http-recorder-websocket-") + let sent = false + const upstream = Socket.make({ + reader: Effect.succeed({ + pull: Effect.suspend(() => { + if (sent) return Effect.fail(new Socket.SocketError({ reason: new Socket.SocketCloseError({ code: 1000 }) })) + sent = true + return Effect.succeed(["pong"]) + }), + upgrade: Socket.SocketUpgradeError.unsupported, + }), + writer: Effect.succeed({ write: () => Effect.void, writeAll: () => Effect.void }), + }) + + await Effect.runPromise( + Effect.gen(function* () { + const socket = yield* Socket.Socket + const writer = yield* socket.writer + // Effect's documented read loop: the close fails the pull and is handled outside the reader's scope. + yield* Effect.gen(function* () { + const reader = yield* socket.reader + yield* writer.write("ping") + yield* writer.writeAll(["batch-1", "batch-2"]) + while (true) yield* reader.pull + }).pipe( + Effect.scoped, + Effect.catchReason("SocketError", "SocketCloseError", () => Effect.void), + ) + }).pipe( + Effect.scoped, + Effect.provide( + layerSocketWithMode("websocket/close-outside-scope", { directory: directory.path, mode: "record" }).pipe( + Layer.provide(Layer.succeed(Socket.Socket, upstream)), + ), + ), + ), + ) + + expect(readCassette(`${directory.path}/websocket/close-outside-scope.json`)).toMatchObject({ + interactions: [ + { + transport: "websocket", + events: [ + { direction: "client", kind: "text", body: "ping" }, + { direction: "client", kind: "text", body: "batch-1" }, + { direction: "client", kind: "text", body: "batch-2" }, + { direction: "server", kind: "text", body: "pong" }, + ], + }, + ], + }) + }) + + test("does not record an abnormal close or a consumer failure after a normal close", async () => { + using directory = tempDirectory("http-recorder-websocket-") + const makeUpstream = (code: number) => + Socket.make({ + reader: Effect.sync(() => { + let sent = false + return { + pull: Effect.suspend(() => { + if (sent) return Effect.fail(new Socket.SocketError({ reason: new Socket.SocketCloseError({ code }) })) + sent = true + return Effect.succeed(["partial"]) + }), + upgrade: Socket.SocketUpgradeError.unsupported, + } + }), + writer: Effect.succeed({ write: () => Effect.void, writeAll: () => Effect.void }), + }) + + const abnormal = await Effect.runPromise( + Effect.gen(function* () { + const socket = yield* Socket.Socket + yield* runRaw(socket, () => {}) + }).pipe( + Effect.scoped, + Effect.exit, + Effect.provide( + layerSocketWithMode("websocket/abnormal-close", { directory: directory.path, mode: "record" }).pipe( + Layer.provide(Layer.succeed(Socket.Socket, makeUpstream(1011))), + ), + ), + ), + ) + expect(Exit.isSuccess(abnormal)).toBe(true) + expect(existsSync(`${directory.path}/websocket/abnormal-close.json`)).toBe(false) + + const consumerDefect = await Effect.runPromise( + Effect.gen(function* () { + const socket = yield* Socket.Socket + const reader = yield* socket.reader + while (true) + yield* reader.pull.pipe( + Effect.catchReason("SocketError", "SocketCloseError", () => + Effect.die(new Error("consumer failed after close")), + ), + ) + }).pipe( + Effect.scoped, + Effect.exit, + Effect.provide( + layerSocketWithMode("websocket/post-close-defect", { directory: directory.path, mode: "record" }).pipe( + Layer.provide(Layer.succeed(Socket.Socket, makeUpstream(1000))), + ), + ), + ), + ) + expect(Exit.isFailure(consumerDefect)).toBe(true) + expect(existsSync(`${directory.path}/websocket/post-close-defect.json`)).toBe(false) + }) + + test.each(["defect", "interruption", "timeout"])( + "does not record a reader %s caught inside its scope", + async (ending) => { + using directory = tempDirectory("http-recorder-websocket-") + let sent = false + const upstream = Socket.make({ + reader: Effect.succeed({ + pull: Effect.suspend(() => { + if (!sent) { + sent = true + return Effect.succeed(["partial"]) + } + if (ending === "defect") return Effect.die(new Error("reader failed")) + if (ending === "interruption") return Effect.interrupt + return Effect.never + }), + upgrade: Socket.SocketUpgradeError.unsupported, + }), + writer: Effect.succeed({ write: () => Effect.void, writeAll: () => Effect.void }), + }) + + await Effect.runPromise( + Effect.gen(function* () { + const socket = yield* Socket.Socket + const reader = yield* socket.reader + expect(yield* reader.pull).toEqual(["partial"]) + yield* reader.pull.pipe( + Effect.timeout("10 millis"), + Effect.catchCause(() => Effect.void), + ) + }).pipe( + Effect.scoped, + Effect.provide( + layerSocketWithMode("websocket/caught-ending", { directory: directory.path, mode: "record" }).pipe( + Layer.provide(Layer.succeed(Socket.Socket, upstream)), + ), + ), + ), + ) + + expect(existsSync(`${directory.path}/websocket/caught-ending.json`)).toBe(false) + }, + ) + test("WebSocket replay preserves causal frame ordering", async () => { using directory = tempDirectory("http-recorder-websocket-") await seedCassetteDirectory(directory.path, "websocket/replay", [ @@ -279,14 +472,14 @@ describe("WebSocket", () => { await Effect.runPromise( Effect.gen(function* () { const socket = yield* Socket.Socket - const write = yield* socket.writer - yield* socket.runRaw((message) => + const writer = yield* socket.writer + yield* runRaw(socket, (message) => Effect.gen(function* () { if (typeof message !== "string") return received.push(message) const event: unknown = JSON.parse(message) if (typeof event !== "object" || event === null || !("type" in event)) return - if (event.type === "session.created") yield* write('{"prompt":"hello","type":"response.create"}') + if (event.type === "session.created") yield* writer.write('{"prompt":"hello","type":"response.create"}') }), ) }).pipe( @@ -343,21 +536,21 @@ describe("WebSocket", () => { await Effect.runPromise( Effect.gen(function* () { const socket = yield* Socket.Socket - const write = yield* socket.writer - yield* socket.runString((message) => + const writer = yield* socket.writer + yield* runString(socket, (message) => Effect.gen(function* () { received.push(message) const event: unknown = JSON.parse(message) if (typeof event !== "object" || event === null) return if ("type" in event && event.type === "session.created") { - yield* write('{"prompt":"first","type":"response.create"}') + yield* writer.write('{"prompt":"first","type":"response.create"}') return } if ("id" in event && event.id === "first") { - yield* write('{"prompt":"second","type":"response.create"}') + yield* writer.write('{"prompt":"second","type":"response.create"}') return } - yield* write(new Socket.CloseEvent(1000, "done")) + yield* writer.write(new Socket.CloseEvent(1000, "done")) }), ) }).pipe( @@ -377,36 +570,6 @@ describe("WebSocket", () => { ]) }) - test("WebSocket replay runs message handlers concurrently", async () => { - using directory = tempDirectory("http-recorder-websocket-") - await seedCassetteDirectory(directory.path, "websocket/concurrent-handlers", [ - { - transport: "websocket", - events: [ - { direction: "server", kind: "text", body: "first" }, - { direction: "server", kind: "text", body: "second" }, - ], - }, - ]) - - await Effect.runPromise( - Effect.gen(function* () { - const socket = yield* Socket.Socket - const second = yield* Deferred.make() - yield* socket.runString((message) => - message === "first" ? Deferred.await(second) : Deferred.succeed(second, undefined), - ) - }).pipe( - Effect.scoped, - Effect.provide( - layerSocketWithMode("websocket/concurrent-handlers", { directory: directory.path, mode: "replay" }).pipe( - Layer.provide(Layer.succeed(Socket.Socket, unavailableSocket)), - ), - ), - ), - ) - }) - test("rejected concurrent replay does not consume the next interaction", async () => { using directory = tempDirectory("http-recorder-websocket-") await seedCassetteDirectory(directory.path, "websocket/concurrent-runs", [ @@ -420,23 +583,21 @@ describe("WebSocket", () => { const socket = yield* Socket.Socket const started = yield* Deferred.make() const release = yield* Deferred.make() - const first = yield* socket - .runString((message) => - Effect.gen(function* () { - received.push(message) - yield* Deferred.succeed(started, undefined) - yield* Deferred.await(release) - }), - ) - .pipe(Effect.forkChild) + const first = yield* runString(socket, (message) => + Effect.gen(function* () { + received.push(message) + yield* Deferred.succeed(started, undefined) + yield* Deferred.await(release) + }), + ).pipe(Effect.forkChild) yield* Deferred.await(started) - const concurrent = yield* Effect.exit(socket.runString(() => Effect.void)) + const concurrent = yield* Effect.exit(runString(socket, () => Effect.void)) expect(failureText(concurrent)).toContain("Concurrent runs") yield* Deferred.succeed(release, undefined) yield* Fiber.join(first) - yield* socket.runString((message) => Effect.sync(() => received.push(message))) + yield* runString(socket, (message) => Effect.sync(() => received.push(message))) }).pipe( Effect.scoped, Effect.provide( @@ -462,11 +623,9 @@ describe("WebSocket", () => { const exit = await Effect.runPromise( Effect.gen(function* () { const socket = yield* Socket.Socket - const write = yield* socket.writer + const writer = yield* socket.writer return yield* Effect.exit( - socket.runRaw(() => {}, { - onOpen: write(new Socket.CloseEvent(1000)).pipe(Effect.orDie), - }), + runRaw(socket, () => {}, writer.write(new Socket.CloseEvent(1000)).pipe(Effect.orDie)), ) }).pipe( Effect.scoped, @@ -486,7 +645,7 @@ describe("WebSocket", () => { const exit = await Effect.runPromise( Effect.gen(function* () { const socket = yield* Socket.Socket - return yield* Effect.exit(socket.runRaw(() => {})) + return yield* Effect.exit(runRaw(socket, () => {})) }).pipe( Effect.scoped, Effect.provide( @@ -495,8 +654,8 @@ describe("WebSocket", () => { Layer.succeed( Socket.Socket, Socket.make({ - runRaw: () => Effect.die(new Error("connection failed")), - writer: Effect.succeed(() => Effect.void), + reader: Effect.die(new Error("connection failed")), + writer: Effect.succeed({ write: () => Effect.void, writeAll: () => Effect.void }), }), ), ), @@ -509,6 +668,44 @@ describe("WebSocket", () => { expect(existsSync(`${directory.path}/websocket/failed-run.json`)).toBe(false) }) + test("recording rejects a write after the client closes, as replay does", async () => { + using directory = tempDirectory("http-recorder-websocket-") + const upstream = Socket.make({ + reader: Effect.succeed({ + pull: Effect.never, + upgrade: Socket.SocketUpgradeError.unsupported, + }), + writer: Effect.succeed({ write: () => Effect.void, writeAll: () => Effect.void }), + }) + const exit = await Effect.runPromise( + Effect.gen(function* () { + const socket = yield* Socket.Socket + const writer = yield* socket.writer + return yield* Effect.exit( + Effect.scoped( + Effect.gen(function* () { + yield* socket.reader + yield* writer.write("before-close") + yield* writer.write(new Socket.CloseEvent(1000)) + yield* writer.write("after-close") + }), + ), + ) + }).pipe( + Effect.scoped, + Effect.provide( + layerSocketWithMode("websocket/write-after-close", { directory: directory.path, mode: "record" }).pipe( + Layer.provide(Layer.succeed(Socket.Socket, upstream)), + ), + ), + ), + ) + + expect(Exit.isFailure(exit)).toBe(true) + expect(failureText(exit)).toContain("after the client closed") + expect(existsSync(`${directory.path}/websocket/write-after-close.json`)).toBe(false) + }) + test("WebSocket replay preserves binary frame kinds across reconnects", async () => { using directory = tempDirectory("http-recorder-websocket-") const interaction = { @@ -534,13 +731,14 @@ describe("WebSocket", () => { await Effect.runPromise( Effect.gen(function* () { const socket = yield* Socket.Socket - const write = yield* socket.writer - const run = socket.runRaw( + const writer = yield* socket.writer + const run = runRaw( + socket, (message) => { if (typeof message === "string") throw new Error("Expected a binary WebSocket frame") received.push([...message]) }, - { onOpen: write(new Uint8Array([1, 2])).pipe(Effect.orDie) }, + writer.write(new Uint8Array([1, 2])).pipe(Effect.orDie), ) yield* run yield* run diff --git a/packages/httpapi-codegen/src/index.ts b/packages/httpapi-codegen/src/index.ts index 266f9344cf26..e37fcc15b2ae 100644 --- a/packages/httpapi-codegen/src/index.ts +++ b/packages/httpapi-codegen/src/index.ts @@ -1,7 +1,7 @@ import { isAbsolute, join } from "node:path" import { Context, Effect, FileSystem, PlatformError, Schema, SchemaAST, SchemaRepresentation } from "effect" -import { HttpMethod, type HttpRouter } from "effect/unstable/http" -import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpMethod, type HttpRouter } from "effect/http" +import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api" import { format } from "prettier" export type InputField = { @@ -746,8 +746,8 @@ function renderImportedEffectFiles( const imports = projection === undefined ? `import { ${api} } from ${JSON.stringify(options.module)}` - : `import { HttpApi, HttpApiClient${"endpoints" in options ? ", HttpApiGroup" : ""} } from "effect/unstable/httpapi"\nimport { ${projection.imports.join(", ")} } from ${JSON.stringify(options.module)}` - const httpApiImport = projection === undefined ? 'import { HttpApiClient } from "effect/unstable/httpapi"\n' : "" + : `import { HttpApi, HttpApiClient${"endpoints" in options ? ", HttpApiGroup" : ""} } from "effect/http-api"\nimport { ${projection.imports.join(", ")} } from ${JSON.stringify(options.module)}` + const httpApiImport = projection === undefined ? 'import { HttpApiClient } from "effect/http-api"\n' : "" const shapeTypes = groups.flatMap((group) => group.endpoints.flatMap((endpoint) => [ ...(endpoint.operation.inputMode === "none" ? [] : [`${endpointTypeName(group, endpoint)}Input`]), @@ -762,7 +762,7 @@ function renderImportedEffectFiles( options.shapeModule === undefined ? "" : `const preserveEffect = () => (effect: Effect.Effect) => effect\n${usesStream ? "const preserveStream = () => (stream: Stream.Stream) => stream\n" : ""}\n` - const client = `// Generated by @opencode/httpapi-codegen. Do not edit.\nimport { Effect${usesStream ? ", Stream" : ""}, Schema } from "effect"\nimport { Sse } from "effect/unstable/encoding"\nimport { HttpClientError } from "effect/unstable/http"\n${httpApiImport}${imports}\n${shapeImport}import { ClientError } from "./client-error.js"\n\n${projection?.source ?? ""}type RawClient = HttpApiClient.ForApi\n\nconst mapClientError = (error: E) => HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) ? new ClientError({ cause: error }) : error\n\n${preserve}${adapters.join("\n\n")}\n\nconst adaptClient = (raw: RawClient) => ({ ${fields.join(", ")} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) => HttpApiClient.make(${api}, options).pipe(Effect.map(adaptClient))\n` + const client = `// Generated by @opencode/httpapi-codegen. Do not edit.\nimport { Effect${usesStream ? ", Stream" : ""}, Schema } from "effect"\nimport { Sse } from "effect/encoding"\nimport { HttpClientError } from "effect/http"\n${httpApiImport}${imports}\n${shapeImport}import { ClientError } from "./client-error.js"\n\n${projection?.source ?? ""}type RawClient = HttpApiClient.ForApi\n\nconst mapClientError = (error: E) => HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) ? new ClientError({ cause: error }) : error\n\n${preserve}${adapters.join("\n\n")}\n\nconst adaptClient = (raw: RawClient) => ({ ${fields.join(", ")} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) => HttpApiClient.make(${api}, options).pipe(Effect.map(adaptClient))\n` return [ { path: "client-error.ts", @@ -1600,7 +1600,15 @@ function assertPortable(schema: Schema.Top, path: string, portable: Map` : `HttpApiClient.Client.Group` const usesStream = group.endpoints.some((item) => item.operation.success === "stream") - return `// Generated by @opencode/httpapi-codegen. Do not edit.\nimport { Effect, Schema${usesStream ? ", Stream" : ""} } from "effect"\nimport { Sse } from "effect/unstable/encoding"\nimport { HttpClientError } from "effect/unstable/http"\nimport { HttpApiClient, HttpApiEndpoint, HttpApiGroup${usesHttpApiSchema ? ", HttpApiSchema" : ""} } from "effect/unstable/httpapi"\nimport { ClientError } from "./client-error.js"\n\n${declarations}\n\nexport const Group${name} = ${groupSource}\n\ntype RawGroup = ${rawGroup}\n\n${adapters.join("\n\n")}\n\nexport const adaptGroup${name} = (raw: RawGroup) => ({ ${methods} })\n` + return `// Generated by @opencode/httpapi-codegen. Do not edit.\nimport { Effect, Schema${usesStream ? ", Stream" : ""} } from "effect"\nimport { Sse } from "effect/encoding"\nimport { HttpClientError } from "effect/http"\nimport { HttpApiClient, HttpApiEndpoint, HttpApiGroup${usesHttpApiSchema ? ", HttpApiSchema" : ""} } from "effect/http-api"\nimport { ClientError } from "./client-error.js"\n\n${declarations}\n\nexport const Group${name} = ${groupSource}\n\ntype RawGroup = ${rawGroup}\n\n${adapters.join("\n\n")}\n\nexport const adaptGroup${name} = (raw: RawGroup) => ({ ${methods} })\n` } function renderEffectRequestPart( @@ -1972,5 +1980,5 @@ function renderClient(groups: ReadonlyArray) { const raw = `{ ${group.endpoints.map((item) => `${JSON.stringify(item.endpoint.identifier)}: raw[${JSON.stringify(item.endpoint.identifier)}]`).join(", ")} }` return [`...adaptGroup${groupTypeName(group)}(${raw})`] }) - return `// Generated by @opencode/httpapi-codegen. Do not edit.\nimport { Effect } from "effect"\nimport { HttpApi, HttpApiClient } from "effect/unstable/httpapi"\n${imports}\n\nconst Api = ${api}\nconst adaptClient = (raw: HttpApiClient.ForApi) => ({ ${fields.join(", ")} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) =>\n HttpApiClient.make(Api, options).pipe(Effect.map(adaptClient))\n` + return `// Generated by @opencode/httpapi-codegen. Do not edit.\nimport { Effect } from "effect"\nimport { HttpApi, HttpApiClient } from "effect/http-api"\n${imports}\n\nconst Api = ${api}\nconst adaptClient = (raw: HttpApiClient.ForApi) => ({ ${fields.join(", ")} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) =>\n HttpApiClient.make(Api, options).pipe(Effect.map(adaptClient))\n` } diff --git a/packages/httpapi-codegen/test/fixture.ts b/packages/httpapi-codegen/test/fixture.ts index 6210cd1422b2..d9cd879e4183 100644 --- a/packages/httpapi-codegen/test/fixture.ts +++ b/packages/httpapi-codegen/test/fixture.ts @@ -1,5 +1,5 @@ import { Schema } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi" +import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api" export class Missing extends Schema.TaggedError()("Missing", { message: Schema.String, diff --git a/packages/httpapi-codegen/test/generate.test.ts b/packages/httpapi-codegen/test/generate.test.ts index 8432988dfda5..7b2a6b356405 100644 --- a/packages/httpapi-codegen/test/generate.test.ts +++ b/packages/httpapi-codegen/test/generate.test.ts @@ -11,7 +11,7 @@ import { HttpApiMiddleware, HttpApiSchema, OpenApi, -} from "effect/unstable/httpapi" +} from "effect/http-api" import { format } from "prettier" import { compile as compileContract, @@ -1473,6 +1473,19 @@ describe("HttpApiCodegen.generate", () => { ) }) + test("emits schema classes with native arbitrary constraints structurally", () => { + class Attempt extends Schema.Class("Attempt")({ + count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), + }) {} + const output = emitPromise( + compileContract(api(HttpApiEndpoint.get("get", "/session", { success: Attempt }))), + ) + const types = output.files.find((file) => file.path === "types.ts")?.content + + expect(types).toContain('export type Attempt = { readonly "count": number }') + expect(types).toContain("export type SessionGetOutput = Attempt") + }) + test("rejects spoofed and aborted validation checks", () => { const Spoofed = Schema.Number.check( Schema.makeFilter(() => "always fails", { meta: { _tag: "isFinite" }, arbitrary: {} }), diff --git a/packages/httpapi-codegen/test/generated-consumer.ts b/packages/httpapi-codegen/test/generated-consumer.ts index 5cd4477bb4d1..f9be3591c6c3 100644 --- a/packages/httpapi-codegen/test/generated-consumer.ts +++ b/packages/httpapi-codegen/test/generated-consumer.ts @@ -1,5 +1,5 @@ import { Effect, Stream } from "effect" -import { HttpClient } from "effect/unstable/http" +import { HttpClient } from "effect/http" import { ClientError, OpenCode } from "./generated" import { Missing } from "./fixture" diff --git a/packages/httpapi-codegen/test/generated/client.ts b/packages/httpapi-codegen/test/generated/client.ts index 0e412b20ef5d..b158bc82c6b0 100644 --- a/packages/httpapi-codegen/test/generated/client.ts +++ b/packages/httpapi-codegen/test/generated/client.ts @@ -1,6 +1,6 @@ // Generated by @opencode/httpapi-codegen. Do not edit. import { Effect } from "effect" -import { HttpApi, HttpApiClient } from "effect/unstable/httpapi" +import { HttpApi, HttpApiClient } from "effect/http-api" import { adaptGroupSession, GroupSession } from "./session" import { adaptGroupEvent, GroupEvent } from "./event" import { adaptGroupSystem, GroupSystem } from "./system" diff --git a/packages/httpapi-codegen/test/generated/event.ts b/packages/httpapi-codegen/test/generated/event.ts index 1f6411dfb26a..7fe72e0bd48f 100644 --- a/packages/httpapi-codegen/test/generated/event.ts +++ b/packages/httpapi-codegen/test/generated/event.ts @@ -1,8 +1,8 @@ // Generated by @opencode/httpapi-codegen. Do not edit. import { Effect, Schema, Stream } from "effect" -import { Sse } from "effect/unstable/encoding" -import { HttpClientError } from "effect/unstable/http" -import { HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi" +import { Sse } from "effect/encoding" +import { HttpClientError } from "effect/http" +import { HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api" import { ClientError } from "./client-error.js" const EndpointSubscribeSuccessData = Schema.Struct({ type: Schema.String }) diff --git a/packages/httpapi-codegen/test/generated/session.ts b/packages/httpapi-codegen/test/generated/session.ts index 23c35b74b0a2..7f0a649472cc 100644 --- a/packages/httpapi-codegen/test/generated/session.ts +++ b/packages/httpapi-codegen/test/generated/session.ts @@ -1,8 +1,8 @@ // Generated by @opencode/httpapi-codegen. Do not edit. import { Effect, Schema } from "effect" -import { Sse } from "effect/unstable/encoding" -import { HttpClientError } from "effect/unstable/http" -import { HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" +import { Sse } from "effect/encoding" +import { HttpClientError } from "effect/http" +import { HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/http-api" import { ClientError } from "./client-error.js" const EndpointHealthSuccess = Schema.String diff --git a/packages/httpapi-codegen/test/generated/system.ts b/packages/httpapi-codegen/test/generated/system.ts index 7b85ecd6011a..50fd40f66f1a 100644 --- a/packages/httpapi-codegen/test/generated/system.ts +++ b/packages/httpapi-codegen/test/generated/system.ts @@ -1,8 +1,8 @@ // Generated by @opencode/httpapi-codegen. Do not edit. import { Effect, Schema } from "effect" -import { Sse } from "effect/unstable/encoding" -import { HttpClientError } from "effect/unstable/http" -import { HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" +import { Sse } from "effect/encoding" +import { HttpClientError } from "effect/http" +import { HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/http-api" import { ClientError } from "./client-error.js" const EndpointStatusSuccess = Schema.String diff --git a/packages/httpapi-codegen/test/write.test.ts b/packages/httpapi-codegen/test/write.test.ts index 734028b30095..734c3b48be88 100644 --- a/packages/httpapi-codegen/test/write.test.ts +++ b/packages/httpapi-codegen/test/write.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { join } from "node:path" -import { Effect, FileSystem, Option } from "effect" +import { ByteSize, Effect, FileSystem, Option } from "effect" import { write, type Output } from "../src" import { it } from "./effect" @@ -150,7 +150,7 @@ describe("HttpApiCodegen.write", () => { uid: Option.none(), gid: Option.none(), rdev: Option.none(), - size: FileSystem.Size(0), + size: ByteSize.bytes(0), blksize: Option.none(), blocks: Option.none(), }), diff --git a/packages/plugin-browser/src/rpc.ts b/packages/plugin-browser/src/rpc.ts index 25a285939c6b..aaba261a16a0 100644 --- a/packages/plugin-browser/src/rpc.ts +++ b/packages/plugin-browser/src/rpc.ts @@ -17,15 +17,15 @@ const limit = optional(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: const timeoutMs = optional(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 30_000 }))).annotate({ description: "Timeout in milliseconds, 1–30000. Default 10000.", }) -export const TabID = Schema.String.check(Schema.isPattern(/^tab_[a-f0-9-]{36}$/)) +export const TabID = Schema.String.check(Schema.isPattern(/^tab_[a-f0-9-]{36}$/u)) .pipe(Schema.brand("Browser.TabID")) .annotate({ identifier: "Browser.TabID" }) export type TabID = typeof TabID.Type -export const Ref = Schema.String.check(Schema.isPattern(/^@?e[1-9][0-9]*$/)) +export const Ref = Schema.String.check(Schema.isPattern(/^@?e[1-9][0-9]*$/u)) .pipe(Schema.brand("Browser.Ref")) .annotate({ identifier: "Browser.Ref" }) export type Ref = typeof Ref.Type -export const FileID = Schema.String.check(Schema.isPattern(/^file_[a-f0-9-]{36}$/)) +export const FileID = Schema.String.check(Schema.isPattern(/^file_[a-f0-9-]{36}$/u)) .pipe(Schema.brand("Browser.FileID")) .annotate({ identifier: "Browser.FileID" }) export type FileID = typeof FileID.Type @@ -524,7 +524,7 @@ export type Outcome = typeof Outcome.Type const attachment = { sessionID: Session.ID, connectionID: Schema.String } const request = { ...attachment, requestID: Schema.String } export const TunnelTarget = Schema.Struct({ - host: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(253), Schema.isPattern(/^[a-zA-Z0-9._:%-]+$/)), + host: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(253), Schema.isPattern(/^[a-zA-Z0-9._:%-]+$/u)), port: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65_535 })), }) export type TunnelTarget = typeof TunnelTarget.Type diff --git a/packages/plugin-browser/src/tools.ts b/packages/plugin-browser/src/tools.ts index 5ecf68a04142..7afb621efaa7 100644 --- a/packages/plugin-browser/src/tools.ts +++ b/packages/plugin-browser/src/tools.ts @@ -2,7 +2,8 @@ export * as BrowserTools from "./tools.js" import type { Context } from "@opencode/plugin/effect/plugin" import { Tool } from "@opencode/schema/tool" -import { Effect, Encoding, Result, Schema } from "effect" +import { Effect, Result, Schema } from "effect" +import { Base64 } from "effect/encoding" import type { BrowserConnection } from "./connection.js" import { BrowserFiles } from "./files.js" import { Browser } from "./rpc.js" @@ -104,7 +105,7 @@ function exportResult(output: Schema.Schema.Type, f .filter((file) => file.mime.startsWith("image/")) .map((file) => ({ type: "file" as const, - uri: `data:${file.mime};base64,${Encoding.encodeBase64(file.data)}`, + uri: `data:${file.mime};base64,${Base64.encode(file.data)}`, mime: file.mime, name: file.name, })), diff --git a/packages/plugin-browser/test/rpc.test.ts b/packages/plugin-browser/test/rpc.test.ts index 9e72fcb1f569..af1d5b53f05b 100644 --- a/packages/plugin-browser/test/rpc.test.ts +++ b/packages/plugin-browser/test/rpc.test.ts @@ -124,3 +124,13 @@ test("network RPC is bounded bytes and does not add model tools", () => { ).toThrow() expect(() => Schema.decodeUnknownSync(Browser.TunnelTarget)({ host: "localhost", port: 0 })).toThrow() }) + +test("browser ID patterns survive JSON Schema export for model tool definitions", () => { + const exported = (schema: Schema.Top) => { + const document = Schema.toJsonSchemaDocument(schema) + return Object.values(document.definitions)[0] ?? document.schema + } + expect(exported(Browser.TabID)).toMatchObject({ type: "string", pattern: "^tab_[a-f0-9-]{36}$" }) + expect(exported(Browser.Ref)).toMatchObject({ type: "string", pattern: "^@?e[1-9][0-9]*$" }) + expect(exported(Browser.FileID)).toMatchObject({ type: "string", pattern: "^file_[a-f0-9-]{36}$" }) +}) diff --git a/packages/plugin/src/promise/adapter.ts b/packages/plugin/src/promise/adapter.ts index eb41969dfd72..ff5efd107ebc 100644 --- a/packages/plugin/src/promise/adapter.ts +++ b/packages/plugin/src/promise/adapter.ts @@ -3,7 +3,7 @@ import type { Rpc } from "@opencode/schema/rpc" import type { RpcCallOptions, RpcEventPayload } from "@opencode/client/promise/api" import { Effect, Schema, SchemaAST, Stream } from "effect" import type { Scope } from "effect" -import { HttpApiEndpoint, HttpApiSchema } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiSchema } from "effect/http-api" import { define } from "../effect/plugin.js" import type { Plugin } from "./plugin.js" import type { Info } from "./tool.js" diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index 90fca5531982..1323650788f0 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -131,14 +131,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - }, - { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - ] + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" } } } @@ -215,6 +208,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Resolve the requested location or the server default location.", @@ -346,6 +349,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve currently registered agents.", @@ -439,11 +452,18 @@ } }, "404": { - "description": "AgentNotFoundError", + "description": "AgentNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AgentNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/AgentNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -533,6 +553,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve enabled server plugins and their current status.", @@ -605,14 +635,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -626,6 +649,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Check one or all package plugins for available updates.", @@ -701,14 +734,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -723,6 +749,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -919,9 +955,6 @@ { "$ref": "#/components/schemas/InvalidCursorErrorEncoded" }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, { "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } @@ -1203,14 +1236,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -1447,7 +1473,8 @@ "^ses": { "$ref": "#/components/schemas/SessionActive" } - } + }, + "additionalProperties": false } }, "required": ["data"], @@ -1642,7 +1669,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -1651,7 +1678,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -1745,14 +1772,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -1768,19 +1788,19 @@ } }, "404": { - "description": "SessionNotFoundError | MessageNotFoundError", + "description": "SessionNotFoundError | MessageNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/MessageNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -1857,7 +1877,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -1866,7 +1886,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -1936,7 +1956,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -1945,7 +1965,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -1999,14 +2019,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -2102,14 +2115,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -2125,7 +2131,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2134,7 +2140,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2266,19 +2272,19 @@ } }, "404": { - "description": "SessionNotFoundError | CommandNotFoundError", + "description": "SessionNotFoundError | CommandNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/CommandNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2389,19 +2395,19 @@ } }, "404": { - "description": "SessionNotFoundError | SkillNotFoundError", + "description": "SessionNotFoundError | SkillNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/SkillNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2495,7 +2501,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2504,7 +2510,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2628,7 +2634,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2637,7 +2643,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2732,7 +2738,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2741,7 +2747,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2838,7 +2844,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2847,7 +2853,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2924,7 +2930,7 @@ } }, "404": { - "description": "MessageNotFoundError | SessionNotFoundError", + "description": "MessageNotFoundError | SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2936,7 +2942,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3037,7 +3043,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3046,7 +3052,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3118,7 +3124,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3127,7 +3133,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3322,14 +3328,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -3345,7 +3344,7 @@ } }, "404": { - "description": "MessageNotFoundError | SessionNotFoundError", + "description": "MessageNotFoundError | SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3355,6 +3354,9 @@ }, { "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3659,7 +3661,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3668,7 +3670,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3729,7 +3731,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3738,7 +3740,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3822,7 +3824,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3831,7 +3833,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3891,7 +3893,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3900,7 +3902,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4011,7 +4013,7 @@ "$ref": "#/components/schemas/SessionLogItemEncoded" } }, - "required": ["id", "event", "data"], + "required": ["event", "data"], "additionalProperties": false }, "x-effect-stream": { @@ -4073,7 +4075,7 @@ "errorSchema": { "not": {} }, - "failureEvent": "effect/httpapi/stream/failure" + "failureEvent": "effect/http-api/stream/failure" } } } @@ -4177,7 +4179,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -4186,7 +4188,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4239,7 +4241,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -4248,7 +4250,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4402,7 +4404,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -4411,7 +4413,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4459,14 +4461,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -4482,7 +4477,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -4491,7 +4486,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4586,19 +4581,19 @@ } }, "404": { - "description": "SessionNotFoundError | FormNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/FormNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4672,19 +4667,19 @@ } }, "404": { - "description": "SessionNotFoundError | FormNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/FormNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4762,19 +4757,19 @@ } }, "404": { - "description": "SessionNotFoundError | FormNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/FormNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -5190,6 +5185,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -5290,6 +5295,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -5327,14 +5342,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -5349,6 +5357,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -5472,6 +5490,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -5574,11 +5602,18 @@ } }, "404": { - "description": "ProviderNotFoundError", + "description": "ProviderNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProviderNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -5678,6 +5713,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve available integrations and their authentication methods.", @@ -5771,11 +5816,18 @@ } }, "404": { - "description": "IntegrationNotFoundError", + "description": "IntegrationNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -5831,14 +5883,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -5852,6 +5897,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Discover and persist an experimental wellknown integration source.", @@ -5929,14 +5984,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -5952,11 +6000,18 @@ } }, "404": { - "description": "IntegrationNotFoundError", + "description": "IntegrationNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -6003,6 +6058,134 @@ } } }, + "/api/integration/{integrationID}/connect/external": { + "post": { + "tags": ["integration"], + "operationId": "integration.connect.external", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "IntegrationNotFoundError | LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] + } + } + } + } + }, + "description": "Run an external authentication method and store a reference to its credential source.", + "summary": "Connect with external credentials", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "methodID": { + "type": "string" + }, + "answer": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.Answer" + }, + { + "type": "null" + } + ] + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["methodID"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, "/api/integration/{integrationID}/connect/oauth": { "post": { "tags": ["integration"], @@ -6074,14 +6257,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -6095,6 +6271,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Start an OAuth attempt and return the authorization details.", @@ -6233,7 +6419,7 @@ } }, "404": { - "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError", + "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -6243,6 +6429,9 @@ }, { "$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -6328,6 +6517,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Cancel an OAuth attempt and release its resources.", @@ -6396,14 +6595,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -6419,7 +6611,7 @@ } }, "404": { - "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError", + "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -6429,6 +6621,9 @@ }, { "$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -6534,14 +6729,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -6557,7 +6745,7 @@ } }, "404": { - "description": "IntegrationNotFoundError | IntegrationMethodNotFoundError", + "description": "IntegrationNotFoundError | IntegrationMethodNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -6567,6 +6755,9 @@ }, { "$ref": "#/components/schemas/IntegrationMethodNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -6700,7 +6891,7 @@ } }, "404": { - "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError", + "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -6710,6 +6901,9 @@ }, { "$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -6795,6 +6989,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Cancel a command authentication attempt and terminate its process.", @@ -6881,6 +7085,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve configured MCP servers and their connection status.", @@ -6955,6 +7169,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Add an MCP server at runtime or replace an existing one, connecting it immediately.", @@ -7053,11 +7277,18 @@ } }, "404": { - "description": "McpServerNotFoundError", + "description": "McpServerNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -7137,11 +7368,18 @@ } }, "404": { - "description": "McpServerNotFoundError", + "description": "McpServerNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -7221,11 +7459,18 @@ } }, "404": { - "description": "McpServerNotFoundError", + "description": "McpServerNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -7312,6 +7557,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve resources and resource templates from connected MCP servers.", @@ -7623,6 +7878,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List known projects.", @@ -7676,11 +7941,18 @@ } }, "404": { - "description": "ProjectNotFoundError", + "description": "ProjectNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProjectNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -7795,6 +8067,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve pending forms for a location.", @@ -7881,6 +8163,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve pending permission requests for a location.", @@ -7949,6 +8241,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve saved permissions, optionally filtered by project.", @@ -7993,6 +8295,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Remove a saved permission by ID.", @@ -8065,7 +8377,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -8074,7 +8386,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -8198,11 +8510,18 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -8276,19 +8595,19 @@ } }, "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", + "description": "SessionNotFoundError | PermissionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/PermissionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -8350,19 +8669,19 @@ } }, "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", + "description": "SessionNotFoundError | PermissionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/PermissionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -8471,11 +8790,18 @@ } }, "404": { - "description": "FileNotFoundError", + "description": "FileNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FileNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/FileNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -8583,11 +8909,18 @@ } }, "404": { - "description": "FileNotFoundError", + "description": "FileNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FileNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/FileNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -8709,6 +9042,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Find recursively ranked filesystem entries relative to the requested location.", @@ -8801,6 +9144,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Write the raw request body to an absolute path or a path relative to the requested location, creating parent directories, and return the resolved absolute path. Unlike read, the target is not confined to the location. Experimental: may change without compatibility guarantees.", @@ -8898,6 +9251,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve currently registered commands.", @@ -8984,6 +9347,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve currently registered skills.", @@ -9081,6 +9454,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "500": { "description": "RpcInternalError", "content": { @@ -9137,7 +9520,7 @@ "$ref": "#/components/schemas/V2EventEncoded" } }, - "required": ["id", "event", "data"], + "required": ["event", "data"], "additionalProperties": false }, "x-effect-stream": { @@ -9199,7 +9582,7 @@ "errorSchema": { "not": {} }, - "failureEvent": "effect/httpapi/stream/failure" + "failureEvent": "effect/http-api/stream/failure" } } } @@ -9309,6 +9692,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List PTY sessions for a location, including exited sessions retained until removal.", @@ -9390,6 +9783,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Create a pseudo-terminal session for a location.", @@ -9518,11 +9921,18 @@ } }, "404": { - "description": "PtyNotFoundError", + "description": "PtyNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -9618,11 +10028,18 @@ } }, "404": { - "description": "PtyNotFoundError", + "description": "PtyNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -9732,11 +10149,18 @@ } }, "404": { - "description": "PtyNotFoundError", + "description": "PtyNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -9859,11 +10283,18 @@ } }, "404": { - "description": "PtyNotFoundError", + "description": "PtyNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -9952,11 +10383,18 @@ } }, "404": { - "description": "PtyNotFoundError", + "description": "PtyNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -10100,14 +10538,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -10172,14 +10603,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -10931,6 +11355,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List currently running shell commands for a location. Exited commands are not included.", @@ -11012,6 +11446,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Spawn one non-interactive shell command for a location. Combined stdout/stderr is captured to a file pageable via output.", @@ -11133,11 +11577,18 @@ } }, "404": { - "description": "ShellNotFoundError", + "description": "ShellNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ShellNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/ShellNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -11214,6 +11665,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Terminate and remove one shell command and its retained output.", @@ -11344,11 +11805,18 @@ } }, "404": { - "description": "ShellNotFoundError", + "description": "ShellNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ShellNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/ShellNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -11438,6 +11906,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List references available in the requested location.", @@ -11762,14 +12240,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -11784,6 +12255,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "409": { "description": "ConflictError", "content": { @@ -11896,6 +12377,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Get current and default branch information for the requested location.", @@ -11987,6 +12478,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -12082,6 +12583,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List uncommitted working-copy changes relative to the requested location.", @@ -12195,6 +12706,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List local and remote branches available at the requested location.", @@ -12313,6 +12834,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -12629,6 +13160,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -12707,14 +13248,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -12729,6 +13263,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -12834,6 +13378,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Return configuration documents and discovery sources for the requested location, from lowest to highest priority.", @@ -12879,6 +13433,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Return shells available to terminal and agent execution.", @@ -12914,6 +13478,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Patch supported fields in the highest-precedence global configuration document.", @@ -13554,7 +14128,7 @@ "properties": { "action": { "type": "string", - "enum": ["provider.use", "permission"] + "enum": ["provider.use", "tool.use", "integration.use"] }, "resource": { "type": "string" @@ -14038,7 +14612,7 @@ }, "method": { "type": "string", - "enum": ["key", "oauth"] + "enum": ["key", "oauth", "external"] }, "status": { "$ref": "#/components/schemas/Connection.Status" @@ -14139,6 +14713,23 @@ "required": ["id", "integrationID", "label", "active", "value"], "additionalProperties": false }, + "Credential.External": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["external"] + }, + "methodID": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": ["type", "methodID"], + "additionalProperties": false + }, "Credential.Key": { "type": "object", "properties": { @@ -14193,6 +14784,9 @@ }, { "$ref": "#/components/schemas/Credential.Key" + }, + { + "$ref": "#/components/schemas/Credential.External" } ] }, @@ -14457,6 +15051,18 @@ "$ref": "#/components/schemas/Form.Field" } }, + "Form.Fields_3": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/Form.Field" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Form.Field" + } + }, "Form.Info": { "type": "object", "properties": { @@ -15432,6 +16038,26 @@ "required": ["type", "names"], "additionalProperties": false }, + "Integration.ExternalMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["external"] + }, + "label": { + "type": "string" + }, + "form": { + "$ref": "#/components/schemas/Form.Fields_3" + } + }, + "required": ["id", "type", "label"], + "additionalProperties": false + }, "Integration.Info": { "type": "object", "properties": { @@ -15488,6 +16114,9 @@ { "$ref": "#/components/schemas/Integration.KeyMethod" }, + { + "$ref": "#/components/schemas/Integration.ExternalMethod" + }, { "$ref": "#/components/schemas/Integration.EnvMethod" } @@ -15654,6 +16283,23 @@ "required": ["directory"], "additionalProperties": false }, + "LocationNotFoundErrorEncoded": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["LocationNotFoundError"] + }, + "location": { + "$ref": "#/components/schemas/Location.PublicRef" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "location", "message"], + "additionalProperties": false + }, "Mcp.LocalConfigEncoded": { "type": "object", "properties": { @@ -16031,6 +16677,12 @@ }, "supportsPromptCacheKey": { "type": "boolean" + }, + "supportsThinkingBlockBinding": { + "type": "boolean" + }, + "supportsEffortUpdates": { + "type": "boolean" } }, "additionalProperties": false @@ -17567,14 +18219,9 @@ "additionalProperties": false }, "Session.Inbox.CompactionPayload": { - "anyOf": [ - { - "type": "object" - }, - { - "type": "array" - } - ] + "not": { + "type": "null" + } }, "Session.Inbox.Delivery": { "type": "string", @@ -18288,6 +18935,9 @@ "outcome": { "type": "string", "enum": ["succeeded", "failed", "interrupted"] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" } }, "required": ["id", "time", "type", "outcome"], diff --git a/packages/protocol/script/generate-openapi.ts b/packages/protocol/script/generate-openapi.ts index 61c4ddecd11b..7fbb47b83d96 100644 --- a/packages/protocol/script/generate-openapi.ts +++ b/packages/protocol/script/generate-openapi.ts @@ -1,4 +1,4 @@ -import { OpenApi } from "effect/unstable/httpapi" +import { OpenApi } from "effect/http-api" import { format } from "prettier" import { fileURLToPath } from "url" import { ClientApi } from "../src/client.js" diff --git a/packages/protocol/src/api.ts b/packages/protocol/src/api.ts index 0691ad4fb243..fe653d55b4f1 100644 --- a/packages/protocol/src/api.ts +++ b/packages/protocol/src/api.ts @@ -1,5 +1,5 @@ import { Context } from "effect" -import { HttpApi, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi" +import { HttpApi, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/http-api" import { SchemaErrorMiddleware } from "./middleware/schema-error.js" import { GenerateGroup } from "./groups/generate.js" import { MessageGroup } from "./groups/message.js" diff --git a/packages/protocol/src/client.ts b/packages/protocol/src/client.ts index a01155ac6666..3af3e3b41a67 100644 --- a/packages/protocol/src/client.ts +++ b/packages/protocol/src/client.ts @@ -2,7 +2,7 @@ import { LocationNotFoundError, InvalidRequestError, SessionNotFoundError } from import { makeDefaultApi } from "./api.js" import type { Api } from "./api.js" import type { Context } from "effect" -import { HttpApiMiddleware } from "effect/unstable/httpapi" +import { HttpApiMiddleware } from "effect/http-api" import type { EventGroup } from "./groups/event.js" class LocationMiddleware extends HttpApiMiddleware.Service()( diff --git a/packages/protocol/src/groups/agent.ts b/packages/protocol/src/groups/agent.ts index 4fd6e58159bc..485d0ae37cbf 100644 --- a/packages/protocol/src/groups/agent.ts +++ b/packages/protocol/src/groups/agent.ts @@ -1,7 +1,7 @@ import { Agent } from "@opencode/schema/agent" import { Location } from "@opencode/schema/location" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api" import { LocationQuery, locationQueryOpenApi } from "./location.js" import { AgentNotFoundError } from "../errors.js" diff --git a/packages/protocol/src/groups/command.ts b/packages/protocol/src/groups/command.ts index dc9b850114e4..f1360b57f82f 100644 --- a/packages/protocol/src/groups/command.ts +++ b/packages/protocol/src/groups/command.ts @@ -1,7 +1,7 @@ import { Command } from "@opencode/schema/command" import { Location } from "@opencode/schema/location" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api" import { LocationQuery, locationQueryOpenApi } from "./location.js" export const CommandGroup = HttpApiGroup.make("server.command") diff --git a/packages/protocol/src/groups/config.ts b/packages/protocol/src/groups/config.ts index cb4692838aaf..c6f6693fe257 100644 --- a/packages/protocol/src/groups/config.ts +++ b/packages/protocol/src/groups/config.ts @@ -1,7 +1,7 @@ import { Config } from "@opencode/schema/config" import { ConfigShell } from "@opencode/schema/config/shell" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api" import { LocationQuery, locationQueryOpenApi } from "./location.js" export const ConfigGroup = HttpApiGroup.make("server.config") diff --git a/packages/protocol/src/groups/credential.ts b/packages/protocol/src/groups/credential.ts index 7832916326ed..4123a48a7a43 100644 --- a/packages/protocol/src/groups/credential.ts +++ b/packages/protocol/src/groups/credential.ts @@ -1,6 +1,6 @@ import { Credential } from "@opencode/schema/credential" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api" import { ConflictError } from "../errors.js" export const CredentialGroup = HttpApiGroup.make("server.credential") diff --git a/packages/protocol/src/groups/debug.ts b/packages/protocol/src/groups/debug.ts index 8b9860076651..fb7777c57cc0 100644 --- a/packages/protocol/src/groups/debug.ts +++ b/packages/protocol/src/groups/debug.ts @@ -1,6 +1,6 @@ import { Location } from "@opencode/schema/location" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api" import { LocationQuery, locationQueryOpenApi } from "./location.js" export const DebugGroup = HttpApiGroup.make("server.debug") diff --git a/packages/protocol/src/groups/event.ts b/packages/protocol/src/groups/event.ts index f067ecfe3967..288709c44090 100644 --- a/packages/protocol/src/groups/event.ts +++ b/packages/protocol/src/groups/event.ts @@ -3,7 +3,7 @@ import { EventManifest } from "@opencode/schema/event-manifest" import { Location } from "@opencode/schema/location" import type { Definition } from "@opencode/schema/event" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api" const fields = { id: Event.ID, diff --git a/packages/protocol/src/groups/form.ts b/packages/protocol/src/groups/form.ts index 16abef80ff98..20407989f8ca 100644 --- a/packages/protocol/src/groups/form.ts +++ b/packages/protocol/src/groups/form.ts @@ -1,7 +1,7 @@ import { Form } from "@opencode/schema/form" import { Location } from "@opencode/schema/location" import { Context, Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/http-api" import { LocationQuery, locationQueryOpenApi } from "./location.js" export const makeFormGroup = ( diff --git a/packages/protocol/src/groups/fs.ts b/packages/protocol/src/groups/fs.ts index 81b56e5cd981..612e8d392115 100644 --- a/packages/protocol/src/groups/fs.ts +++ b/packages/protocol/src/groups/fs.ts @@ -2,7 +2,7 @@ import { FileSystem } from "@opencode/schema/filesystem" import { Location } from "@opencode/schema/location" import { PositiveInt } from "@opencode/schema/schema" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api" import { FileNotFoundError } from "../errors.js" import { LocationQuery, locationQueryOpenApi } from "./location.js" diff --git a/packages/protocol/src/groups/generate.ts b/packages/protocol/src/groups/generate.ts index ab44243578e7..4fe7d189579a 100644 --- a/packages/protocol/src/groups/generate.ts +++ b/packages/protocol/src/groups/generate.ts @@ -1,6 +1,6 @@ import { Model } from "@opencode/schema/model" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api" import { LocationNotFoundError, InvalidRequestError, ServiceUnavailableError } from "../errors.js" export const GenerateGroup = HttpApiGroup.make("server.generate") diff --git a/packages/protocol/src/groups/integration.ts b/packages/protocol/src/groups/integration.ts index cab3742ef2d8..49b0c78206de 100644 --- a/packages/protocol/src/groups/integration.ts +++ b/packages/protocol/src/groups/integration.ts @@ -2,7 +2,7 @@ import { Integration } from "@opencode/schema/integration" import { Location } from "@opencode/schema/location" import { Form } from "@opencode/schema/form" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api" import { IntegrationAttemptNotFoundError, IntegrationMethodNotFoundError, diff --git a/packages/protocol/src/groups/location.ts b/packages/protocol/src/groups/location.ts index ffcce9a4cd2f..37f171d8db05 100644 --- a/packages/protocol/src/groups/location.ts +++ b/packages/protocol/src/groups/location.ts @@ -1,6 +1,6 @@ import { Location } from "@opencode/schema/location" import { Context, Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/http-api" import { ServiceUnavailableError } from "../errors.js" export const LocationQuery = Schema.Struct({ diff --git a/packages/protocol/src/groups/mcp.ts b/packages/protocol/src/groups/mcp.ts index eb17f3632f2a..639466e7244f 100644 --- a/packages/protocol/src/groups/mcp.ts +++ b/packages/protocol/src/groups/mcp.ts @@ -1,7 +1,7 @@ import { Mcp } from "@opencode/schema/mcp" import { Location } from "@opencode/schema/location" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api" import { McpServerNotFoundError } from "../errors.js" import { LocationQuery, locationQueryOpenApi } from "./location.js" diff --git a/packages/protocol/src/groups/message.ts b/packages/protocol/src/groups/message.ts index c744abd880f1..0a2b4c8622d6 100644 --- a/packages/protocol/src/groups/message.ts +++ b/packages/protocol/src/groups/message.ts @@ -2,7 +2,7 @@ import { Session } from "@opencode/schema/session" import { SessionMessage } from "@opencode/schema/session-message" import { Location } from "@opencode/schema/location" import { Schema, Struct } from "effect" -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api" import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../errors.js" export const SessionMessagesQuery = Schema.Struct({ diff --git a/packages/protocol/src/groups/migration.ts b/packages/protocol/src/groups/migration.ts index fb069604b17a..d41db114a7d8 100644 --- a/packages/protocol/src/groups/migration.ts +++ b/packages/protocol/src/groups/migration.ts @@ -1,5 +1,5 @@ import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api" const V1MigrationProgress = Schema.Struct({ label: Schema.String, diff --git a/packages/protocol/src/groups/model.ts b/packages/protocol/src/groups/model.ts index 4b8ebee3491c..8df7d46fce62 100644 --- a/packages/protocol/src/groups/model.ts +++ b/packages/protocol/src/groups/model.ts @@ -1,7 +1,7 @@ import { Model } from "@opencode/schema/model" import { Location } from "@opencode/schema/location" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api" import { ServiceUnavailableError } from "../errors.js" import { LocationQuery, locationQueryOpenApi } from "./location.js" diff --git a/packages/protocol/src/groups/permission.ts b/packages/protocol/src/groups/permission.ts index 9085a51f9f89..a5f93eb61bab 100644 --- a/packages/protocol/src/groups/permission.ts +++ b/packages/protocol/src/groups/permission.ts @@ -5,7 +5,7 @@ import { PermissionSaved } from "@opencode/schema/permission-saved" import { Project } from "@opencode/schema/project" import { Session } from "@opencode/schema/session" import { Context, Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/http-api" import { LocationNotFoundError, PermissionNotFoundError, SessionNotFoundError } from "../errors.js" import { LocationQuery, locationQueryOpenApi } from "./location.js" diff --git a/packages/protocol/src/groups/persistent-pty.ts b/packages/protocol/src/groups/persistent-pty.ts index dd1abbe9d1a4..9974318772e6 100644 --- a/packages/protocol/src/groups/persistent-pty.ts +++ b/packages/protocol/src/groups/persistent-pty.ts @@ -3,7 +3,7 @@ import { Pty } from "@opencode/schema/pty" import { PtyTicket } from "@opencode/schema/pty-ticket" import { Session } from "@opencode/schema/session" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api" import { ForbiddenError, InvalidRequestError, PtyNotFoundError, ServiceUnavailableError } from "../errors.js" import { PTY_CONNECT_TICKET_QUERY, PTY_CONNECT_TOKEN_HEADER, PTY_CONNECT_TOKEN_HEADER_VALUE } from "./pty.js" diff --git a/packages/protocol/src/groups/plugin.ts b/packages/protocol/src/groups/plugin.ts index 24409d72233c..8e756e8fe467 100644 --- a/packages/protocol/src/groups/plugin.ts +++ b/packages/protocol/src/groups/plugin.ts @@ -1,7 +1,7 @@ import { Location } from "@opencode/schema/location" import { Plugin } from "@opencode/schema/plugin" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api" import { InvalidRequestError, ServiceUnavailableError } from "../errors.js" import { LocationQuery, locationQueryOpenApi } from "./location.js" diff --git a/packages/protocol/src/groups/project.ts b/packages/protocol/src/groups/project.ts index 13fd20cb47f3..f7aae9cf3673 100644 --- a/packages/protocol/src/groups/project.ts +++ b/packages/protocol/src/groups/project.ts @@ -1,6 +1,6 @@ import { Project } from "@opencode/schema/project" import { Schema, Struct } from "effect" -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api" import { ProjectNotFoundError } from "../errors.js" const root = "/api/project" diff --git a/packages/protocol/src/groups/provider.ts b/packages/protocol/src/groups/provider.ts index 77e8fdb3fe70..e5b6b39f242d 100644 --- a/packages/protocol/src/groups/provider.ts +++ b/packages/protocol/src/groups/provider.ts @@ -1,7 +1,7 @@ import { Provider } from "@opencode/schema/provider" import { Location } from "@opencode/schema/location" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api" import { ProviderNotFoundError, ServiceUnavailableError } from "../errors.js" import { LocationQuery, locationQueryOpenApi } from "./location.js" diff --git a/packages/protocol/src/groups/pty.ts b/packages/protocol/src/groups/pty.ts index f86677ba4b21..dd0c465553b1 100644 --- a/packages/protocol/src/groups/pty.ts +++ b/packages/protocol/src/groups/pty.ts @@ -2,7 +2,7 @@ import { Pty } from "@opencode/schema/pty" import { PtyTicket } from "@opencode/schema/pty-ticket" import { Location } from "@opencode/schema/location" import { Context, Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/http-api" import { ForbiddenError, LocationNotFoundError, PtyNotFoundError } from "../errors.js" import { LocationQuery, locationQueryOpenApi } from "./location.js" diff --git a/packages/protocol/src/groups/reference.ts b/packages/protocol/src/groups/reference.ts index c1f727ff6555..6935a75b84c1 100644 --- a/packages/protocol/src/groups/reference.ts +++ b/packages/protocol/src/groups/reference.ts @@ -1,7 +1,7 @@ import { Location } from "@opencode/schema/location" import { Reference } from "@opencode/schema/reference" import { Schema, Struct } from "effect" -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api" import { LocationQuery, locationQueryOpenApi } from "./location.js" const PublicLocalSource = Schema.Struct(Struct.omit(Reference.LocalSource.fields, ["description", "hidden"])).annotate({ diff --git a/packages/protocol/src/groups/rpc.ts b/packages/protocol/src/groups/rpc.ts index ab533c17104a..175229a7f063 100644 --- a/packages/protocol/src/groups/rpc.ts +++ b/packages/protocol/src/groups/rpc.ts @@ -1,6 +1,6 @@ import { optional } from "@opencode/schema/schema" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api" import { RpcError, RpcInternalError } from "../errors.js" import { LocationQuery, locationQueryOpenApi } from "./location.js" diff --git a/packages/protocol/src/groups/server.ts b/packages/protocol/src/groups/server.ts index 9b81a2511f9d..53e80925bc02 100644 --- a/packages/protocol/src/groups/server.ts +++ b/packages/protocol/src/groups/server.ts @@ -1,5 +1,5 @@ import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api" import { UnauthorizedError } from "../errors.js" export const ServerInfo = Schema.Struct({ diff --git a/packages/protocol/src/groups/session.ts b/packages/protocol/src/groups/session.ts index d5bf2af1db12..87d214ba4d05 100644 --- a/packages/protocol/src/groups/session.ts +++ b/packages/protocol/src/groups/session.ts @@ -14,8 +14,9 @@ import { statics, } from "@opencode/schema/schema" import { Event } from "@opencode/schema/event" -import { Context, Effect, Encoding, Result, Schema, SchemaGetter, Struct } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { Context, Effect, Result, Schema, SchemaGetter, SchemaParser, Struct } from "effect" +import { Base64Url } from "effect/encoding" +import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/http-api" import { ConflictError, LocationNotFoundError, @@ -99,12 +100,13 @@ const invalidCursor = "Invalid cursor" as const export const SessionsCursor = Schema.String.pipe( Schema.brand("SessionsCursor"), statics((schema) => { - const make = schema.make.bind(schema) + // Read the default constructor without caching it on the schema, so `make` can be replaced. + const make = SchemaParser.make(schema) return { - make: (input: typeof SessionsCursorInput.Type) => make(Encoding.encodeBase64Url(encodeSessionsCursor(input))), + make: (input: typeof SessionsCursorInput.Type) => make(Base64Url.encode(encodeSessionsCursor(input))), parse: (input: string) => Effect.suspend(() => { - const result = Encoding.decodeBase64UrlString(input) + const result = Base64Url.decodeString(input) return Result.isFailure(result) ? Effect.fail(invalidCursor) : decodeSessionsCursor(result.success).pipe(Effect.mapError(() => invalidCursor)) diff --git a/packages/protocol/src/groups/shell.ts b/packages/protocol/src/groups/shell.ts index ab908f4c8264..f8948caae937 100644 --- a/packages/protocol/src/groups/shell.ts +++ b/packages/protocol/src/groups/shell.ts @@ -1,7 +1,7 @@ import { Shell } from "@opencode/schema/shell" import { Location } from "@opencode/schema/location" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api" import { ShellNotFoundError } from "../errors.js" import { LocationQuery, locationQueryOpenApi } from "./location.js" diff --git a/packages/protocol/src/groups/skill.ts b/packages/protocol/src/groups/skill.ts index 103e52515f44..b01bf265d4d4 100644 --- a/packages/protocol/src/groups/skill.ts +++ b/packages/protocol/src/groups/skill.ts @@ -1,7 +1,7 @@ import { Skill } from "@opencode/schema/skill" import { Location } from "@opencode/schema/location" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api" import { LocationQuery, locationQueryOpenApi } from "./location.js" export const SkillGroup = HttpApiGroup.make("server.skill") diff --git a/packages/protocol/src/groups/vcs.ts b/packages/protocol/src/groups/vcs.ts index ad6026cdabef..ab6aa9300d8e 100644 --- a/packages/protocol/src/groups/vcs.ts +++ b/packages/protocol/src/groups/vcs.ts @@ -3,7 +3,7 @@ import { Location } from "@opencode/schema/location" import { NonNegativeInt, PositiveInt, optional } from "@opencode/schema/schema" import { Vcs } from "@opencode/schema/vcs" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api" import { LocationQuery, locationQueryOpenApi } from "./location.js" import { ConflictError, InvalidRequestError, ServiceUnavailableError } from "../errors.js" diff --git a/packages/protocol/src/groups/websearch.ts b/packages/protocol/src/groups/websearch.ts index 8a333898de08..7218d67364d8 100644 --- a/packages/protocol/src/groups/websearch.ts +++ b/packages/protocol/src/groups/websearch.ts @@ -1,7 +1,7 @@ import { Location } from "@opencode/schema/location" import { WebSearch } from "@opencode/schema/websearch" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api" import { InvalidRequestError, ServiceUnavailableError } from "../errors.js" import { LocationQuery, locationQueryOpenApi } from "./location.js" diff --git a/packages/protocol/src/groups/worktree.ts b/packages/protocol/src/groups/worktree.ts index 0e8f26a8c24f..1ef649f6c6f1 100644 --- a/packages/protocol/src/groups/worktree.ts +++ b/packages/protocol/src/groups/worktree.ts @@ -1,6 +1,6 @@ import { Worktree } from "@opencode/schema/worktree" import { Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api" import { Project } from "@opencode/schema/project" import { ProjectNotFoundError } from "../errors.js" diff --git a/packages/protocol/src/middleware/authorization.ts b/packages/protocol/src/middleware/authorization.ts index 54d33bc26ee2..57bbf8434ccd 100644 --- a/packages/protocol/src/middleware/authorization.ts +++ b/packages/protocol/src/middleware/authorization.ts @@ -1,4 +1,4 @@ -import { HttpApiMiddleware } from "effect/unstable/httpapi" +import { HttpApiMiddleware } from "effect/http-api" import { UnauthorizedError } from "../errors.js" export class Authorization extends HttpApiMiddleware.Service()("@opencode/HttpApiAuthorization", { diff --git a/packages/protocol/src/middleware/schema-error.ts b/packages/protocol/src/middleware/schema-error.ts index a4c36d5b8c05..d8d917930ae5 100644 --- a/packages/protocol/src/middleware/schema-error.ts +++ b/packages/protocol/src/middleware/schema-error.ts @@ -1,4 +1,4 @@ -import { HttpApiMiddleware } from "effect/unstable/httpapi" +import { HttpApiMiddleware } from "effect/http-api" import { InvalidRequestError } from "../errors.js" export class SchemaErrorMiddleware extends HttpApiMiddleware.Service()( diff --git a/packages/protocol/src/simulation.ts b/packages/protocol/src/simulation.ts index 38c9fde40fa0..ad94fc8e4275 100644 --- a/packages/protocol/src/simulation.ts +++ b/packages/protocol/src/simulation.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import { Rpc, RpcGroup } from "effect/unstable/rpc" +import { Rpc, RpcGroup } from "effect/rpc" const JsonRpcID = Schema.Union([Schema.String, Schema.Number, Schema.Null]) const decodeJson = Schema.decodeUnknownSync(Schema.Json) diff --git a/packages/protocol/test/openapi-patterns.test.ts b/packages/protocol/test/openapi-patterns.test.ts new file mode 100644 index 000000000000..bc048e2b5cf1 --- /dev/null +++ b/packages/protocol/test/openapi-patterns.test.ts @@ -0,0 +1,28 @@ +import { expect, test } from "bun:test" +import { OpenApi } from "effect/http-api" +import { ClientApi } from "../src/client.js" + +test("the OpenAPI document keeps string pattern constraints from protocol schemas", () => { + const patterns = new Set() + const collect = (value: unknown): void => { + if (Array.isArray(value)) return value.forEach(collect) + if (typeof value !== "object" || value === null) return + Object.entries(value).forEach(([key, item]) => { + if (key === "pattern" && typeof item === "string") patterns.add(item) + collect(item) + }) + } + collect(OpenApi.fromApi(ClientApi)) + + expect([...patterns]).toEqual( + expect.arrayContaining([ + "^[^/#]+\\/[^#]+(?:#[^#]+)?$", + "^[^/#]+$", + "^[^#]+$", + "^#[0-9a-fA-F]{6}$", + "^[a-z0-9][a-z0-9._-]*$", + "^[a-z][a-z0-9._-]*$", + "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + ]), + ) +}) diff --git a/packages/schema/src/config/agent.ts b/packages/schema/src/config/agent.ts index f8a38e00e88d..c28933251b58 100644 --- a/packages/schema/src/config/agent.ts +++ b/packages/schema/src/config/agent.ts @@ -6,7 +6,7 @@ import { optional, PositiveInt } from "../schema.js" import { ConfigModel } from "./model.js" import { ConfigProvider } from "./provider.js" -export const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) +export const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/u)) export class Info extends Schema.Class("Config.Agent")({ model: ConfigModel.Selection.pipe(optional), diff --git a/packages/schema/src/config/model.ts b/packages/schema/src/config/model.ts index dc2a0a212a6b..92625bdbbf7c 100644 --- a/packages/schema/src/config/model.ts +++ b/packages/schema/src/config/model.ts @@ -5,9 +5,9 @@ import { Model } from "../model.js" import { Provider } from "../provider.js" import { optional } from "../schema.js" -const ProviderID = Provider.ID.check(Schema.isPattern(/^[^/#]+$/)) -const ModelID = Model.ID.check(Schema.isPattern(/^[^#]+$/)) -const VariantID = Model.VariantID.check(Schema.isPattern(/^[^#]+$/)) +const ProviderID = Provider.ID.check(Schema.isPattern(/^[^/#]+$/u)) +const ModelID = Model.ID.check(Schema.isPattern(/^[^#]+$/u)) +const VariantID = Model.VariantID.check(Schema.isPattern(/^[^#]+$/u)) const Explicit = Schema.Struct({ providerID: ProviderID, @@ -15,7 +15,7 @@ const Explicit = Schema.Struct({ variant: VariantID.pipe(optional), }) -const Short = Schema.String.check(Schema.isPattern(/^[^/#]+\/[^#]+(?:#[^#]+)?$/)) +const Short = Schema.String.check(Schema.isPattern(/^[^/#]+\/[^#]+(?:#[^#]+)?$/u)) export interface Selection extends Schema.Schema.Type {} export const Selection = Schema.Union([Short, Explicit]) diff --git a/packages/schema/src/event.ts b/packages/schema/src/event.ts index db0c8c557357..b1dc5b04627d 100644 --- a/packages/schema/src/event.ts +++ b/packages/schema/src/event.ts @@ -6,7 +6,7 @@ import { ascending } from "./identifier.js" import { Location } from "./location.js" import { statics } from "./schema.js" -export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe( +export const ID = Schema.String.check(Schema.isStartingWith("evt_")).pipe( Schema.brand("Event.ID"), statics((schema) => ({ create: () => schema.make("evt_" + ascending()) })), ) diff --git a/packages/schema/src/form.ts b/packages/schema/src/form.ts index adbf14a9b65e..b3cd03e8782e 100644 --- a/packages/schema/src/form.ts +++ b/packages/schema/src/form.ts @@ -5,7 +5,7 @@ import { ephemeral, inventory } from "./event.js" import { ascending } from "./identifier.js" import { NonNegativeInt, optional, statics } from "./schema.js" -const IDSchema = Schema.String.check(Schema.isStartsWith("frm_")).pipe(Schema.brand("Form.ID")) +const IDSchema = Schema.String.check(Schema.isStartingWith("frm_")).pipe(Schema.brand("Form.ID")) export const ID = IDSchema.pipe( statics((schema: typeof IDSchema) => ({ create: (id?: string) => schema.make(id ?? "frm_" + ascending()) })), diff --git a/packages/schema/src/instruction-entry.ts b/packages/schema/src/instruction-entry.ts index 8d4050114afd..b96eb589df32 100644 --- a/packages/schema/src/instruction-entry.ts +++ b/packages/schema/src/instruction-entry.ts @@ -7,7 +7,7 @@ import { Schema } from "effect" * derives the namespaced Instructions key as `api/`, keeping the * `api/*` namespace enforced by construction. */ -export const Key = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9._-]*$/)).annotate({ +export const Key = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9._-]*$/u)).annotate({ identifier: "InstructionEntry.Key", description: "Instruction entry key (lowercase alphanumerics plus . _ -)", }) diff --git a/packages/schema/src/instruction.ts b/packages/schema/src/instruction.ts index a2c1e93a1c9d..f2ff1773854d 100644 --- a/packages/schema/src/instruction.ts +++ b/packages/schema/src/instruction.ts @@ -2,12 +2,12 @@ export * as Instruction from "./instruction.js" import { Schema } from "effect" -export const Key = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._/-]*$/)).pipe( +export const Key = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._/-]*$/u)).pipe( Schema.brand("Instruction.Key"), ) export type Key = typeof Key.Type -export const Hash = Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/)).pipe(Schema.brand("Instruction.Hash")) +export const Hash = Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/u)).pipe(Schema.brand("Instruction.Hash")) export type Hash = typeof Hash.Type export const Values = Schema.Record(Key, Hash) diff --git a/packages/schema/src/permission.ts b/packages/schema/src/permission.ts index e978c9463292..85fedd8b2f09 100644 --- a/packages/schema/src/permission.ts +++ b/packages/schema/src/permission.ts @@ -7,7 +7,7 @@ import { ascending } from "./identifier.js" import { SessionID } from "./session-id.js" import { statics } from "./schema.js" -export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe( +export const ID = Schema.String.check(Schema.isStartingWith("per")).pipe( Schema.brand("Permission.ID"), statics((schema) => ({ create: (id?: string) => schema.make(id ?? "per_" + ascending()) })), ) diff --git a/packages/schema/src/project.ts b/packages/schema/src/project.ts index 5545265c623e..2763e3172489 100644 --- a/packages/schema/src/project.ts +++ b/packages/schema/src/project.ts @@ -8,7 +8,7 @@ import { ProjectID } from "./project-id.js" export const ID = ProjectID export type ID = typeof ID.Type -export const Vcs = Schema.String.check(Schema.isPattern(/^[a-z][a-z0-9._-]*$/)).annotate({ +export const Vcs = Schema.String.check(Schema.isPattern(/^[a-z][a-z0-9._-]*$/u)).annotate({ identifier: "Project.Vcs", }) export const Current = Schema.Struct({ diff --git a/packages/schema/src/prompt.ts b/packages/schema/src/prompt.ts index d458f8283ff8..ba8a7a730794 100644 --- a/packages/schema/src/prompt.ts +++ b/packages/schema/src/prompt.ts @@ -19,7 +19,7 @@ export const FileSource = Schema.Union([ export type FileSource = typeof FileSource.Type export const Base64 = Schema.String.check( - Schema.isPattern(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/), + Schema.isPattern(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u), ).annotate({ identifier: "Prompt.Base64" }) export type Base64 = typeof Base64.Type diff --git a/packages/schema/src/pty.ts b/packages/schema/src/pty.ts index b2da9093f0fb..848d743417f7 100644 --- a/packages/schema/src/pty.ts +++ b/packages/schema/src/pty.ts @@ -6,7 +6,7 @@ import { ephemeral, inventory } from "./event.js" import { ascending } from "./identifier.js" import { NonNegativeInt, PositiveInt, statics } from "./schema.js" -const IDSchema = Schema.String.check(Schema.isStartsWith("pty")).pipe(Schema.brand("PtyID")) +const IDSchema = Schema.String.check(Schema.isStartingWith("pty")).pipe(Schema.brand("PtyID")) export const ID = IDSchema.pipe( statics((schema: typeof IDSchema) => { diff --git a/packages/schema/src/schema.ts b/packages/schema/src/schema.ts index d19a39b9708c..6cbbc0b1bf79 100644 --- a/packages/schema/src/schema.ts +++ b/packages/schema/src/schema.ts @@ -17,10 +17,15 @@ export const optional = (schema: S) => }), ) +// Effect schemas expose `make` through prototype getters, so `Object.assign` cannot override it. +// Defining the properties replaces a constructor as long as the schema's own `make` has not been read. +export const withStatics = >(schema: S, methods: M): S & M => + Object.defineProperties(schema, Object.getOwnPropertyDescriptors(methods)) as S & M + export const statics = >(methods: (schema: S) => M) => (schema: S): S & M => - Object.assign(schema, methods(schema)) + withStatics(schema, methods(schema)) export const DateTimeUtcFromMillis = Schema.Finite.pipe( Schema.decodeTo(Schema.DateTimeUtc, { diff --git a/packages/schema/src/session-id.ts b/packages/schema/src/session-id.ts index ba0b48ce7ef5..4e74e7ab9977 100644 --- a/packages/schema/src/session-id.ts +++ b/packages/schema/src/session-id.ts @@ -2,7 +2,7 @@ import { Schema } from "effect" import { descending } from "./identifier.js" import { statics } from "./schema.js" -export const SessionID = Schema.String.check(Schema.isStartsWith("ses")).pipe( +export const SessionID = Schema.String.check(Schema.isStartingWith("ses")).pipe( Schema.brand("SessionID"), statics((schema) => { const create = () => schema.make("ses_" + descending()) diff --git a/packages/schema/src/session-message.ts b/packages/schema/src/session-message.ts index f8a3e64b01c1..82f6a5164147 100644 --- a/packages/schema/src/session-message.ts +++ b/packages/schema/src/session-message.ts @@ -20,7 +20,7 @@ import { Money } from "./money.js" import { Snapshot } from "./snapshot.js" import { TokenUsage } from "./token-usage.js" -export const ID = Schema.String.check(Schema.isStartsWith("msg_")).pipe( +export const ID = Schema.String.check(Schema.isStartingWith("msg_")).pipe( Schema.brand("Session.Message.ID"), statics((schema) => ({ create: () => schema.make("msg_" + ascending()), diff --git a/packages/schema/src/shell.ts b/packages/schema/src/shell.ts index 251a61417b11..edeeeed6eb88 100644 --- a/packages/schema/src/shell.ts +++ b/packages/schema/src/shell.ts @@ -6,7 +6,7 @@ import { ephemeral, inventory } from "./event.js" import { ascending } from "./identifier.js" import { NonNegativeInt, statics } from "./schema.js" -const IDSchema = Schema.String.check(Schema.isStartsWith("sh_")).pipe(Schema.brand("Shell.ID")) +const IDSchema = Schema.String.check(Schema.isStartingWith("sh_")).pipe(Schema.brand("Shell.ID")) export const ID = IDSchema.pipe( statics((schema: typeof IDSchema) => { diff --git a/packages/schema/src/v1/permission.ts b/packages/schema/src/v1/permission.ts index c7d2e71b3da0..00dc2f9b19b9 100644 --- a/packages/schema/src/v1/permission.ts +++ b/packages/schema/src/v1/permission.ts @@ -7,7 +7,7 @@ import { Project } from "../project.js" import { statics } from "../schema.js" import { SessionID } from "../session-id.js" -export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe( +export const ID = Schema.String.check(Schema.isStartingWith("per")).pipe( Schema.brand("PermissionV1.ID"), statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "per_" + ascending()) })), ) diff --git a/packages/schema/src/v1/session.ts b/packages/schema/src/v1/session.ts index 02a8d5aa9531..e5e3eda4a7af 100644 --- a/packages/schema/src/v1/session.ts +++ b/packages/schema/src/v1/session.ts @@ -14,13 +14,13 @@ import { FileDiff } from "../file-diff.js" const Timestamp = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)) -export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe( +export const MessageID = Schema.String.check(Schema.isStartingWith("msg")).pipe( Schema.brand("SessionV1.MessageID"), statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "msg_" + ascending()) })), ) export type MessageID = typeof MessageID.Type -export const PartID = Schema.String.check(Schema.isStartsWith("prt")).pipe( +export const PartID = Schema.String.check(Schema.isStartingWith("prt")).pipe( Schema.brand("SessionV1.PartID"), statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "prt_" + ascending()) })), ) diff --git a/packages/schema/src/workspace-id.ts b/packages/schema/src/workspace-id.ts index ad2ed5a71291..54a7e1cd93fb 100644 --- a/packages/schema/src/workspace-id.ts +++ b/packages/schema/src/workspace-id.ts @@ -2,7 +2,7 @@ import { Schema } from "effect" import { ascending } from "./identifier.js" import { statics } from "./schema.js" -export const WorkspaceID = Schema.String.check(Schema.isStartsWith("wrk")).pipe( +export const WorkspaceID = Schema.String.check(Schema.isStartingWith("wrk")).pipe( Schema.brand("Workspace.ID"), statics((schema) => { const create = () => schema.make("wrk_" + ascending()) diff --git a/packages/sdk/src/effect/opencode.ts b/packages/sdk/src/effect/opencode.ts index f4c88089132e..4787d8fbec74 100644 --- a/packages/sdk/src/effect/opencode.ts +++ b/packages/sdk/src/effect/opencode.ts @@ -5,7 +5,7 @@ import type { Session } from "@opencode/core/session" import type { Workspace } from "@opencode/core/workspace" import { Context, Effect, Layer } from "effect" import type { Config, Scope } from "effect" -import { FetchHttpClient, HttpClient } from "effect/unstable/http" +import { FetchHttpClient, HttpClient } from "effect/http" import { EmbeddedHost } from "../internal/host" import type { SdkInstances } from "../internal/instances" diff --git a/packages/sdk/src/internal/host.ts b/packages/sdk/src/internal/host.ts index 5e1dd60404a7..3c354ae6653e 100644 --- a/packages/sdk/src/internal/host.ts +++ b/packages/sdk/src/internal/host.ts @@ -9,7 +9,7 @@ import { createEmbeddedRoutes } from "@opencode/server/routes" import type { ServerOptions } from "@opencode/server/options" import type { LayerNode } from "@opencode/util/effect/layer-node" import { Context, Effect, Layer, ManagedRuntime, Scope } from "effect" -import { HttpEffect, HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http" +import { HttpEffect, HttpRouter, HttpServer, HttpServerRequest } from "effect/http" import { context, layer, type LogOptions } from "../logging" import { OwnedFetch } from "./fetch" import { SdkInstances } from "./instances" diff --git a/packages/sdk/test/transport.test.ts b/packages/sdk/test/transport.test.ts index 2da7f7dac4a1..f7243d0deccb 100644 --- a/packages/sdk/test/transport.test.ts +++ b/packages/sdk/test/transport.test.ts @@ -1,6 +1,6 @@ import { expect } from "bun:test" import { Context, Effect, Exit, Layer, Scope, Stream } from "effect" -import { FetchHttpClient } from "effect/unstable/http" +import { FetchHttpClient } from "effect/http" import { tmpdirScoped } from "../../core/test/fixture/tmpdir" import { testEffect } from "../../core/test/lib/effect" import { AbsolutePath, Location, OpenCode, Session } from "../src/effect" diff --git a/packages/server/package.json b/packages/server/package.json index 74a6796c1ef7..635cdfa106d0 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -35,10 +35,12 @@ "effect": "catalog:" }, "devDependencies": { + "@opencode-ai/pty": "0.2.0", "@opencode/client": "workspace:*", "@opencode/plugin": "workspace:*", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", - "@typescript/native-preview": "catalog:" + "@typescript/native-preview": "catalog:", + "fast-check": "catalog:" } } diff --git a/packages/server/src/fetch.ts b/packages/server/src/fetch.ts index 88a52634897b..e6aba4da568e 100644 --- a/packages/server/src/fetch.ts +++ b/packages/server/src/fetch.ts @@ -1,7 +1,7 @@ export * as ServerFetch from "./fetch" import { Context, Effect, Layer } from "effect" -import { HttpEffect, HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http" +import { HttpEffect, HttpMiddleware, HttpRouter, HttpServer } from "effect/http" import { SessionRestart } from "@opencode/core/session/execution/restart" import type { LayerNode } from "@opencode/util/effect/layer-node" import { isAllowedCorsOrigin } from "./cors" diff --git a/packages/server/src/handlers/agent.ts b/packages/server/src/handlers/agent.ts index 713cf3f62626..f54148af2266 100644 --- a/packages/server/src/handlers/agent.ts +++ b/packages/server/src/handlers/agent.ts @@ -1,6 +1,6 @@ import { Agent } from "@opencode/core/agent" import { Effect } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" import { response } from "../location" import { AgentNotFoundError } from "@opencode/protocol/errors" diff --git a/packages/server/src/handlers/command.ts b/packages/server/src/handlers/command.ts index bf289721bc8b..c540478fa51b 100644 --- a/packages/server/src/handlers/command.ts +++ b/packages/server/src/handlers/command.ts @@ -1,5 +1,5 @@ import { Command } from "@opencode/core/command" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" import { response } from "../location" diff --git a/packages/server/src/handlers/config.ts b/packages/server/src/handlers/config.ts index 4a22939dba26..9713b10a5201 100644 --- a/packages/server/src/handlers/config.ts +++ b/packages/server/src/handlers/config.ts @@ -1,7 +1,7 @@ import { Config } from "@opencode/core/config" import { ShellSelect } from "@opencode/core/shell/select" import { Effect } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" export const ConfigHandler = HttpApiBuilder.group(Api, "server.config", (handlers) => diff --git a/packages/server/src/handlers/credential.ts b/packages/server/src/handlers/credential.ts index a2cf71581812..4f6f31a9c0cb 100644 --- a/packages/server/src/handlers/credential.ts +++ b/packages/server/src/handlers/credential.ts @@ -1,7 +1,7 @@ import { Credential } from "@opencode/core/credential" import { ConflictError } from "@opencode/protocol/errors" import { Effect } from "effect" -import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" +import { HttpApiBuilder, HttpApiSchema } from "effect/http-api" import { Api } from "../api" export const CredentialHandler = HttpApiBuilder.group(Api, "server.credential", (handlers) => diff --git a/packages/server/src/handlers/debug.ts b/packages/server/src/handlers/debug.ts index dd5b83762f60..bb875b170816 100644 --- a/packages/server/src/handlers/debug.ts +++ b/packages/server/src/handlers/debug.ts @@ -1,6 +1,6 @@ import { LocationServiceMap } from "@opencode/core/location-service-map" import { Effect, Option, RcMap } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" import { requestRef } from "../location" diff --git a/packages/server/src/handlers/event.ts b/packages/server/src/handlers/event.ts index 40963783e4fa..987f2879d3aa 100644 --- a/packages/server/src/handlers/event.ts +++ b/packages/server/src/handlers/event.ts @@ -1,7 +1,7 @@ import { Event } from "@opencode/schema/event" import { Effect, Stream } from "effect" -import { HttpServerResponse } from "effect/unstable/http" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpServerResponse } from "effect/http" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" import { EventFeed } from "../event-feed" diff --git a/packages/server/src/handlers/form.ts b/packages/server/src/handlers/form.ts index 15f2714c0562..8d9640481fef 100644 --- a/packages/server/src/handlers/form.ts +++ b/packages/server/src/handlers/form.ts @@ -1,6 +1,6 @@ import { Form } from "@opencode/core/form" import { Effect } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" import { response } from "../location" diff --git a/packages/server/src/handlers/fs.ts b/packages/server/src/handlers/fs.ts index 51c9f95cb975..11225d42b716 100644 --- a/packages/server/src/handlers/fs.ts +++ b/packages/server/src/handlers/fs.ts @@ -2,8 +2,8 @@ import { FileSystem } from "@opencode/core/filesystem" import { RelativePath } from "@opencode/core/schema" import { FileNotFoundError } from "@opencode/protocol/errors" import { Effect, Option, Stream } from "effect" -import { type HttpServerRequest, HttpServerResponse } from "effect/unstable/http" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { type HttpServerRequest, HttpServerResponse } from "effect/http" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" import { response } from "../location" diff --git a/packages/server/src/handlers/generate.ts b/packages/server/src/handlers/generate.ts index 9d1f4346edda..d4c7bf8d6054 100644 --- a/packages/server/src/handlers/generate.ts +++ b/packages/server/src/handlers/generate.ts @@ -5,7 +5,7 @@ import { AbsolutePath } from "@opencode/core/schema" import { InvalidRequestError, ServiceUnavailableError } from "@opencode/protocol/errors" import { Global } from "@opencode/util/global" import { Effect } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" import { locationErrors } from "../location" diff --git a/packages/server/src/handlers/integration.ts b/packages/server/src/handlers/integration.ts index 04e6f604e0a5..dfdbaa447620 100644 --- a/packages/server/src/handlers/integration.ts +++ b/packages/server/src/handlers/integration.ts @@ -1,7 +1,7 @@ import { Integration } from "@opencode/core/integration" import { Plugin } from "@opencode/core/plugin" import { Effect } from "effect" -import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" +import { HttpApiBuilder, HttpApiSchema } from "effect/http-api" import { Api } from "../api" import { IntegrationAttemptNotFoundError, diff --git a/packages/server/src/handlers/location.ts b/packages/server/src/handlers/location.ts index 2d376f8755d3..942b9d923bcf 100644 --- a/packages/server/src/handlers/location.ts +++ b/packages/server/src/handlers/location.ts @@ -2,7 +2,7 @@ import { Location } from "@opencode/core/location" import { LocationServiceMap } from "@opencode/core/location-service-map" import { ServiceUnavailableError } from "@opencode/protocol/errors" import { Cause, Effect } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" export const LocationHandler = HttpApiBuilder.group(Api, "server.location", (handlers) => diff --git a/packages/server/src/handlers/mcp.ts b/packages/server/src/handlers/mcp.ts index 47933234585c..d6b47b27e632 100644 --- a/packages/server/src/handlers/mcp.ts +++ b/packages/server/src/handlers/mcp.ts @@ -1,7 +1,7 @@ import { Mcp } from "@opencode/core/mcp/index" import { McpServerNotFoundError } from "@opencode/protocol/errors" import { Effect } from "effect" -import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" +import { HttpApiBuilder, HttpApiSchema } from "effect/http-api" import { Api } from "../api" import { response } from "../location" diff --git a/packages/server/src/handlers/message.ts b/packages/server/src/handlers/message.ts index 04f6120b74b4..5bf8d6213952 100644 --- a/packages/server/src/handlers/message.ts +++ b/packages/server/src/handlers/message.ts @@ -1,7 +1,7 @@ import { SessionMessage } from "@opencode/core/session/message" import { Session } from "@opencode/core/session" import { Effect, Schema } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" import { InvalidCursorError } from "@opencode/protocol/errors" import { failedMessageDecode, missingSession } from "./session-error" diff --git a/packages/server/src/handlers/migration.ts b/packages/server/src/handlers/migration.ts index 1c287560050b..15c0e5f435b8 100644 --- a/packages/server/src/handlers/migration.ts +++ b/packages/server/src/handlers/migration.ts @@ -1,5 +1,5 @@ import { V1Migration } from "@opencode/core/database/v1-migration" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Effect } from "effect" import { Api } from "../api" diff --git a/packages/server/src/handlers/model.ts b/packages/server/src/handlers/model.ts index b316fd2b6ddf..43599e9ea940 100644 --- a/packages/server/src/handlers/model.ts +++ b/packages/server/src/handlers/model.ts @@ -1,6 +1,6 @@ import { Model } from "@opencode/core/model" import { Effect } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" import { response } from "../location" diff --git a/packages/server/src/handlers/permission.ts b/packages/server/src/handlers/permission.ts index 72b61c0fbd7b..2832c502d649 100644 --- a/packages/server/src/handlers/permission.ts +++ b/packages/server/src/handlers/permission.ts @@ -4,7 +4,7 @@ import { Permission } from "@opencode/core/permission" import { PermissionSaved } from "@opencode/core/permission/saved" import { Session } from "@opencode/core/session" import { Effect } from "effect" -import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" +import { HttpApiBuilder, HttpApiSchema } from "effect/http-api" import { Api } from "../api" import { PermissionNotFoundError } from "@opencode/protocol/errors" import { locationErrors, response, sessionInfo } from "../location" diff --git a/packages/server/src/handlers/persistent-pty.ts b/packages/server/src/handlers/persistent-pty.ts index 748b86e42f31..cb452e96e4e2 100644 --- a/packages/server/src/handlers/persistent-pty.ts +++ b/packages/server/src/handlers/persistent-pty.ts @@ -6,13 +6,13 @@ import { PTY_CONNECT_TOKEN_HEADER, PTY_CONNECT_TOKEN_HEADER_VALUE, } from "@opencode/protocol/groups/persistent-pty" -import { Effect, Queue, Semaphore } from "effect" -import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http" -import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" -import { Socket } from "effect/unstable/socket" +import { Effect, Queue } from "effect" +import { HttpServerRequest, HttpServerResponse } from "effect/http" +import { HttpApiBuilder, HttpApiSchema } from "effect/http-api" +import { Socket } from "effect/socket" import { Api } from "../api" import { CorsConfig, isAllowedRequestOrigin } from "../cors" -import { runPtySocket } from "./pty-socket" +import { type Outbound, runPtySocket } from "./pty-socket" export const PersistentPtyHandler = HttpApiBuilder.group(Api, "server.experimental", (handlers) => Effect.gen(function* () { @@ -123,9 +123,7 @@ export const PersistentPtyHandler = HttpApiBuilder.group(Api, "server.experiment if (!Number.isSafeInteger(cursor) || cursor < 0) return HttpServerResponse.empty({ status: 400 }) const socket = yield* Effect.orDie(ctx.request.upgrade) - const write = yield* socket.writer - const outbox = yield* Queue.unbounded() - const input = yield* Semaphore.make(1) + const outbox = yield* Queue.unbounded() let attachment: PersistentPty.Attachment | undefined // Bun's native ws upgrade must start before asynchronous daemon I/O. const onOpen = Effect.gen(function* () { @@ -183,49 +181,28 @@ export const PersistentPtyHandler = HttpApiBuilder.group(Api, "server.experiment attachment.activate() }) - const drain = Effect.gen(function* () { - while (true) { - const item = yield* Queue.take(outbox) - yield* write(item) - if (item instanceof Socket.CloseEvent) return - } + yield* runPtySocket({ + socket, + outbox, + onOpen, + onMessage: (message) => { + if (!attachment) return Effect.void + const data = typeof message === "string" ? Buffer.from(message) : message + if (!framedInput) + return pty + .input(ctx.params.ptyID, attachmentID, attachment.info.size.cols, attachment.info.size.rows, data) + .pipe(Effect.ignore) + if (data.byteLength < 5) return Effect.void + const view = new DataView(data.buffer, data.byteOffset, data.byteLength) + const type = data[0] + const cols = view.getUint16(1) + const rows = view.getUint16(3) + if ((type !== 0 && type !== 1) || cols === 0 || rows === 0) return Effect.void + if (type === 0) return pty.control(ctx.params.ptyID, attachmentID, cols, rows).pipe(Effect.ignore) + return pty.input(ctx.params.ptyID, attachmentID, cols, rows, data.subarray(5)).pipe(Effect.ignore) + }, + detach: () => attachment?.detach(), }) - - yield* runPtySocket( - drain, - socket.runRaw( - (message) => - input.withPermit( - Effect.suspend(() => { - if (!attachment) return Effect.void - const data = typeof message === "string" ? Buffer.from(message) : message - if (!framedInput) - return pty - .input( - ctx.params.ptyID, - attachmentID, - attachment.info.size.cols, - attachment.info.size.rows, - data, - ) - .pipe(Effect.ignore) - if (data.byteLength < 5) return Effect.void - const view = new DataView(data.buffer, data.byteOffset, data.byteLength) - const type = data[0] - const cols = view.getUint16(1) - const rows = view.getUint16(3) - if ((type !== 0 && type !== 1) || cols === 0 || rows === 0) return Effect.void - if (type === 0) return pty.control(ctx.params.ptyID, attachmentID, cols, rows).pipe(Effect.ignore) - return pty.input(ctx.params.ptyID, attachmentID, cols, rows, data.subarray(5)).pipe(Effect.ignore) - }), - ), - { onOpen }, - ), - () => attachment?.detach(), - ).pipe( - Effect.catchReason("SocketError", "SocketCloseError", () => Effect.void), - Effect.orDie, - ) return HttpServerResponse.empty() }), ) diff --git a/packages/server/src/handlers/plugin.ts b/packages/server/src/handlers/plugin.ts index 31fe39c483e8..76e157466e0e 100644 --- a/packages/server/src/handlers/plugin.ts +++ b/packages/server/src/handlers/plugin.ts @@ -2,7 +2,7 @@ import { Plugin } from "@opencode/core/plugin" import { PluginUpdate } from "@opencode/core/plugin/update" import { InvalidRequestError, ServiceUnavailableError } from "@opencode/protocol/errors" import { Cause, Effect, Exit } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" import { response } from "../location" diff --git a/packages/server/src/handlers/project.ts b/packages/server/src/handlers/project.ts index f6bcf2a13583..7a32951b3fee 100644 --- a/packages/server/src/handlers/project.ts +++ b/packages/server/src/handlers/project.ts @@ -1,6 +1,6 @@ import { Project } from "@opencode/core/project" import { Effect } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" import { ProjectNotFoundError } from "@opencode/protocol/errors" diff --git a/packages/server/src/handlers/provider.ts b/packages/server/src/handlers/provider.ts index 75bdb0e5503e..ab9c2201a5ee 100644 --- a/packages/server/src/handlers/provider.ts +++ b/packages/server/src/handlers/provider.ts @@ -1,7 +1,7 @@ import { Provider } from "@opencode/core/provider" import { ProviderNotFoundError } from "@opencode/protocol/errors" import { Effect } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" import { response } from "../location" diff --git a/packages/server/src/handlers/pty-socket.ts b/packages/server/src/handlers/pty-socket.ts index ecf965bb1797..21cf51f818bd 100644 --- a/packages/server/src/handlers/pty-socket.ts +++ b/packages/server/src/handlers/pty-socket.ts @@ -1,9 +1,33 @@ -import { Effect } from "effect" +import { Effect, Queue } from "effect" +import { Socket } from "effect/socket" -export function runPtySocket( - drain: Effect.Effect, - socket: Effect.Effect, - detach: () => void, -) { - return Effect.raceFirst(drain, socket).pipe(Effect.ensuring(Effect.sync(detach))) +export type Outbound = string | Uint8Array | Socket.CloseEvent + +// Outbound frames flow through one queue drained by a single writer so replay, live output, and the close +// frame keep their order. Either side closing ends the connection normally, and every exit detaches. +export function runPtySocket(input: { + readonly socket: Socket.Socket + readonly outbox: Queue.Dequeue + readonly onOpen?: Effect.Effect + readonly onMessage: (message: string | Uint8Array) => Effect.Effect + readonly detach: () => void +}) { + const drain = Effect.gen(function* () { + const writer = yield* input.socket.writer + while (true) { + const item = yield* Queue.take(input.outbox) + yield* writer.write(item) + if (item instanceof Socket.CloseEvent) return + } + }) + const read = Effect.gen(function* () { + const reader = yield* input.socket.reader + if (input.onOpen) yield* input.onOpen + while (true) yield* Effect.forEach(yield* reader.pull, input.onMessage, { discard: true }) + }) + return Effect.raceFirst(drain, read).pipe( + Effect.ensuring(Effect.sync(input.detach)), + Effect.catchReason("SocketError", "SocketCloseError", () => Effect.void), + Effect.orDie, + ) } diff --git a/packages/server/src/handlers/pty.ts b/packages/server/src/handlers/pty.ts index 3de690575501..4a2ebbe6a7ba 100644 --- a/packages/server/src/handlers/pty.ts +++ b/packages/server/src/handlers/pty.ts @@ -4,9 +4,9 @@ import { PtyTicket } from "@opencode/core/pty/ticket" import { Location } from "@opencode/core/location" import { LocationServiceMap } from "@opencode/core/location-service-map" import { Effect, Queue } from "effect" -import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http" -import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" -import { Socket } from "effect/unstable/socket" +import { HttpServerRequest, HttpServerResponse } from "effect/http" +import { HttpApiBuilder, HttpApiSchema } from "effect/http-api" +import { Socket } from "effect/socket" import { Api } from "../api" import { CorsConfig, isAllowedRequestOrigin } from "../cors" import { ForbiddenError, PtyNotFoundError } from "@opencode/protocol/errors" @@ -17,7 +17,7 @@ import { } from "@opencode/protocol/groups/pty" import { locationErrors, requestRef, response } from "../location" import { PtyEnvironment } from "../pty-environment" -import { runPtySocket } from "./pty-socket" +import { type Outbound, runPtySocket } from "./pty-socket" const ticketScope = Effect.gen(function* () { const location = yield* Location.Service @@ -176,20 +176,8 @@ export const PtyHandler = HttpApiBuilder.group(Api, "server.pty", (handlers) => : undefined const socket = yield* Effect.orDie(ctx.request.upgrade) - const write = yield* socket.writer - const closeAccepted = (event: Socket.CloseEvent) => - socket - .runRaw(() => Effect.void, { onOpen: write(event).pipe(Effect.catch(() => Effect.void)) }) - .pipe( - Effect.timeout("1 second"), - Effect.catchReason("SocketError", "SocketCloseError", () => Effect.void), - Effect.catch(() => Effect.void), - ) - - // Outbound frames flow through one queue drained by a single writer so replay, live - // output, and the close frame keep their order. // TODO: Integrate graceful-shutdown socket tracking before clients migrate to this route. - const outbox = yield* Queue.unbounded() + const outbox = yield* Queue.unbounded() const attachment = yield* pty .attach(ctx.params.ptyID, { cursor, @@ -199,36 +187,32 @@ export const PtyHandler = HttpApiBuilder.group(Api, "server.pty", (handlers) => .pipe( Effect.catchTags({ "Pty.NotFoundError": () => - closeAccepted(new Socket.CloseEvent(4404, "session not found")).pipe(Effect.as(undefined)), + Effect.sync(() => { + Queue.offerUnsafe(outbox, new Socket.CloseEvent(4404, "session not found")) + }), "Pty.ExitedError": () => - closeAccepted(new Socket.CloseEvent(4404, "session exited")).pipe(Effect.as(undefined)), + Effect.sync(() => { + Queue.offerUnsafe(outbox, new Socket.CloseEvent(4404, "session exited")) + }), }), ) - if (!attachment) return HttpServerResponse.empty() + if (attachment) { + for (const chunk of PtyProtocol.chunks(attachment.replay)) Queue.offerUnsafe(outbox, chunk) + Queue.offerUnsafe(outbox, PtyProtocol.metaFrame(attachment.cursor)) + attachment.activate() + } - for (const chunk of PtyProtocol.chunks(attachment.replay)) Queue.offerUnsafe(outbox, chunk) - Queue.offerUnsafe(outbox, PtyProtocol.metaFrame(attachment.cursor)) - attachment.activate() - - const drain = Effect.gen(function* () { - while (true) { - const item = yield* Queue.take(outbox) - yield* write(item) - if (item instanceof Socket.CloseEvent) return - } + yield* runPtySocket({ + socket, + outbox, + onMessage: (message) => + Effect.sync(() => { + if (!attachment) return + const decoded = PtyProtocol.decodeInput(message) + if (decoded !== undefined) attachment.write(decoded) + }), + detach: () => attachment?.detach(), }) - - yield* runPtySocket( - drain, - socket.runRaw((message) => { - const decoded = PtyProtocol.decodeInput(message) - if (decoded !== undefined) attachment.write(decoded) - }), - attachment.detach, - ).pipe( - Effect.catchReason("SocketError", "SocketCloseError", () => Effect.void), - Effect.orDie, - ) return HttpServerResponse.empty() }).pipe(Effect.provide(locations.get(ref)), locationErrors) }), diff --git a/packages/server/src/handlers/reference.ts b/packages/server/src/handlers/reference.ts index 3fb20bc328c1..aae02c9b1c43 100644 --- a/packages/server/src/handlers/reference.ts +++ b/packages/server/src/handlers/reference.ts @@ -1,5 +1,5 @@ import { Reference } from "@opencode/core/reference" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" import { response } from "../location" diff --git a/packages/server/src/handlers/rpc.ts b/packages/server/src/handlers/rpc.ts index a0bd96a28e75..ded5610ae618 100644 --- a/packages/server/src/handlers/rpc.ts +++ b/packages/server/src/handlers/rpc.ts @@ -2,7 +2,7 @@ import { Rpc } from "@opencode/core/rpc" import { Plugin } from "@opencode/core/plugin" import { RpcError, RpcInternalError } from "@opencode/protocol/errors" import { Effect } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" export const RpcHandler = HttpApiBuilder.group(Api, "server.rpc", (handlers) => diff --git a/packages/server/src/handlers/server.ts b/packages/server/src/handlers/server.ts index 81a933789d31..d85594b38d42 100644 --- a/packages/server/src/handlers/server.ts +++ b/packages/server/src/handlers/server.ts @@ -1,6 +1,6 @@ import { Duration, Effect } from "effect" -import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpServerRequest, HttpServerResponse } from "effect/http" +import { HttpApiBuilder } from "effect/http-api" import { PersistentPty } from "@opencode/core/persistent-pty" import { UnauthorizedError } from "@opencode/protocol/errors" import { Api } from "../api" diff --git a/packages/server/src/handlers/session.ts b/packages/server/src/handlers/session.ts index 73a076335209..dd85c2f4ff51 100644 --- a/packages/server/src/handlers/session.ts +++ b/packages/server/src/handlers/session.ts @@ -5,7 +5,7 @@ import { SessionTransfer } from "@opencode/core/session/transfer" import { InstructionEntry } from "@opencode/core/session/instruction-entry" import { Form } from "@opencode/core/form" import { DateTime, Effect, Stream } from "effect" -import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" +import { HttpApiBuilder, HttpApiSchema } from "effect/http-api" import { Api } from "../api" import { SessionsCursor } from "@opencode/protocol/groups/session" import { diff --git a/packages/server/src/handlers/shell.ts b/packages/server/src/handlers/shell.ts index 0beeafc279a7..6b08faf80753 100644 --- a/packages/server/src/handlers/shell.ts +++ b/packages/server/src/handlers/shell.ts @@ -1,7 +1,7 @@ import { Shell } from "@opencode/core/shell" import { Location } from "@opencode/core/location" import { Effect } from "effect" -import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" +import { HttpApiBuilder, HttpApiSchema } from "effect/http-api" import { ShellNotFoundError } from "@opencode/protocol/errors" import { Api } from "../api" import { response } from "../location" diff --git a/packages/server/src/handlers/skill.ts b/packages/server/src/handlers/skill.ts index a0cba465d33a..39039fcd638b 100644 --- a/packages/server/src/handlers/skill.ts +++ b/packages/server/src/handlers/skill.ts @@ -1,5 +1,5 @@ import { Skill } from "@opencode/core/skill" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" import { response } from "../location" diff --git a/packages/server/src/handlers/vcs.ts b/packages/server/src/handlers/vcs.ts index 623134755cda..ec5b3176765d 100644 --- a/packages/server/src/handlers/vcs.ts +++ b/packages/server/src/handlers/vcs.ts @@ -8,7 +8,7 @@ import { LocationServiceMap } from "@opencode/core/location-services" import { ConflictError, InvalidRequestError, ServiceUnavailableError } from "@opencode/protocol/errors" import { VcsInitNotSupportedError } from "@opencode/protocol/groups/vcs" import { Effect } from "effect" -import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" +import { HttpApiBuilder, HttpApiSchema } from "effect/http-api" import { Api } from "../api" import { response } from "../location" diff --git a/packages/server/src/handlers/websearch.ts b/packages/server/src/handlers/websearch.ts index bb838a6173cc..71808b86e206 100644 --- a/packages/server/src/handlers/websearch.ts +++ b/packages/server/src/handlers/websearch.ts @@ -1,7 +1,7 @@ import { WebSearch } from "@opencode/core/websearch" import { InvalidRequestError, ServiceUnavailableError } from "@opencode/protocol/errors" import { Effect } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/http-api" import { Api } from "../api" import { response } from "../location" diff --git a/packages/server/src/handlers/worktree.ts b/packages/server/src/handlers/worktree.ts index c90b57e608e3..dc6867da86c1 100644 --- a/packages/server/src/handlers/worktree.ts +++ b/packages/server/src/handlers/worktree.ts @@ -4,7 +4,7 @@ import { Project } from "@opencode/core/project" import { ProjectNotFoundError } from "@opencode/protocol/errors" import { WorktreeError } from "@opencode/protocol/groups/worktree" import { Effect } from "effect" -import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" +import { HttpApiBuilder, HttpApiSchema } from "effect/http-api" import { Api } from "../api" export const WorktreeHandler = HttpApiBuilder.group(Api, "server.worktree", (handlers) => diff --git a/packages/server/src/location.ts b/packages/server/src/location.ts index 7553fa9adc6f..c37979e94138 100644 --- a/packages/server/src/location.ts +++ b/packages/server/src/location.ts @@ -5,8 +5,8 @@ import { AbsolutePath } from "@opencode/core/schema" import { Session } from "@opencode/core/session" import { LocationNotFoundError, InvalidRequestError } from "@opencode/protocol/errors" import { Effect, Layer, Schema } from "effect" -import { HttpServerRequest } from "effect/unstable/http" -import { HttpApiMiddleware } from "effect/unstable/httpapi" +import { HttpServerRequest } from "effect/http" +import { HttpApiMiddleware } from "effect/http-api" import { missingSession } from "./handlers/session-error" export type LocationServices = Layer.Success> diff --git a/packages/server/src/middleware/authorization.ts b/packages/server/src/middleware/authorization.ts index 84906429d776..7e995c79059e 100644 --- a/packages/server/src/middleware/authorization.ts +++ b/packages/server/src/middleware/authorization.ts @@ -5,8 +5,9 @@ export { Authorization } from "@opencode/protocol/middleware/authorization" import { hasPtyConnectTicketURL } from "@opencode/protocol/groups/pty" import { hasPersistentPtyConnectTicketURL } from "@opencode/protocol/groups/persistent-pty" import { isPairingConnectURL } from "@opencode/protocol/groups/server" -import { Effect, Encoding, Layer, Redacted } from "effect" -import { HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" +import { Effect, Layer, Redacted } from "effect" +import { Base64 } from "effect/encoding" +import { HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/http" const AUTH_TOKEN_QUERY = "auth_token" const WWW_AUTHENTICATE = 'Basic realm="Secure Area"' @@ -16,7 +17,7 @@ function emptyCredential() { } function decodeCredential(input: string) { - return Effect.fromResult(Encoding.decodeBase64String(input)).pipe( + return Effect.fromResult(Base64.decodeString(input)).pipe( Effect.match({ onFailure: emptyCredential, onSuccess: (header) => { diff --git a/packages/server/src/middleware/form-location.ts b/packages/server/src/middleware/form-location.ts index f622c9317da4..b975d1aacfb1 100644 --- a/packages/server/src/middleware/form-location.ts +++ b/packages/server/src/middleware/form-location.ts @@ -3,8 +3,8 @@ import { LocationServiceMap } from "@opencode/core/location-services" import { Session } from "@opencode/core/session" import { LocationNotFoundError, InvalidRequestError, SessionNotFoundError } from "@opencode/protocol/errors" import { Effect, Layer } from "effect" -import { HttpRouter, HttpServerRequest } from "effect/unstable/http" -import { HttpApiMiddleware } from "effect/unstable/httpapi" +import { HttpRouter, HttpServerRequest } from "effect/http" +import { HttpApiMiddleware } from "effect/http-api" import { locationErrors, requestRef, sessionInfo, type LocationServices } from "../location" export class FormLocationMiddleware extends HttpApiMiddleware.Service< diff --git a/packages/server/src/middleware/schema-error.ts b/packages/server/src/middleware/schema-error.ts index 6762cd461618..36a4ee7af9df 100644 --- a/packages/server/src/middleware/schema-error.ts +++ b/packages/server/src/middleware/schema-error.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { HttpApiMiddleware } from "effect/unstable/httpapi" +import { HttpApiMiddleware } from "effect/http-api" import { InvalidRequestError } from "@opencode/protocol/errors" import { SchemaErrorMiddleware } from "@opencode/protocol/middleware/schema-error" export { SchemaErrorMiddleware } from "@opencode/protocol/middleware/schema-error" diff --git a/packages/server/src/middleware/session-location.ts b/packages/server/src/middleware/session-location.ts index 341ff73e6067..3de912a04d25 100644 --- a/packages/server/src/middleware/session-location.ts +++ b/packages/server/src/middleware/session-location.ts @@ -1,8 +1,8 @@ import { Instance } from "@opencode/core/instance/service" import { Session } from "@opencode/core/session" import { Effect, Layer } from "effect" -import { HttpRouter } from "effect/unstable/http" -import { HttpApiMiddleware } from "effect/unstable/httpapi" +import { HttpRouter } from "effect/http" +import { HttpApiMiddleware } from "effect/http-api" import { LocationNotFoundError, InvalidRequestError, SessionNotFoundError } from "@opencode/protocol/errors" import { locationErrors, sessionInfo, type LocationServices } from "../location" diff --git a/packages/server/src/process.ts b/packages/server/src/process.ts index 890307d28b28..d7f79a7e655e 100644 --- a/packages/server/src/process.ts +++ b/packages/server/src/process.ts @@ -9,7 +9,8 @@ import { hasPersistentPtyConnectTicketURL } from "@opencode/protocol/groups/pers import { isPairingConnectURL } from "@opencode/protocol/groups/server" import { Global } from "@opencode/util/global" import { Cause, Context, Effect, Exit, Latch, Layer, Option, Ref, Scope } from "effect" -import { HttpMiddleware, HttpRouter, HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" +import { HttpMiddleware, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http" +import { NetAddress } from "effect/net" import { createServer } from "node:http" import { ServerAuth } from "./auth" import { isAllowedCorsOrigin } from "./cors" @@ -22,7 +23,7 @@ import type { ServerOptions } from "./options" export interface Lifecycle { readonly onListen: ( - address: HttpServer.Address, + address: NetAddress.SocketAddress, shutdown: Effect.Effect, ) => Effect.Effect, E, R> } diff --git a/packages/server/src/routes.ts b/packages/server/src/routes.ts index f8e6da476ff2..5a5b953684a9 100644 --- a/packages/server/src/routes.ts +++ b/packages/server/src/routes.ts @@ -32,8 +32,8 @@ import { SdkPlugins } from "@opencode/core/plugin/sdk" import { WellKnown } from "@opencode/core/wellknown" import { Workspace } from "@opencode/core/workspace" import { Watcher } from "@opencode/core/filesystem/watcher" -import { HttpRouter } from "effect/unstable/http" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpRouter } from "effect/http" +import { HttpApiBuilder } from "effect/http-api" import { Context, Effect, Layer, Option } from "effect" import { Api } from "./api" import { ServerAuth } from "./auth" diff --git a/packages/server/test/fixture/server.ts b/packages/server/test/fixture/server.ts index 8eeeece1860b..0a13c02fead4 100644 --- a/packages/server/test/fixture/server.ts +++ b/packages/server/test/fixture/server.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { HttpServer } from "effect/unstable/http" +import { HttpServer } from "effect/http" import { ServerProcess } from "../../src/process" export const startServer = Effect.fnUntraced(function* (directory: string) { diff --git a/packages/server/test/persistent-pty.test.ts b/packages/server/test/persistent-pty.test.ts index 67d5270f54a9..f8346456cc87 100644 --- a/packages/server/test/persistent-pty.test.ts +++ b/packages/server/test/persistent-pty.test.ts @@ -3,16 +3,18 @@ import fs from "node:fs/promises" import os from "node:os" import path from "node:path" import { expect } from "bun:test" +import { binaryPath } from "@opencode-ai/pty" import { PersistentPty } from "@opencode/schema/persistent-pty" import { Session } from "@opencode/schema/session" import { Effect, Exit, Schema, Scope } from "effect" -import { HttpServer } from "effect/unstable/http" +import { HttpServer } from "effect/http" import { OpenCode } from "../../client/src/promise/index" import { it } from "../../core/test/lib/effect" import { ServerProcess } from "../src/process" -const binary = process.env.OPENCODE_PTY_BIN ?? "/root/projects/opencode-pty/target/debug/opencode-pty" -const smoke = existsSync(binary) ? it.live : it.live.skip +// opencode-pty does not support Windows, and its package resolves no binary on other unsupported platforms. +const binary = process.platform === "win32" ? undefined : (process.env.OPENCODE_PTY_BIN ?? binaryPath) +const smoke = binary !== undefined && existsSync(binary) ? it.live : it.live.skip smoke( "reads the latest controlled terminal with optional physical line counts through the SDK", @@ -190,14 +192,15 @@ smoke( const first = Schema.decodeUnknownSync(PersistentPty.Info)( (yield* request(base, "POST", `/api/experimental/session/${sessionID}/terminal`, { command: "/usr/bin/env", - args: ["/bin/sh", "-c", "stty -echo; printf terminal-one; cat"], + // Exercise transport bursts without macOS's 1024-byte canonical line buffer limit. + args: ["/bin/sh", "-c", "stty -echo -icanon min 1 time 0; printf terminal-one; cat"], cwd: process.cwd(), title: "first", env: {}, })).data, ) expect(first.command).toBe("/usr/bin/env") - expect(first.args).toEqual(["/bin/sh", "-c", "stty -echo; printf terminal-one; cat"]) + expect(first.args).toEqual(["/bin/sh", "-c", "stty -echo -icanon min 1 time 0; printf terminal-one; cat"]) expect(first.cwd).toBe(process.cwd()) expect(yield* Effect.promise(() => events.next("persistent-pty.added"))).toMatchObject({ data: { sessionID, terminal: { id: first.id } }, @@ -636,7 +639,13 @@ async function verifySharedControl(base: string, ptyID: string) { second.socket.send(inputFrame(70, 20, "from-second\n")) await waitForSocketOutput([first, second], "from-second") - await waitForForegroundProcess([first, second], "cat") + // opencode-pty v0.1.13 uses /proc on Linux and returns None on other platforms: + // https://github.com/anomalyco/opencode-pty/blob/v0.1.13/src/service.rs#L1091-L1128 + if (process.platform === "linux") await waitForForegroundProcess([first, second], "cat") + if (process.platform !== "linux") { + expect(first.foregroundProcess).toBeNull() + expect(second.foregroundProcess).toBeNull() + } for (const character of "printf abc | rev\n") second.socket.send(inputFrame(70, 20, character)) await waitForSocketOutput([first, second], "printf abc | rev") @@ -644,12 +653,36 @@ async function verifySharedControl(base: string, ptyID: string) { second.socket.send(inputFrame(70, 20, "x".repeat(1024))) second.socket.send(inputFrame(70, 20, "after-burst\n")) await waitForSocketOutput([first, second], "after-burst") + expect(first.output).toContain("x".repeat(1024) + "after-burst") + expect(second.output).toContain("x".repeat(1024) + "after-burst") expect(first.closed).toBeFalse() expect(second.closed).toBeFalse() expect(first.resizes).toBeGreaterThan(0) expect(second.resizes).toBeGreaterThan(0) expect(first.output).not.toContain("\0") expect(second.output).not.toContain("\0") + expect(first.size).toEqual({ cols: 70, rows: 20 }) + expect(second.size).toEqual({ cols: 70, rows: 20 }) + + await waitForController(first, "second") + second.socket.close() + // Promotion is emitted by the daemon only after the server detaches the closed socket. + await waitForController(first, "first") + expect(second.closed).toBeTrue() + first.socket.send(inputFrame(70, 20, "after-detach\n")) + await waitForSocketOutput([first], "after-detach") + + const replay = await openTerminalSocket(base, ptyID, "replay", "observer") + try { + // openTerminalSocket resolves at replay_complete, before any new input is sent. + expect(replay.output).toContain("from-first") + expect(replay.output).toContain("from-second") + expect(replay.output).toContain("after-detach") + first.socket.send(inputFrame(70, 20, "after-replay\n")) + await waitForSocketOutput([first, replay], "after-replay") + } finally { + replay.socket.close() + } } finally { first.socket.close() second.socket.close() @@ -681,6 +714,8 @@ async function openTerminalSocket( output: "", closed: false, resizes: 0, + size: undefined as { cols: number; rows: number } | undefined, + controller: undefined as string | undefined, foregroundProcess: null as string | null, } state.socket.binaryType = "arraybuffer" @@ -702,6 +737,12 @@ async function openTerminalSocket( return } state.resizes++ + if (typeof message.cols === "number" && typeof message.rows === "number") + state.size = { cols: message.cols, rows: message.rows } + return + } + if (message.type === "controller_changed") { + state.controller = typeof message.attachmentID === "string" ? message.attachmentID : undefined return } if (message.type === "foreground_process_changed") { @@ -734,6 +775,15 @@ async function openTerminalSocket( return state } +async function waitForController(socket: { controller: string | undefined; closed: boolean }, expected: string) { + for (let attempt = 0; attempt < 100; attempt++) { + if (socket.controller === expected) return + if (socket.closed) throw new Error("Persistent PTY observer disconnected") + await Bun.sleep(20) + } + throw new Error(`Persistent PTY controller did not become ${expected}: ${socket.controller}`) +} + async function waitForForegroundProcess( sockets: Array<{ foregroundProcess: string | null; closed: boolean }>, expected: string, diff --git a/packages/server/test/process.test.ts b/packages/server/test/process.test.ts index 4163d7e19a94..20ada8bae073 100644 --- a/packages/server/test/process.test.ts +++ b/packages/server/test/process.test.ts @@ -1,6 +1,6 @@ import { expect } from "bun:test" import { Effect } from "effect" -import { HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" +import { HttpServer, HttpServerRequest, HttpServerResponse } from "effect/http" import { it } from "../../core/test/lib/effect" import { ServerProcess } from "../src/process" diff --git a/packages/server/test/provider.test.ts b/packages/server/test/provider.test.ts index 77d2cea8c526..9da592289ee8 100644 --- a/packages/server/test/provider.test.ts +++ b/packages/server/test/provider.test.ts @@ -2,7 +2,7 @@ import { expect } from "bun:test" import { SdkPlugins } from "@opencode/core/plugin/sdk" import { Plugin } from "@opencode/plugin/effect" import { Context, Deferred, Effect, Fiber, Layer } from "effect" -import { HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http" +import { HttpEffect, HttpRouter, HttpServer } from "effect/http" import { tmpdirScoped } from "../../core/test/fixture/tmpdir" import { it } from "../../core/test/lib/effect" import { createRoutes } from "../src/routes" diff --git a/packages/server/test/pty-socket.property.test.ts b/packages/server/test/pty-socket.property.test.ts new file mode 100644 index 000000000000..dd7428306fed --- /dev/null +++ b/packages/server/test/pty-socket.property.test.ts @@ -0,0 +1,240 @@ +import { expect, test } from "bun:test" +import { Cause, Effect, Fiber, Queue } from "effect" +import { Socket } from "effect/socket" +import FastCheck from "fast-check" +import { type Outbound, runPtySocket } from "../src/handlers/pty-socket" + +// Latencies are mostly scheduler yields so interleavings stay dense and fast; a few are real sleeps. +type Latency = { readonly yields: number } | { readonly millis: number } + +type Step = + | { readonly type: "inbound"; readonly message: string | Uint8Array } + | { readonly type: "outbound"; readonly frame: string | Uint8Array } + | { readonly type: "serverClose"; readonly code: number } + | { readonly type: "clientClose"; readonly code: number } + | { readonly type: "readError" } + | { readonly type: "writeError" } + | { readonly type: "interrupt" } + +type Scenario = { + readonly replay: ReadonlyArray + readonly steps: ReadonlyArray<{ readonly step: Step; readonly delay: Latency }> + readonly reader: Latency + readonly open: Latency | undefined + readonly message: ReadonlyArray + readonly write: ReadonlyArray + readonly batch: number +} + +type Terminal = "close" | "readError" | "writeError" | "interrupt" + +type Entry = + | { readonly type: "reader" | "openStart" | "openEnd" | "detach" } + | { readonly type: "messageStart" | "messageEnd"; readonly message: string | Uint8Array } + | { readonly type: "write"; readonly chunk: Outbound } + | { readonly type: "terminal"; readonly terminal: Terminal } + +const latency: FastCheck.Arbitrary = FastCheck.oneof( + { weight: 12, arbitrary: FastCheck.record({ yields: FastCheck.nat(6) }) }, + { weight: 1, arbitrary: FastCheck.record({ millis: FastCheck.integer({ min: 1, max: 3 }) }) }, +) +const payload = FastCheck.oneof(FastCheck.string({ maxLength: 8 }), FastCheck.uint8Array({ maxLength: 8 })) +const code = FastCheck.constantFrom(1000, 1001, 1006, 4404) +const step: FastCheck.Arbitrary = FastCheck.oneof( + { weight: 8, arbitrary: FastCheck.record({ type: FastCheck.constant("inbound"), message: payload }) }, + { weight: 8, arbitrary: FastCheck.record({ type: FastCheck.constant("outbound"), frame: payload }) }, + { weight: 1, arbitrary: FastCheck.record({ type: FastCheck.constant("serverClose"), code }) }, + { weight: 1, arbitrary: FastCheck.record({ type: FastCheck.constant("clientClose"), code }) }, + { weight: 1, arbitrary: FastCheck.record({ type: FastCheck.constant("readError") }) }, + { weight: 1, arbitrary: FastCheck.record({ type: FastCheck.constant("writeError") }) }, + { weight: 1, arbitrary: FastCheck.record({ type: FastCheck.constant("interrupt") }) }, +) +const scenario: FastCheck.Arbitrary = FastCheck.record({ + replay: FastCheck.array(payload, { maxLength: 4 }), + steps: FastCheck.array(FastCheck.record({ step, delay: latency }), { maxLength: 24 }), + reader: latency, + open: FastCheck.option(latency, { nil: undefined }), + message: FastCheck.array(latency, { minLength: 1, maxLength: 4 }), + write: FastCheck.array(latency, { minLength: 1, maxLength: 4 }), + batch: FastCheck.integer({ min: 1, max: 3 }), +}) + +const wait = (value: Latency) => + Effect.gen(function* () { + if ("millis" in value) return yield* Effect.sleep(value.millis) + for (let i = 0; i < value.yields; i++) yield* Effect.yieldNow + }) + +const nonEmpty = (values: ReadonlyArray): values is readonly [A, ...Array] => values.length > 0 + +const socketError = (reason: Socket.SocketError["reason"]) => new Socket.SocketError({ reason }) + +// Drives one connection through the scenario against an in-memory socket and records every observable event. +const simulate = (input: Scenario) => + Effect.gen(function* () { + const inbound = yield* Queue.unbounded() + const outbox = yield* Queue.unbounded() + const log: Entry[] = [] + const sent: Array = [] + const offered: Outbound[] = [...input.replay] + const state = { failNextWrite: false, writes: 0, messages: 0, detached: 0 } + yield* Queue.offerAll(outbox, input.replay) + + const socket = Socket.make({ + reader: wait(input.reader).pipe( + Effect.tap(() => Effect.sync(() => log.push({ type: "reader" }))), + Effect.as({ + pull: Queue.takeBetween(inbound, 1, input.batch).pipe( + Effect.tapError((error) => + Effect.sync(() => + log.push({ + type: "terminal", + terminal: error.reason._tag === "SocketCloseError" ? "close" : "readError", + }), + ), + ), + Effect.flatMap((batch) => + nonEmpty(batch) ? Effect.succeed(batch) : Effect.die(new Error("empty batch")), + ), + ), + upgrade: Socket.SocketUpgradeError.unsupported, + }), + ), + writer: Effect.succeed({ + write: (chunk: Outbound) => + Effect.gen(function* () { + yield* wait(input.write[state.writes++ % input.write.length]!) + if (state.failNextWrite) { + state.failNextWrite = false + log.push({ type: "terminal", terminal: "writeError" }) + return yield* socketError(new Socket.SocketWriteError({ cause: new Error("write failed") })) + } + log.push({ type: "write", chunk }) + if (chunk instanceof Socket.CloseEvent) log.push({ type: "terminal", terminal: "close" }) + }), + writeAll: () => Effect.die(new Error("runPtySocket writes frames one at a time")), + }), + }) + + const open = input.open + const run = yield* runPtySocket({ + socket, + outbox, + onOpen: + open && + Effect.gen(function* () { + log.push({ type: "openStart" }) + yield* wait(open) + log.push({ type: "openEnd" }) + }), + onMessage: (message) => + Effect.gen(function* () { + log.push({ type: "messageStart", message }) + yield* wait(input.message[state.messages++ % input.message.length]!) + log.push({ type: "messageEnd", message }) + }), + detach: () => { + state.detached++ + log.push({ type: "detach" }) + }, + }).pipe(Effect.scoped, Effect.forkChild({ startImmediately: true })) + + for (const { step, delay } of input.steps) { + yield* wait(delay) + if (step.type === "inbound" && (yield* Queue.offer(inbound, step.message))) sent.push(step.message) + if (step.type === "outbound") { + offered.push(step.frame) + yield* Queue.offer(outbox, step.frame) + } + if (step.type === "serverClose") { + const close = new Socket.CloseEvent(step.code) + offered.push(close) + yield* Queue.offer(outbox, close) + } + if (step.type === "clientClose") + yield* Queue.fail(inbound, socketError(new Socket.SocketCloseError({ code: step.code }))) + if (step.type === "readError") + yield* Queue.fail(inbound, socketError(new Socket.SocketReadError({ cause: new Error("reset") }))) + if (step.type === "writeError") state.failNextWrite = true + if (step.type === "interrupt") { + log.push({ type: "terminal", terminal: "interrupt" }) + // Fiber.interrupt waits for the run to finish, so a run that ignores interruption would stall here. + if ((yield* Fiber.interrupt(run).pipe(Effect.timeoutOption("2 seconds")))._tag === "None") + return yield* Effect.die(new Error("runPtySocket ignored interruption")) + } + } + // Every real connection eventually closes; this bounds runs whose scenario never ended them. + yield* Queue.fail(inbound, socketError(new Socket.SocketCloseError({ code: 1000 }))) + const exit = yield* Fiber.await(run).pipe(Effect.timeoutOption("5 seconds")) + return { exit, log, sent, offered, state, hasOpen: open !== undefined } + }) + +function verify(result: Effect.Success>) { + // The run terminates within the scheduler bound. + if (result.exit._tag === "None") throw new Error("runPtySocket did not terminate") + const exit = result.exit.value + const log = result.log + const index = (predicate: (entry: Entry) => boolean) => log.findIndex(predicate) + const detach = index((entry) => entry.type === "detach") + + // detach runs exactly once, whatever ended the connection. + expect(result.state.detached).toBe(1) + + // Outbound frames are written in queue order and nothing follows a close frame or detach. + const written = log.flatMap((entry) => (entry.type === "write" ? [entry.chunk] : [])) + expect(written.every((chunk, position) => chunk === result.offered[position])).toBeTrue() + const close = written.findIndex((chunk) => chunk instanceof Socket.CloseEvent) + if (close !== -1) expect(close).toBe(written.length - 1) + expect(log.slice(detach).some((entry) => entry.type === "write")).toBeFalse() + + // Inbound messages reach onMessage one at a time, in order, after onOpen and before detach. + const started = log.flatMap((entry) => (entry.type === "messageStart" ? [entry.message] : [])) + expect(started).toEqual(result.sent.slice(0, started.length)) + const reader = index((entry) => entry.type === "reader") + const openStart = index((entry) => entry.type === "openStart") + const openEnd = index((entry) => entry.type === "openEnd") + if (openStart !== -1) expect(openStart).toBeGreaterThan(reader) + const firstMessage = index((entry) => entry.type === "messageStart") + if (firstMessage !== -1) { + expect(firstMessage).toBeGreaterThan(reader) + if (result.hasOpen) expect(firstMessage).toBeGreaterThan(openEnd) + expect(firstMessage).toBeLessThan(detach) + } + expect(log.slice(detach).some((entry) => entry.type === "messageStart")).toBeFalse() + let inFlight = 0 + for (const entry of log) { + if (entry.type === "messageStart") inFlight++ + if (entry.type === "messageEnd") inFlight-- + expect(inFlight).toBeLessThanOrEqual(1) + } + + // Clean closes succeed, socket failures die, and interruption is honored. The outcome follows the first + // close or failure the run observed; an interrupt that lands while the race settles may still win, but + // nothing observed after an interrupt may decide the outcome. + const terminals = log.flatMap((entry) => (entry.type === "terminal" ? [entry.terminal] : [])) + if (exit._tag === "Failure" && Cause.hasInterrupts(exit.cause)) { + expect(terminals).toContain("interrupt") + return + } + const first = terminals[0] + if (first === "close") { + expect(exit._tag).toBe("Success") + return + } + if (first === "interrupt" || first === undefined) throw new Error(`Interrupt was not honored: ${exit._tag}`) + if (exit._tag !== "Failure" || !Cause.hasDies(exit.cause)) + throw new Error(`Expected the run to die after ${first}, got ${exit._tag}`) + const defect = Cause.squash(exit.cause) + if (!Socket.isSocketError(defect)) throw new Error(`Expected a socket error defect, got ${String(defect)}`) + expect(defect.reason._tag).toBe(first === "readError" ? "SocketReadError" : "SocketWriteError") +} + +test( + "runPtySocket preserves ordering, detaches once, and ends correctly under random interleavings", + () => + FastCheck.assert( + FastCheck.asyncProperty(scenario, async (input) => verify(await Effect.runPromise(simulate(input)))), + { numRuns: 300 }, + ), + 60_000, +) diff --git a/packages/server/test/pty-socket.test.ts b/packages/server/test/pty-socket.test.ts index 405ff2c5d4e9..71b9708d482c 100644 --- a/packages/server/test/pty-socket.test.ts +++ b/packages/server/test/pty-socket.test.ts @@ -1,16 +1,89 @@ import { expect } from "bun:test" -import { Effect, Option, Result } from "effect" +import { Effect, Exit, Fiber, Queue } from "effect" +import { Socket } from "effect/socket" import { it } from "../../core/test/lib/effect" -import { runPtySocket } from "../src/handlers/pty-socket" +import { type Outbound, runPtySocket } from "../src/handlers/pty-socket" + +const connection = Effect.gen(function* () { + const inbound = yield* Queue.unbounded() + const outbox = yield* Queue.unbounded() + const state = { + opened: false, + detached: false, + received: [] as Array, + written: [] as Outbound[], + } + const socket = Socket.make({ + reader: Effect.succeed({ + pull: Queue.take(inbound).pipe(Effect.map((message) => [message] as const)), + upgrade: Socket.SocketUpgradeError.unsupported, + }), + writer: Effect.succeed({ + write: (chunk) => Effect.sync(() => void state.written.push(chunk)), + writeAll: (chunks) => Effect.sync(() => void state.written.push(...chunks)), + }), + }) + const run = runPtySocket({ + socket, + outbox, + onOpen: Effect.sync(() => { + state.opened = true + }), + onMessage: (message) => + Effect.sync(() => { + expect(state.opened).toBeTrue() + state.received.push(message) + }), + detach: () => { + state.detached = true + }, + }) + return { inbound, outbox, state, run } +}) + +const fail = (reason: Socket.SocketError["reason"]) => new Socket.SocketError({ reason }) + +it.live("delivers input in order and detaches when the client closes", () => + Effect.gen(function* () { + const pty = yield* connection + yield* Queue.offerAll(pty.inbound, ["a", "b"]) + yield* Queue.fail(pty.inbound, fail(new Socket.SocketCloseError({ code: 1000 }))) + + expect(yield* Effect.exit(pty.run)).toEqual(Exit.void) + expect(pty.state.received).toEqual(["a", "b"]) + expect(pty.state.detached).toBeTrue() + }), +) + +it.live("writes queued output through the close frame and detaches", () => + Effect.gen(function* () { + const pty = yield* connection + const close = new Socket.CloseEvent(1000) + yield* Queue.offerAll(pty.outbox, ["output", close, "after close"]) + + expect(yield* Effect.exit(pty.run)).toEqual(Exit.void) + expect(pty.state.written).toEqual(["output", close]) + expect(pty.state.detached).toBeTrue() + }), +) it.live("detaches when the socket fails while the outbox drain is blocked", () => Effect.gen(function* () { - const state = { detached: false } - const result = yield* runPtySocket(Effect.never, Effect.fail("socket closed"), () => { - state.detached = true - }).pipe(Effect.result, Effect.timeoutOption("100 millis")) + const pty = yield* connection + yield* Queue.fail(pty.inbound, fail(new Socket.SocketReadError({ cause: new Error("reset") }))) - expect(Option.isSome(result) && Result.isFailure(result.value)).toBeTrue() - expect(state.detached).toBeTrue() + expect(Exit.hasDies(yield* Effect.exit(pty.run))).toBeTrue() + expect(pty.state.detached).toBeTrue() + }), +) + +it.live("detaches when interrupted", () => + Effect.gen(function* () { + const pty = yield* connection + const fiber = yield* Effect.forkChild(pty.run, { startImmediately: true }) + yield* Fiber.interrupt(fiber) + + expect(pty.state.opened).toBeTrue() + expect(pty.state.detached).toBeTrue() }), ) diff --git a/packages/server/test/pty-transport.test.ts b/packages/server/test/pty-transport.test.ts new file mode 100644 index 000000000000..67974f5a0c18 --- /dev/null +++ b/packages/server/test/pty-transport.test.ts @@ -0,0 +1,137 @@ +import { expect } from "bun:test" +import { Effect } from "effect" +import { HttpServer } from "effect/http" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { OpenCode } from "../../client/src/promise/index" +import { it } from "../../core/test/lib/effect" +import { ServerProcess } from "../src/process" + +const live = process.platform === "win32" ? it.live.skip : it.live + +live( + "PTY websocket preserves replay, text and binary input, resize, reconnect, and exit", + () => + Effect.gen(function* () { + const directory = yield* Effect.acquireRelease( + Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "opencode-pty-transport-"))), + (directory) => Effect.promise(() => fs.rm(directory, { recursive: true, force: true })), + ) + const server = yield* ServerProcess.start({ + hostname: "127.0.0.1", + port: 0, + password: "secret", + app: { version: "test-version" }, + database: { path: ":memory:" }, + models: { fetch: false }, + config: { directory, project: false, content: "{}" }, + fs: { filewatcher: false }, + }) + const base = HttpServer.formatAddress(server.address) + const client = OpenCode.make({ baseUrl: base, headers: { authorization: `Basic ${btoa("opencode:secret")}` } }) + const location = { directory } + const terminal = yield* Effect.acquireRelease( + Effect.promise(() => + client.pty.create({ + location, + command: "/bin/sh", + args: [ + "-c", + "stty -echo; printf 'ready\\n'; while IFS= read -r line; do case \"$line\" in size) stty size;; quit) exit 0;; *) printf 'reply:%s\\n' \"$line\";; esac; done", + ], + cwd: directory, + }), + ), + (terminal) => Effect.promise(() => client.pty.remove({ location, ptyID: terminal.data.id })), + ) + yield* Effect.promise(async () => { + const open = async () => { + const token = await client.pty.connect.token({ location, ptyID: terminal.data.id, "x-opencode-ticket": "1" }) + const url = new URL(`/api/pty/${terminal.data.id}/connect`, base) + url.protocol = "ws:" + url.searchParams.set("location[directory]", directory) + url.searchParams.set("ticket", token.data.ticket) + const socket = new WebSocket(url) + socket.binaryType = "arraybuffer" + const state = { output: "", frames: [] as string[], closed: false, code: 0, reason: "", error: false } + socket.addEventListener("message", (event) => { + if (typeof event.data === "string") { + state.output += event.data + state.frames.push("output") + return + } + const bytes = new Uint8Array(event.data) + if (bytes[0] === 0) { + state.frames.push("cursor") + return + } + state.output += new TextDecoder().decode(bytes) + state.frames.push("output") + }) + socket.addEventListener("close", (event) => { + state.closed = true + state.code = event.code + state.reason = event.reason + }) + socket.addEventListener("error", () => { + state.error = true + }) + return { socket, state } + } + const connect = async () => { + const connection = await open() + await waitFor( + () => connection.state.frames.includes("cursor") || connection.state.closed || connection.state.error, + ) + expect(connection.state.error).toBeFalse() + expect(connection.state.closed).toBeFalse() + return connection + } + const first = await connect() + try { + await waitFor(() => first.state.output.includes("ready")) + first.socket.send("text\n") + first.socket.send(new TextEncoder().encode("binary\n")) + await waitFor(() => first.state.output.includes("reply:binary")) + expect(first.state.output).toContain("reply:text") + await client.pty.update({ location, ptyID: terminal.data.id, size: { rows: 31, cols: 93 } }) + first.socket.send("size\n") + await waitFor(() => first.state.output.includes("31 93")) + first.socket.close() + await waitFor(() => first.state.closed) + const second = await connect() + try { + expect(second.state.output).toContain("reply:text") + expect(second.state.output).toContain("reply:binary") + expect(second.state.frames.at(-1)).toBe("cursor") + second.socket.send("reconnected\n") + await waitFor(() => second.state.output.includes("reply:reconnected")) + expect(first.state.output).not.toContain("reply:reconnected") + second.socket.send("quit\n") + await waitFor(() => second.state.closed) + expect(second.state.code).toBe(1000) + expect((await client.pty.get({ location, ptyID: terminal.data.id })).data.status).toBe("exited") + const exited = await open() + await waitFor(() => exited.state.closed) + expect(exited.state.code).toBe(4404) + expect(exited.state.reason).toBe("session exited") + expect(exited.state.frames).toEqual([]) + } finally { + second.socket.close() + } + } finally { + first.socket.close() + } + }) + }), + 20_000, +) + +async function waitFor(predicate: () => boolean) { + for (let attempt = 0; attempt < 200; attempt++) { + if (predicate()) return + await Bun.sleep(20) + } + throw new Error("PTY transport did not reach the expected state") +} diff --git a/packages/server/test/request-tracing.test.ts b/packages/server/test/request-tracing.test.ts index 94a37f759a78..3854a11b5e5d 100644 --- a/packages/server/test/request-tracing.test.ts +++ b/packages/server/test/request-tracing.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test" import { Effect, Option, Scope, Tracer } from "effect" -import { HttpMiddleware, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" +import { HttpMiddleware, HttpServerRequest, HttpServerResponse } from "effect/http" import { withoutParentSpan } from "../src/request-tracing" test("requests ignore ambient parents and continue inbound trace context", async () => { diff --git a/packages/server/test/rpc-handler-errors.test.ts b/packages/server/test/rpc-handler-errors.test.ts index c99377ef67ed..6b141d0e25bd 100644 --- a/packages/server/test/rpc-handler-errors.test.ts +++ b/packages/server/test/rpc-handler-errors.test.ts @@ -3,7 +3,7 @@ import { SdkPlugins } from "@opencode/core/plugin/sdk" import { define } from "@opencode/plugin/effect/plugin" import { Rpc } from "@opencode/schema/rpc" import { Context, Effect, Layer, Schema } from "effect" -import { HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http" +import { HttpEffect, HttpRouter, HttpServer } from "effect/http" import { tmpdirScoped } from "../../core/test/fixture/tmpdir" import { it } from "../../core/test/lib/effect" import { createEmbeddedRoutes } from "../src/routes" diff --git a/packages/server/test/session-instances.test.ts b/packages/server/test/session-instances.test.ts index 0cea57e4aff4..8ddc3601f94b 100644 --- a/packages/server/test/session-instances.test.ts +++ b/packages/server/test/session-instances.test.ts @@ -21,7 +21,7 @@ import { LayerNode } from "@opencode/util/effect/layer-node" import { makeGlobalNode } from "@opencode/util/effect/app-node" import { Global } from "@opencode/util/global" import { Context, Duration, Effect, Layer, LayerMap, RcMap, Schema } from "effect" -import { HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http" +import { HttpEffect, HttpRouter, HttpServer } from "effect/http" import { LanguageModel, LLMClient } from "../../ai/src" import { OpenAIChat } from "../../ai/src/protocols/openai-chat" import { TestLLM } from "../../ai/src/testing" diff --git a/packages/server/test/sse-failure.test.ts b/packages/server/test/sse-failure.test.ts new file mode 100644 index 000000000000..df5c19ef5e88 --- /dev/null +++ b/packages/server/test/sse-failure.test.ts @@ -0,0 +1,71 @@ +import { expect } from "bun:test" +import { OpenCode } from "@opencode/client/effect" +import { ClientApi } from "@opencode/protocol/client" +import { Authorization } from "@opencode/protocol/middleware/authorization" +import { Event } from "@opencode/schema/event" +import { Session } from "@opencode/schema/session" +import { Cause, Effect, Exit, Layer, Stream } from "effect" +import { HttpClient, HttpClientResponse, HttpRouter, HttpServer } from "effect/http" +import { HttpApi, HttpApiBuilder, HttpApiGroup } from "effect/http-api" +import { it } from "../../core/test/lib/effect" +import { schemaErrorLayer } from "../src/middleware/schema-error" + +// Serve the real protocol declaration for the session log stream, so the server encoder and the +// generated Effect client agree on the reserved SSE failure event for this Effect version. +const LogApi = HttpApi.make("log").add( + HttpApiGroup.make("server.session").add(ClientApi.groups["server.session"].endpoints["session.log"]), +) +const synced = { type: "log.synced" as const, aggregateID: "ses_test", seq: Event.Seq.make(1) } + +const handler = HttpRouter.toWebHandler( + HttpApiBuilder.layer(LogApi).pipe( + Layer.provide( + HttpApiBuilder.group(LogApi, "server.session", (handlers) => + handlers.handle("session.log", () => + Effect.succeed(Stream.make(synced).pipe(Stream.concat(Stream.die(new Error("log reader failed"))))), + ), + ), + ), + Layer.provide(schemaErrorLayer), + Layer.provide( + Layer.succeed( + Authorization, + Authorization.of((effect) => effect), + ), + ), + Layer.provide(HttpServer.layerServices), + ), +).handler + +const fetchLog = (request: Request) => Effect.promise(() => handler(request)) + +it.live("encodes a mid-stream session log failure as the reserved SSE failure event", () => + Effect.gen(function* () { + const response = yield* fetchLog(new Request("http://opencode.local/api/experimental/session/ses_test/log")) + const body = yield* Effect.promise(() => response.text()) + expect(response.headers.get("content-type")).toContain("text/event-stream") + expect(body).toContain("event: effect/http-api/stream/failure") + }), +) + +it.live("the generated Effect client fails the session log stream with the server's defect", () => + Effect.gen(function* () { + const httpClient = HttpClient.make((request) => + fetchLog(new Request(request.url, { method: request.method })).pipe( + Effect.map((response) => HttpClientResponse.fromWeb(request, response)), + ), + ) + const received: unknown[] = [] + const exit = yield* Effect.gen(function* () { + const client = yield* OpenCode.make({ baseUrl: "http://opencode.local" }) + return yield* client.session + .log({ sessionID: Session.ID.make("ses_test") }) + .pipe(Stream.runForEach((item) => Effect.sync(() => received.push(item)))) + }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.exit) + + expect(received).toEqual([synced]) + expect(Exit.isFailure(exit) && Cause.hasDies(exit.cause)).toBe(true) + if (Exit.isSuccess(exit)) return + expect(Cause.squash(exit.cause)).toMatchObject({ message: "log reader failed" }) + }), +) diff --git a/packages/simulation/src/backend/index.ts b/packages/simulation/src/backend/index.ts index a3ec0f61dbea..00d4fe1bd7fa 100644 --- a/packages/simulation/src/backend/index.ts +++ b/packages/simulation/src/backend/index.ts @@ -3,7 +3,7 @@ import { makeGlobalNode } from "@opencode/util/effect/app-node" import { httpClient } from "@opencode/util/effect/app-node-platform" import { SdkPlugins } from "@opencode/core/plugin/sdk" import { Config, Effect, FileSystem, Layer } from "effect" -import { HttpClient } from "effect/unstable/http" +import { HttpClient } from "effect/http" import { DriveManifest } from "../manifest" import { SimulationNetwork } from "./network" import { SimulationOpenAI } from "./openai" @@ -28,7 +28,7 @@ export const simulationReplacements: (app: { // ModelsDev dies when its catalog fetch fails, so simulation answers it with // an empty catalog; providers come from seeded config instead. const models = SimulationNetwork.json("GET", "https://models.opencode.ai/api.json", {}) - const drive = yield* Config.string("OPENCODE_DRIVE").pipe(Config.withDefault(undefined)) + const drive = yield* Config.String("OPENCODE_DRIVE").pipe(Config.withDefault(undefined)) if (!drive) return [httpClient.replace(SimulationNetwork.layer([models]))] satisfies LayerNode.Replacements const manifest = yield* DriveManifest.resolve() diff --git a/packages/simulation/src/backend/network.ts b/packages/simulation/src/backend/network.ts index f91ec8246ceb..52aacd8e4d08 100644 --- a/packages/simulation/src/backend/network.ts +++ b/packages/simulation/src/backend/network.ts @@ -1,7 +1,7 @@ import { Clock, Effect, Layer, Ref } from "effect" -import { HttpClient, HttpClientResponse, type HttpMethod } from "effect/unstable/http" -import { HttpClientError, TransportError } from "effect/unstable/http/HttpClientError" -import type { HttpClientRequest } from "effect/unstable/http" +import { HttpClient, HttpClientResponse, type HttpMethod } from "effect/http" +import { HttpClientError, TransportError } from "effect/http/HttpClientError" +import type { HttpClientRequest } from "effect/http" import { SimulationProtocol } from "../protocol" /** diff --git a/packages/simulation/src/backend/openai.ts b/packages/simulation/src/backend/openai.ts index ccf67d716555..2ed835aac60d 100644 --- a/packages/simulation/src/backend/openai.ts +++ b/packages/simulation/src/backend/openai.ts @@ -1,6 +1,6 @@ import { Effect, Schema, Stream } from "effect" -import { HttpClientResponse } from "effect/unstable/http" -import { HttpClientError, TransportError } from "effect/unstable/http/HttpClientError" +import { HttpClientResponse } from "effect/http" +import { HttpClientError, TransportError } from "effect/http/HttpClientError" import { OpenAIChatEvent, DEFAULT_BASE_URL, PATH } from "@opencode/ai/protocols/openai-chat" import { SimulationNetwork } from "./network" import { SimulatedProvider } from "./simulated-provider" diff --git a/packages/simulation/src/frontend/actions.ts b/packages/simulation/src/frontend/actions.ts index 82823519be85..da1c94bc1cf5 100644 --- a/packages/simulation/src/frontend/actions.ts +++ b/packages/simulation/src/frontend/actions.ts @@ -7,7 +7,8 @@ import { type MockInput, type MockMouse, } from "@opentui/core/testing" -import { Effect, Encoding, Schema } from "effect" +import { Effect, Schema } from "effect" +import { Base64 } from "effect/encoding" import { SimulationProtocol } from "../protocol" import { SimulationRenderer } from "./renderer" import { SimulationSemantics } from "./semantics" @@ -206,7 +207,7 @@ function captureImages(renderer: CliRenderer): SimulationProtocol.Frontend.Captu height: fitted.height, pixelWidth: raw.width, pixelHeight: raw.height, - rgba: Encoding.encodeBase64(rgba), + rgba: Base64.encode(rgba), }] } finally { extracted?.dispose() diff --git a/packages/simulation/src/frontend/simulation.ts b/packages/simulation/src/frontend/simulation.ts index 0f077fb2b8b2..0b67f429f604 100644 --- a/packages/simulation/src/frontend/simulation.ts +++ b/packages/simulation/src/frontend/simulation.ts @@ -7,7 +7,7 @@ import { SimulationServer } from "./server" /** Drive-mode renderer and control-server acquisition. */ export const create = Effect.fn("Drive.create")(function* (options: CliRendererConfig, version: string) { - const headless = (yield* Config.string("OPENCODE_DRIVE_RENDERER").pipe(Config.withDefault("visible"))) === "headless" + const headless = (yield* Config.String("OPENCODE_DRIVE_RENDERER").pipe(Config.withDefault("visible"))) === "headless" const manifest = yield* DriveManifest.resolve() const renderer = headless ? yield* SimulationRenderer.create(options, manifest.recording?.timeline, manifest.viewport) diff --git a/packages/simulation/src/manifest.ts b/packages/simulation/src/manifest.ts index 1b48590db91c..25884594ffb4 100644 --- a/packages/simulation/src/manifest.ts +++ b/packages/simulation/src/manifest.ts @@ -70,11 +70,11 @@ export const resolve = Effect.fn("DriveManifest.resolve")(function* () { const name = yield* Config.schema(InstanceName, "OPENCODE_DRIVE").pipe(Effect.mapError(configError)) if (name === "1") return defaults - const state = yield* Config.string("XDG_STATE_HOME").pipe( + const state = yield* Config.String("XDG_STATE_HOME").pipe( Config.withDefault(join(homedir(), ".local", "state")), Effect.mapError(configError), ) - const directory = yield* Config.string("DRIVE_REGISTRY_DIR").pipe( + const directory = yield* Config.String("DRIVE_REGISTRY_DIR").pipe( Config.withDefault(join(state, "opencode-drive", "instances")), Effect.mapError(configError), ) diff --git a/packages/simulation/test/network.test.ts b/packages/simulation/test/network.test.ts index 9049b5700ca8..863048e1ff73 100644 --- a/packages/simulation/test/network.test.ts +++ b/packages/simulation/test/network.test.ts @@ -1,7 +1,7 @@ import { expect, test } from "bun:test" import { Effect, Exit } from "effect" import { TestClock } from "effect/testing" -import { HttpClientRequest } from "effect/unstable/http" +import { HttpClientRequest } from "effect/http" import { SimulationNetwork } from "../src/backend/network" test("keeps routes and request logs local to each network", async () => { diff --git a/packages/simulation/test/openai.test.ts b/packages/simulation/test/openai.test.ts index 6b5fd2f5b7d5..b500b5a6af11 100644 --- a/packages/simulation/test/openai.test.ts +++ b/packages/simulation/test/openai.test.ts @@ -1,8 +1,8 @@ import { expect, test } from "bun:test" import { DEFAULT_BASE_URL, PATH } from "@opencode/ai/protocols/openai-chat" import { Effect, Stream } from "effect" -import { HttpClientRequest } from "effect/unstable/http" -import { HttpClientError } from "effect/unstable/http/HttpClientError" +import { HttpClientRequest } from "effect/http" +import { HttpClientError } from "effect/http/HttpClientError" import { SimulationOpenAI } from "../src/backend/openai" import { SimulatedProvider } from "../src/backend/simulated-provider" diff --git a/packages/stats/core/src/config.ts b/packages/stats/core/src/config.ts index fb5a4de35290..f6aa6d63323e 100644 --- a/packages/stats/core/src/config.ts +++ b/packages/stats/core/src/config.ts @@ -11,7 +11,7 @@ const decodeAppConfigValue = Schema.decodeUnknownSync(AppConfigValue) const config = Config.all({ stage: Config.succeed(Resource.App.stage), - publicUrl: Config.string("PUBLIC_URL").pipe(Config.withDefault("http://localhost:3000")), + publicUrl: Config.String("PUBLIC_URL").pipe(Config.withDefault("http://localhost:3000")), }).pipe(Config.map(decodeAppConfigValue)) export class AppConfig extends Context.Service()("@opencode/stats/AppConfig") { diff --git a/packages/stats/core/src/database.ts b/packages/stats/core/src/database.ts index 8821714d7bdb..c8df5bd402f5 100644 --- a/packages/stats/core/src/database.ts +++ b/packages/stats/core/src/database.ts @@ -16,8 +16,8 @@ export class DatabaseSettings extends Schema.Class("DatabaseSe const decodeDatabaseSettings = Schema.decodeUnknownSync(DatabaseSettings) const config = Config.all({ - url: Config.nonEmptyString("DATABASE_URL").pipe(Config.withDefault(Resource.StatsDatabase.url)), - migrationsDir: Config.nonEmptyString("DATABASE_MIGRATIONS_DIR").pipe(Config.withDefault("./migrations")), + url: Config.NonEmptyString("DATABASE_URL").pipe(Config.withDefault(Resource.StatsDatabase.url)), + migrationsDir: Config.NonEmptyString("DATABASE_MIGRATIONS_DIR").pipe(Config.withDefault("./migrations")), }).pipe(Config.map(decodeDatabaseSettings)) export class DatabaseConfig extends Context.Service()( diff --git a/packages/stats/server/src/router.ts b/packages/stats/server/src/router.ts index e951118079ea..0fea1f2efa67 100644 --- a/packages/stats/server/src/router.ts +++ b/packages/stats/server/src/router.ts @@ -2,7 +2,7 @@ import { Buffer } from "node:buffer" import { timingSafeEqual } from "node:crypto" import { Effect, Schema } from "effect" import * as Semaphore from "effect/Semaphore" -import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" +import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http" import { Resource } from "sst/resource" import { Ingest } from "./ingest" import { isShuttingDown } from "./shutdown" diff --git a/packages/stats/server/src/server.ts b/packages/stats/server/src/server.ts index 95104bc45eee..424c5fe8cf36 100644 --- a/packages/stats/server/src/server.ts +++ b/packages/stats/server/src/server.ts @@ -1,7 +1,7 @@ import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer" import * as NodeRuntime from "@effect/platform-node/NodeRuntime" import { Config, Layer } from "effect" -import { HttpRouter } from "effect/unstable/http" +import { HttpRouter } from "effect/http" import { createServer } from "node:http" import { Ingest } from "./ingest" import { Routes } from "./router" @@ -12,8 +12,8 @@ registerShutdownSignalHandlers() const ServerLive = NodeHttpServer.layerConfig( () => createServer(), Config.all({ - port: Config.number("PORT").pipe(Config.withDefault(3000)), - host: Config.string("HOST").pipe(Config.withDefault("0.0.0.0")), + port: Config.Number("PORT").pipe(Config.withDefault(3000)), + host: Config.String("HOST").pipe(Config.withDefault("0.0.0.0")), }), ) diff --git a/packages/util/src/cross-spawn-spawner.ts b/packages/util/src/cross-spawn-spawner.ts index ffd9071cb024..8df615c8fbd3 100644 --- a/packages/util/src/cross-spawn-spawner.ts +++ b/packages/util/src/cross-spawn-spawner.ts @@ -3,7 +3,7 @@ import * as NodeSink from "@effect/platform-node/NodeSink" import * as NodeStream from "@effect/platform-node/NodeStream" import { Deferred, Effect, Exit, FileSystem, Layer, Path, PlatformError, Predicate, Sink, Stream } from "effect" import type { Scope } from "effect" -import { ChildProcess } from "effect/unstable/process" +import { ChildProcess } from "effect/process" import { ChildProcessSpawner, ExitCode, @@ -11,7 +11,7 @@ import { makeHandle, ProcessId, type ChildProcessHandle, -} from "effect/unstable/process/ChildProcessSpawner" +} from "effect/process/ChildProcessSpawner" // ast-grep-ignore: no-star-import import * as NodeChildProcess from "node:child_process" import { PassThrough } from "node:stream" diff --git a/packages/util/src/effect/app-node-platform.ts b/packages/util/src/effect/app-node-platform.ts index a34f0447ddb2..ee9e462039f7 100644 --- a/packages/util/src/effect/app-node-platform.ts +++ b/packages/util/src/effect/app-node-platform.ts @@ -3,7 +3,7 @@ import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem" import * as NodePath from "@effect/platform-node/NodePath" import { FileSystem, Path } from "effect" -import { FetchHttpClient, HttpClient } from "effect/unstable/http" +import { FetchHttpClient, HttpClient } from "effect/http" import { makeGlobalNode } from "./app-node.js" export const filesystem = makeGlobalNode({ service: FileSystem.FileSystem, layer: NodeFileSystem.layer, deps: [] }) diff --git a/packages/util/src/observability.ts b/packages/util/src/observability.ts index e518fbeccf99..78be301eeaf4 100644 --- a/packages/util/src/observability.ts +++ b/packages/util/src/observability.ts @@ -3,8 +3,8 @@ export * as Observability from "./observability.js" import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem" import { LayerNode } from "./effect/layer-node.js" import { Effect, Layer, Logger, References, Schema } from "effect" -import { FetchHttpClient } from "effect/unstable/http" -import { OtlpExporter, OtlpSerialization } from "effect/unstable/observability" +import { FetchHttpClient } from "effect/http" +import { OtlpExporter, OtlpSerialization } from "effect/observability" import { Logging } from "./observability/logging.js" import { Otlp } from "./observability/otlp.js" diff --git a/packages/util/src/observability/logging.ts b/packages/util/src/observability/logging.ts index 1dd56270fa37..d942afae6bc2 100644 --- a/packages/util/src/observability/logging.ts +++ b/packages/util/src/observability/logging.ts @@ -111,7 +111,7 @@ export const trim = Effect.fn("Logging.trim")(function* ( if (size <= max) return const handle = yield* fs.open(target, { flag: "r+" }) const start = yield* lineStart(handle, size - keep) - yield* handle.seek(0, "start") + yield* handle.seek(0n, "start") // Reads run to the current EOF so lines appended since the stat survive; the write cursor always // trails the read cursor so the forward copy never overwrites unread bytes. const written = yield* fs.stream(target, { offset: start, chunkSize: LOG_TRIM_CHUNK }).pipe( @@ -146,7 +146,7 @@ function lineStart(handle: FileSystem.File, from: number) { return Effect.gen(function* () { let cursor = from while (true) { - yield* handle.seek(cursor, "start") + yield* handle.seek(BigInt(cursor), "start") const chunk = yield* handle.readAlloc(LOG_TRIM_CHUNK) if (Option.isNone(chunk)) return cursor const newline = chunk.value.indexOf(10) diff --git a/packages/util/src/observability/otlp.ts b/packages/util/src/observability/otlp.ts index a72a5a85bcbf..2fa76f114a3a 100644 --- a/packages/util/src/observability/otlp.ts +++ b/packages/util/src/observability/otlp.ts @@ -1,5 +1,5 @@ import { Effect, Layer, Scope } from "effect" -import { OtlpLogger } from "effect/unstable/observability" +import { OtlpLogger } from "effect/observability" import { runID } from "./shared.js" export interface Options { diff --git a/packages/util/src/process.ts b/packages/util/src/process.ts index 88758b07b952..e971a9e62c5b 100644 --- a/packages/util/src/process.ts +++ b/packages/util/src/process.ts @@ -1,7 +1,7 @@ import { Context, Duration, Effect, Fiber, Layer, Schema, Stream } from "effect" import type { PlatformError } from "effect/PlatformError" -import { ChildProcess } from "effect/unstable/process" -import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" +import { ChildProcess } from "effect/process" +import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner" import { CrossSpawnSpawner } from "./cross-spawn-spawner.js" import { makeGlobalNode } from "./effect/app-node.js" diff --git a/services/www/openapi.json b/services/www/openapi.json index 90fca5531982..1323650788f0 100644 --- a/services/www/openapi.json +++ b/services/www/openapi.json @@ -131,14 +131,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - }, - { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - ] + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" } } } @@ -215,6 +208,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Resolve the requested location or the server default location.", @@ -346,6 +349,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve currently registered agents.", @@ -439,11 +452,18 @@ } }, "404": { - "description": "AgentNotFoundError", + "description": "AgentNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AgentNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/AgentNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -533,6 +553,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve enabled server plugins and their current status.", @@ -605,14 +635,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -626,6 +649,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Check one or all package plugins for available updates.", @@ -701,14 +734,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -723,6 +749,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -919,9 +955,6 @@ { "$ref": "#/components/schemas/InvalidCursorErrorEncoded" }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, { "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } @@ -1203,14 +1236,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -1447,7 +1473,8 @@ "^ses": { "$ref": "#/components/schemas/SessionActive" } - } + }, + "additionalProperties": false } }, "required": ["data"], @@ -1642,7 +1669,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -1651,7 +1678,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -1745,14 +1772,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -1768,19 +1788,19 @@ } }, "404": { - "description": "SessionNotFoundError | MessageNotFoundError", + "description": "SessionNotFoundError | MessageNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/MessageNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -1857,7 +1877,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -1866,7 +1886,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -1936,7 +1956,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -1945,7 +1965,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -1999,14 +2019,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -2102,14 +2115,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -2125,7 +2131,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2134,7 +2140,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2266,19 +2272,19 @@ } }, "404": { - "description": "SessionNotFoundError | CommandNotFoundError", + "description": "SessionNotFoundError | CommandNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/CommandNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2389,19 +2395,19 @@ } }, "404": { - "description": "SessionNotFoundError | SkillNotFoundError", + "description": "SessionNotFoundError | SkillNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/SkillNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2495,7 +2501,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2504,7 +2510,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2628,7 +2634,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2637,7 +2643,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2732,7 +2738,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2741,7 +2747,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2838,7 +2844,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2847,7 +2853,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2924,7 +2930,7 @@ } }, "404": { - "description": "MessageNotFoundError | SessionNotFoundError", + "description": "MessageNotFoundError | SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2936,7 +2942,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3037,7 +3043,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3046,7 +3052,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3118,7 +3124,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3127,7 +3133,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3322,14 +3328,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -3345,7 +3344,7 @@ } }, "404": { - "description": "MessageNotFoundError | SessionNotFoundError", + "description": "MessageNotFoundError | SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3355,6 +3354,9 @@ }, { "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3659,7 +3661,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3668,7 +3670,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3729,7 +3731,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3738,7 +3740,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3822,7 +3824,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3831,7 +3833,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3891,7 +3893,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3900,7 +3902,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4011,7 +4013,7 @@ "$ref": "#/components/schemas/SessionLogItemEncoded" } }, - "required": ["id", "event", "data"], + "required": ["event", "data"], "additionalProperties": false }, "x-effect-stream": { @@ -4073,7 +4075,7 @@ "errorSchema": { "not": {} }, - "failureEvent": "effect/httpapi/stream/failure" + "failureEvent": "effect/http-api/stream/failure" } } } @@ -4177,7 +4179,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -4186,7 +4188,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4239,7 +4241,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -4248,7 +4250,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4402,7 +4404,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -4411,7 +4413,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4459,14 +4461,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -4482,7 +4477,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -4491,7 +4486,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4586,19 +4581,19 @@ } }, "404": { - "description": "SessionNotFoundError | FormNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/FormNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4672,19 +4667,19 @@ } }, "404": { - "description": "SessionNotFoundError | FormNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/FormNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4762,19 +4757,19 @@ } }, "404": { - "description": "SessionNotFoundError | FormNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/FormNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -5190,6 +5185,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -5290,6 +5295,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -5327,14 +5342,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -5349,6 +5357,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -5472,6 +5490,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -5574,11 +5602,18 @@ } }, "404": { - "description": "ProviderNotFoundError", + "description": "ProviderNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProviderNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -5678,6 +5713,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve available integrations and their authentication methods.", @@ -5771,11 +5816,18 @@ } }, "404": { - "description": "IntegrationNotFoundError", + "description": "IntegrationNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -5831,14 +5883,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -5852,6 +5897,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Discover and persist an experimental wellknown integration source.", @@ -5929,14 +5984,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -5952,11 +6000,18 @@ } }, "404": { - "description": "IntegrationNotFoundError", + "description": "IntegrationNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -6003,6 +6058,134 @@ } } }, + "/api/integration/{integrationID}/connect/external": { + "post": { + "tags": ["integration"], + "operationId": "integration.connect.external", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "IntegrationNotFoundError | LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] + } + } + } + } + }, + "description": "Run an external authentication method and store a reference to its credential source.", + "summary": "Connect with external credentials", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "methodID": { + "type": "string" + }, + "answer": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.Answer" + }, + { + "type": "null" + } + ] + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["methodID"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, "/api/integration/{integrationID}/connect/oauth": { "post": { "tags": ["integration"], @@ -6074,14 +6257,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -6095,6 +6271,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Start an OAuth attempt and return the authorization details.", @@ -6233,7 +6419,7 @@ } }, "404": { - "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError", + "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -6243,6 +6429,9 @@ }, { "$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -6328,6 +6517,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Cancel an OAuth attempt and release its resources.", @@ -6396,14 +6595,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -6419,7 +6611,7 @@ } }, "404": { - "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError", + "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -6429,6 +6621,9 @@ }, { "$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -6534,14 +6729,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -6557,7 +6745,7 @@ } }, "404": { - "description": "IntegrationNotFoundError | IntegrationMethodNotFoundError", + "description": "IntegrationNotFoundError | IntegrationMethodNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -6567,6 +6755,9 @@ }, { "$ref": "#/components/schemas/IntegrationMethodNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -6700,7 +6891,7 @@ } }, "404": { - "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError", + "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -6710,6 +6901,9 @@ }, { "$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -6795,6 +6989,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Cancel a command authentication attempt and terminate its process.", @@ -6881,6 +7085,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve configured MCP servers and their connection status.", @@ -6955,6 +7169,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Add an MCP server at runtime or replace an existing one, connecting it immediately.", @@ -7053,11 +7277,18 @@ } }, "404": { - "description": "McpServerNotFoundError", + "description": "McpServerNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -7137,11 +7368,18 @@ } }, "404": { - "description": "McpServerNotFoundError", + "description": "McpServerNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -7221,11 +7459,18 @@ } }, "404": { - "description": "McpServerNotFoundError", + "description": "McpServerNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -7312,6 +7557,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve resources and resource templates from connected MCP servers.", @@ -7623,6 +7878,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List known projects.", @@ -7676,11 +7941,18 @@ } }, "404": { - "description": "ProjectNotFoundError", + "description": "ProjectNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProjectNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -7795,6 +8067,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve pending forms for a location.", @@ -7881,6 +8163,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve pending permission requests for a location.", @@ -7949,6 +8241,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve saved permissions, optionally filtered by project.", @@ -7993,6 +8295,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Remove a saved permission by ID.", @@ -8065,7 +8377,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -8074,7 +8386,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -8198,11 +8510,18 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -8276,19 +8595,19 @@ } }, "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", + "description": "SessionNotFoundError | PermissionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/PermissionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -8350,19 +8669,19 @@ } }, "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", + "description": "SessionNotFoundError | PermissionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/PermissionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -8471,11 +8790,18 @@ } }, "404": { - "description": "FileNotFoundError", + "description": "FileNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FileNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/FileNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -8583,11 +8909,18 @@ } }, "404": { - "description": "FileNotFoundError", + "description": "FileNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FileNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/FileNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -8709,6 +9042,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Find recursively ranked filesystem entries relative to the requested location.", @@ -8801,6 +9144,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Write the raw request body to an absolute path or a path relative to the requested location, creating parent directories, and return the resolved absolute path. Unlike read, the target is not confined to the location. Experimental: may change without compatibility guarantees.", @@ -8898,6 +9251,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve currently registered commands.", @@ -8984,6 +9347,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve currently registered skills.", @@ -9081,6 +9454,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "500": { "description": "RpcInternalError", "content": { @@ -9137,7 +9520,7 @@ "$ref": "#/components/schemas/V2EventEncoded" } }, - "required": ["id", "event", "data"], + "required": ["event", "data"], "additionalProperties": false }, "x-effect-stream": { @@ -9199,7 +9582,7 @@ "errorSchema": { "not": {} }, - "failureEvent": "effect/httpapi/stream/failure" + "failureEvent": "effect/http-api/stream/failure" } } } @@ -9309,6 +9692,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List PTY sessions for a location, including exited sessions retained until removal.", @@ -9390,6 +9783,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Create a pseudo-terminal session for a location.", @@ -9518,11 +9921,18 @@ } }, "404": { - "description": "PtyNotFoundError", + "description": "PtyNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -9618,11 +10028,18 @@ } }, "404": { - "description": "PtyNotFoundError", + "description": "PtyNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -9732,11 +10149,18 @@ } }, "404": { - "description": "PtyNotFoundError", + "description": "PtyNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -9859,11 +10283,18 @@ } }, "404": { - "description": "PtyNotFoundError", + "description": "PtyNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -9952,11 +10383,18 @@ } }, "404": { - "description": "PtyNotFoundError", + "description": "PtyNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -10100,14 +10538,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -10172,14 +10603,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -10931,6 +11355,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List currently running shell commands for a location. Exited commands are not included.", @@ -11012,6 +11446,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Spawn one non-interactive shell command for a location. Combined stdout/stderr is captured to a file pageable via output.", @@ -11133,11 +11577,18 @@ } }, "404": { - "description": "ShellNotFoundError", + "description": "ShellNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ShellNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/ShellNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -11214,6 +11665,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Terminate and remove one shell command and its retained output.", @@ -11344,11 +11805,18 @@ } }, "404": { - "description": "ShellNotFoundError", + "description": "ShellNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ShellNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/ShellNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -11438,6 +11906,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List references available in the requested location.", @@ -11762,14 +12240,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -11784,6 +12255,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "409": { "description": "ConflictError", "content": { @@ -11896,6 +12377,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Get current and default branch information for the requested location.", @@ -11987,6 +12478,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -12082,6 +12583,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List uncommitted working-copy changes relative to the requested location.", @@ -12195,6 +12706,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List local and remote branches available at the requested location.", @@ -12313,6 +12834,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -12629,6 +13160,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -12707,14 +13248,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -12729,6 +13263,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -12834,6 +13378,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Return configuration documents and discovery sources for the requested location, from lowest to highest priority.", @@ -12879,6 +13433,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Return shells available to terminal and agent execution.", @@ -12914,6 +13478,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Patch supported fields in the highest-precedence global configuration document.", @@ -13554,7 +14128,7 @@ "properties": { "action": { "type": "string", - "enum": ["provider.use", "permission"] + "enum": ["provider.use", "tool.use", "integration.use"] }, "resource": { "type": "string" @@ -14038,7 +14612,7 @@ }, "method": { "type": "string", - "enum": ["key", "oauth"] + "enum": ["key", "oauth", "external"] }, "status": { "$ref": "#/components/schemas/Connection.Status" @@ -14139,6 +14713,23 @@ "required": ["id", "integrationID", "label", "active", "value"], "additionalProperties": false }, + "Credential.External": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["external"] + }, + "methodID": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": ["type", "methodID"], + "additionalProperties": false + }, "Credential.Key": { "type": "object", "properties": { @@ -14193,6 +14784,9 @@ }, { "$ref": "#/components/schemas/Credential.Key" + }, + { + "$ref": "#/components/schemas/Credential.External" } ] }, @@ -14457,6 +15051,18 @@ "$ref": "#/components/schemas/Form.Field" } }, + "Form.Fields_3": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/Form.Field" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Form.Field" + } + }, "Form.Info": { "type": "object", "properties": { @@ -15432,6 +16038,26 @@ "required": ["type", "names"], "additionalProperties": false }, + "Integration.ExternalMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["external"] + }, + "label": { + "type": "string" + }, + "form": { + "$ref": "#/components/schemas/Form.Fields_3" + } + }, + "required": ["id", "type", "label"], + "additionalProperties": false + }, "Integration.Info": { "type": "object", "properties": { @@ -15488,6 +16114,9 @@ { "$ref": "#/components/schemas/Integration.KeyMethod" }, + { + "$ref": "#/components/schemas/Integration.ExternalMethod" + }, { "$ref": "#/components/schemas/Integration.EnvMethod" } @@ -15654,6 +16283,23 @@ "required": ["directory"], "additionalProperties": false }, + "LocationNotFoundErrorEncoded": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["LocationNotFoundError"] + }, + "location": { + "$ref": "#/components/schemas/Location.PublicRef" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "location", "message"], + "additionalProperties": false + }, "Mcp.LocalConfigEncoded": { "type": "object", "properties": { @@ -16031,6 +16677,12 @@ }, "supportsPromptCacheKey": { "type": "boolean" + }, + "supportsThinkingBlockBinding": { + "type": "boolean" + }, + "supportsEffortUpdates": { + "type": "boolean" } }, "additionalProperties": false @@ -17567,14 +18219,9 @@ "additionalProperties": false }, "Session.Inbox.CompactionPayload": { - "anyOf": [ - { - "type": "object" - }, - { - "type": "array" - } - ] + "not": { + "type": "null" + } }, "Session.Inbox.Delivery": { "type": "string", @@ -18288,6 +18935,9 @@ "outcome": { "type": "string", "enum": ["succeeded", "failed", "interrupted"] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" } }, "required": ["id", "time", "type", "outcome"], diff --git a/services/www/public/openapi.json b/services/www/public/openapi.json index 90fca5531982..1323650788f0 100644 --- a/services/www/public/openapi.json +++ b/services/www/public/openapi.json @@ -131,14 +131,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - }, - { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - ] + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" } } } @@ -215,6 +208,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Resolve the requested location or the server default location.", @@ -346,6 +349,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve currently registered agents.", @@ -439,11 +452,18 @@ } }, "404": { - "description": "AgentNotFoundError", + "description": "AgentNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AgentNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/AgentNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -533,6 +553,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve enabled server plugins and their current status.", @@ -605,14 +635,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -626,6 +649,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Check one or all package plugins for available updates.", @@ -701,14 +734,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -723,6 +749,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -919,9 +955,6 @@ { "$ref": "#/components/schemas/InvalidCursorErrorEncoded" }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, { "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } @@ -1203,14 +1236,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -1447,7 +1473,8 @@ "^ses": { "$ref": "#/components/schemas/SessionActive" } - } + }, + "additionalProperties": false } }, "required": ["data"], @@ -1642,7 +1669,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -1651,7 +1678,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -1745,14 +1772,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -1768,19 +1788,19 @@ } }, "404": { - "description": "SessionNotFoundError | MessageNotFoundError", + "description": "SessionNotFoundError | MessageNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/MessageNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -1857,7 +1877,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -1866,7 +1886,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -1936,7 +1956,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -1945,7 +1965,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -1999,14 +2019,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -2102,14 +2115,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -2125,7 +2131,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2134,7 +2140,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2266,19 +2272,19 @@ } }, "404": { - "description": "SessionNotFoundError | CommandNotFoundError", + "description": "SessionNotFoundError | CommandNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/CommandNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2389,19 +2395,19 @@ } }, "404": { - "description": "SessionNotFoundError | SkillNotFoundError", + "description": "SessionNotFoundError | SkillNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/SkillNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2495,7 +2501,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2504,7 +2510,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2628,7 +2634,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2637,7 +2643,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2732,7 +2738,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2741,7 +2747,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2838,7 +2844,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2847,7 +2853,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -2924,7 +2930,7 @@ } }, "404": { - "description": "MessageNotFoundError | SessionNotFoundError", + "description": "MessageNotFoundError | SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -2936,7 +2942,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3037,7 +3043,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3046,7 +3052,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3118,7 +3124,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3127,7 +3133,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3322,14 +3328,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -3345,7 +3344,7 @@ } }, "404": { - "description": "MessageNotFoundError | SessionNotFoundError", + "description": "MessageNotFoundError | SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3355,6 +3354,9 @@ }, { "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3659,7 +3661,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3668,7 +3670,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3729,7 +3731,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3738,7 +3740,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3822,7 +3824,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3831,7 +3833,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -3891,7 +3893,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -3900,7 +3902,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4011,7 +4013,7 @@ "$ref": "#/components/schemas/SessionLogItemEncoded" } }, - "required": ["id", "event", "data"], + "required": ["event", "data"], "additionalProperties": false }, "x-effect-stream": { @@ -4073,7 +4075,7 @@ "errorSchema": { "not": {} }, - "failureEvent": "effect/httpapi/stream/failure" + "failureEvent": "effect/http-api/stream/failure" } } } @@ -4177,7 +4179,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -4186,7 +4188,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4239,7 +4241,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -4248,7 +4250,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4402,7 +4404,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -4411,7 +4413,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4459,14 +4461,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -4482,7 +4477,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -4491,7 +4486,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4586,19 +4581,19 @@ } }, "404": { - "description": "SessionNotFoundError | FormNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/FormNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4672,19 +4667,19 @@ } }, "404": { - "description": "SessionNotFoundError | FormNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/FormNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -4762,19 +4757,19 @@ } }, "404": { - "description": "SessionNotFoundError | FormNotFoundError", + "description": "SessionNotFoundError | FormNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/FormNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -5190,6 +5185,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -5290,6 +5295,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -5327,14 +5342,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -5349,6 +5357,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -5472,6 +5490,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -5574,11 +5602,18 @@ } }, "404": { - "description": "ProviderNotFoundError", + "description": "ProviderNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProviderNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -5678,6 +5713,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve available integrations and their authentication methods.", @@ -5771,11 +5816,18 @@ } }, "404": { - "description": "IntegrationNotFoundError", + "description": "IntegrationNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -5831,14 +5883,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -5852,6 +5897,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Discover and persist an experimental wellknown integration source.", @@ -5929,14 +5984,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -5952,11 +6000,18 @@ } }, "404": { - "description": "IntegrationNotFoundError", + "description": "IntegrationNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -6003,6 +6058,134 @@ } } }, + "/api/integration/{integrationID}/connect/external": { + "post": { + "tags": ["integration"], + "operationId": "integration.connect.external", + "parameters": [ + { + "name": "integrationID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "IntegrationNotFoundError | LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] + } + } + } + } + }, + "description": "Run an external authentication method and store a reference to its credential source.", + "summary": "Connect with external credentials", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "methodID": { + "type": "string" + }, + "answer": { + "anyOf": [ + { + "$ref": "#/components/schemas/Form.Answer" + }, + { + "type": "null" + } + ] + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["methodID"], + "additionalProperties": false + } + } + }, + "required": true + } + } + }, "/api/integration/{integrationID}/connect/oauth": { "post": { "tags": ["integration"], @@ -6074,14 +6257,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -6095,6 +6271,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Start an OAuth attempt and return the authorization details.", @@ -6233,7 +6419,7 @@ } }, "404": { - "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError", + "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -6243,6 +6429,9 @@ }, { "$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -6328,6 +6517,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Cancel an OAuth attempt and release its resources.", @@ -6396,14 +6595,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -6419,7 +6611,7 @@ } }, "404": { - "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError", + "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -6429,6 +6621,9 @@ }, { "$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -6534,14 +6729,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -6557,7 +6745,7 @@ } }, "404": { - "description": "IntegrationNotFoundError | IntegrationMethodNotFoundError", + "description": "IntegrationNotFoundError | IntegrationMethodNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -6567,6 +6755,9 @@ }, { "$ref": "#/components/schemas/IntegrationMethodNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -6700,7 +6891,7 @@ } }, "404": { - "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError", + "description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -6710,6 +6901,9 @@ }, { "$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -6795,6 +6989,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Cancel a command authentication attempt and terminate its process.", @@ -6881,6 +7085,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve configured MCP servers and their connection status.", @@ -6955,6 +7169,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Add an MCP server at runtime or replace an existing one, connecting it immediately.", @@ -7053,11 +7277,18 @@ } }, "404": { - "description": "McpServerNotFoundError", + "description": "McpServerNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -7137,11 +7368,18 @@ } }, "404": { - "description": "McpServerNotFoundError", + "description": "McpServerNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -7221,11 +7459,18 @@ } }, "404": { - "description": "McpServerNotFoundError", + "description": "McpServerNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/McpServerNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -7312,6 +7557,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve resources and resource templates from connected MCP servers.", @@ -7623,6 +7878,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List known projects.", @@ -7676,11 +7941,18 @@ } }, "404": { - "description": "ProjectNotFoundError", + "description": "ProjectNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProjectNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/ProjectNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -7795,6 +8067,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve pending forms for a location.", @@ -7881,6 +8163,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve pending permission requests for a location.", @@ -7949,6 +8241,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve saved permissions, optionally filtered by project.", @@ -7993,6 +8295,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Remove a saved permission by ID.", @@ -8065,7 +8377,7 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { @@ -8074,7 +8386,7 @@ "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -8198,11 +8510,18 @@ } }, "404": { - "description": "SessionNotFoundError", + "description": "SessionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -8276,19 +8595,19 @@ } }, "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", + "description": "SessionNotFoundError | PermissionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/PermissionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -8350,19 +8669,19 @@ } }, "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", + "description": "SessionNotFoundError | PermissionNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, { "$ref": "#/components/schemas/PermissionNotFoundErrorEncoded" }, { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" } ] } @@ -8471,11 +8790,18 @@ } }, "404": { - "description": "FileNotFoundError", + "description": "FileNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FileNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/FileNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -8583,11 +8909,18 @@ } }, "404": { - "description": "FileNotFoundError", + "description": "FileNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FileNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/FileNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -8709,6 +9042,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Find recursively ranked filesystem entries relative to the requested location.", @@ -8801,6 +9144,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Write the raw request body to an absolute path or a path relative to the requested location, creating parent directories, and return the resolved absolute path. Unlike read, the target is not confined to the location. Experimental: may change without compatibility guarantees.", @@ -8898,6 +9251,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve currently registered commands.", @@ -8984,6 +9347,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Retrieve currently registered skills.", @@ -9081,6 +9454,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "500": { "description": "RpcInternalError", "content": { @@ -9137,7 +9520,7 @@ "$ref": "#/components/schemas/V2EventEncoded" } }, - "required": ["id", "event", "data"], + "required": ["event", "data"], "additionalProperties": false }, "x-effect-stream": { @@ -9199,7 +9582,7 @@ "errorSchema": { "not": {} }, - "failureEvent": "effect/httpapi/stream/failure" + "failureEvent": "effect/http-api/stream/failure" } } } @@ -9309,6 +9692,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List PTY sessions for a location, including exited sessions retained until removal.", @@ -9390,6 +9783,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Create a pseudo-terminal session for a location.", @@ -9518,11 +9921,18 @@ } }, "404": { - "description": "PtyNotFoundError", + "description": "PtyNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -9618,11 +10028,18 @@ } }, "404": { - "description": "PtyNotFoundError", + "description": "PtyNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -9732,11 +10149,18 @@ } }, "404": { - "description": "PtyNotFoundError", + "description": "PtyNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -9859,11 +10283,18 @@ } }, "404": { - "description": "PtyNotFoundError", + "description": "PtyNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -9952,11 +10383,18 @@ } }, "404": { - "description": "PtyNotFoundError", + "description": "PtyNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/PtyNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -10100,14 +10538,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -10172,14 +10603,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -10931,6 +11355,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List currently running shell commands for a location. Exited commands are not included.", @@ -11012,6 +11446,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Spawn one non-interactive shell command for a location. Combined stdout/stderr is captured to a file pageable via output.", @@ -11133,11 +11577,18 @@ } }, "404": { - "description": "ShellNotFoundError", + "description": "ShellNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ShellNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/ShellNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -11214,6 +11665,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Terminate and remove one shell command and its retained output.", @@ -11344,11 +11805,18 @@ } }, "404": { - "description": "ShellNotFoundError", + "description": "ShellNotFoundError | LocationNotFoundError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ShellNotFoundErrorEncoded" + "anyOf": [ + { + "$ref": "#/components/schemas/ShellNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + ] } } } @@ -11438,6 +11906,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List references available in the requested location.", @@ -11762,14 +12240,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -11784,6 +12255,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "409": { "description": "ConflictError", "content": { @@ -11896,6 +12377,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Get current and default branch information for the requested location.", @@ -11987,6 +12478,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -12082,6 +12583,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List uncommitted working-copy changes relative to the requested location.", @@ -12195,6 +12706,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "List local and remote branches available at the requested location.", @@ -12313,6 +12834,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -12629,6 +13160,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -12707,14 +13248,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" } } } @@ -12729,6 +13263,16 @@ } } }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } + }, "503": { "description": "ServiceUnavailableError", "content": { @@ -12834,6 +13378,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Return configuration documents and discovery sources for the requested location, from lowest to highest priority.", @@ -12879,6 +13433,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Return shells available to terminal and agent execution.", @@ -12914,6 +13478,16 @@ } } } + }, + "404": { + "description": "LocationNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationNotFoundErrorEncoded" + } + } + } } }, "description": "Patch supported fields in the highest-precedence global configuration document.", @@ -13554,7 +14128,7 @@ "properties": { "action": { "type": "string", - "enum": ["provider.use", "permission"] + "enum": ["provider.use", "tool.use", "integration.use"] }, "resource": { "type": "string" @@ -14038,7 +14612,7 @@ }, "method": { "type": "string", - "enum": ["key", "oauth"] + "enum": ["key", "oauth", "external"] }, "status": { "$ref": "#/components/schemas/Connection.Status" @@ -14139,6 +14713,23 @@ "required": ["id", "integrationID", "label", "active", "value"], "additionalProperties": false }, + "Credential.External": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["external"] + }, + "methodID": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": ["type", "methodID"], + "additionalProperties": false + }, "Credential.Key": { "type": "object", "properties": { @@ -14193,6 +14784,9 @@ }, { "$ref": "#/components/schemas/Credential.Key" + }, + { + "$ref": "#/components/schemas/Credential.External" } ] }, @@ -14457,6 +15051,18 @@ "$ref": "#/components/schemas/Form.Field" } }, + "Form.Fields_3": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/components/schemas/Form.Field" + } + ], + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Form.Field" + } + }, "Form.Info": { "type": "object", "properties": { @@ -15432,6 +16038,26 @@ "required": ["type", "names"], "additionalProperties": false }, + "Integration.ExternalMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["external"] + }, + "label": { + "type": "string" + }, + "form": { + "$ref": "#/components/schemas/Form.Fields_3" + } + }, + "required": ["id", "type", "label"], + "additionalProperties": false + }, "Integration.Info": { "type": "object", "properties": { @@ -15488,6 +16114,9 @@ { "$ref": "#/components/schemas/Integration.KeyMethod" }, + { + "$ref": "#/components/schemas/Integration.ExternalMethod" + }, { "$ref": "#/components/schemas/Integration.EnvMethod" } @@ -15654,6 +16283,23 @@ "required": ["directory"], "additionalProperties": false }, + "LocationNotFoundErrorEncoded": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["LocationNotFoundError"] + }, + "location": { + "$ref": "#/components/schemas/Location.PublicRef" + }, + "message": { + "type": "string" + } + }, + "required": ["_tag", "location", "message"], + "additionalProperties": false + }, "Mcp.LocalConfigEncoded": { "type": "object", "properties": { @@ -16031,6 +16677,12 @@ }, "supportsPromptCacheKey": { "type": "boolean" + }, + "supportsThinkingBlockBinding": { + "type": "boolean" + }, + "supportsEffortUpdates": { + "type": "boolean" } }, "additionalProperties": false @@ -17567,14 +18219,9 @@ "additionalProperties": false }, "Session.Inbox.CompactionPayload": { - "anyOf": [ - { - "type": "object" - }, - { - "type": "array" - } - ] + "not": { + "type": "null" + } }, "Session.Inbox.Delivery": { "type": "string", @@ -18288,6 +18935,9 @@ "outcome": { "type": "string", "enum": ["succeeded", "failed", "interrupted"] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" } }, "required": ["id", "time", "type", "outcome"], diff --git a/services/www/src/docs/content/build/client/effect.mdx b/services/www/src/docs/content/build/client/effect.mdx index 560030d72089..75b53998f2f6 100644 --- a/services/www/src/docs/content/build/client/effect.mdx +++ b/services/www/src/docs/content/build/client/effect.mdx @@ -16,7 +16,7 @@ Create a client with the server URL, then call methods grouped by API resource. ```ts import { AbsolutePath, Location, OpenCode } from "@opencode/client/effect" import { Effect } from "effect" -import { FetchHttpClient } from "effect/unstable/http" +import { FetchHttpClient } from "effect/http" const program = Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://localhost:4096" }) @@ -40,7 +40,7 @@ normal Effect interruption for cancellation. RPC methods additionally accept per-call location, header, and signal options. ```ts -import { HttpClient, HttpClientRequest } from "effect/unstable/http" +import { HttpClient, HttpClientRequest } from "effect/http" const httpClient = yield * HttpClient.HttpClient const client = @@ -123,7 +123,7 @@ import { NodeFileSystem } from "@effect/platform-node" import { OpenCode } from "@opencode/client/effect" import { Service } from "@opencode/client/effect/service" import { Effect } from "effect" -import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http" const program = Effect.gen(function* () { const endpoint = yield* Service.ensure() diff --git a/services/www/src/docs/content/build/plugins/effect.mdx b/services/www/src/docs/content/build/plugins/effect.mdx index 5f187ad0cd72..4769c3c6c4ac 100644 --- a/services/www/src/docs/content/build/plugins/effect.mdx +++ b/services/www/src/docs/content/build/plugins/effect.mdx @@ -550,7 +550,7 @@ Connect with a key or drive an OAuth attempt through the connected API. effect: (ctx) => Effect.gen(function* () { const integration = ctx.integration - const token = yield* Config.redacted("GITHUB_TOKEN").pipe(Effect.orDie) + const token = yield* Config.Redacted("GITHUB_TOKEN").pipe(Effect.orDie) yield* integration.connect .key({ integrationID: Integration.ID.make("github"), key: Redacted.value(token) }) .pipe(Effect.orDie) @@ -1517,7 +1517,7 @@ entrypoint and declare `effect` as a `peerDependency`. "@opencode/plugin": "latest" }, "peerDependencies": { - "effect": ">=4.0.0-rc.112" + "effect": ">=4.0.0-rc.118" } } ``` diff --git a/services/www/src/docs/content/build/plugins/effect/rpc.mdx b/services/www/src/docs/content/build/plugins/effect/rpc.mdx index 51073744aed2..9234b292972b 100644 --- a/services/www/src/docs/content/build/plugins/effect/rpc.mdx +++ b/services/www/src/docs/content/build/plugins/effect/rpc.mdx @@ -122,7 +122,7 @@ Create the Effect client, then create the RPC subclient: ```ts import { OpenCode } from "@opencode/client/effect" import { Effect } from "effect" -import { FetchHttpClient } from "effect/unstable/http" +import { FetchHttpClient } from "effect/http" import { Acme } from "opencode-acme-plugin/rpc" const program = Effect.gen(function* () {