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 12134e5b24b..0215a1424c3 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 635b9444e60..2aa7d04fca1 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 b97fbb212df..eb7b75f39a9 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 94eac741d2c..4b583076a89 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 55a386b79dd..3e924f2810e 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 fca61a3cc2a..91c0d832c9e 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 5bb2d27d24d..1483249ec50 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 46c07413225..6446b04f482 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 d512e91e041..f4d467108e3 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 a29cc1c529d..af81da859c2 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 b5f7b532bf0..ca4e128f568 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 1c6e8517c8c..c3868c71cd6 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 626df5e8e0a..1f971231a8c 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 d3de229dc4e..ce722a5e078 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 02e77db373f..dc5a0664d21 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 b8f7ff1584c..ed67d294bba 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 c3ff3a621fe..af1adc31f4f 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 4e83f847801..e0efbe34ae1 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 083a753e866..33c88caa8c4 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 15abd8a62c3..5ff0e956f7a 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 b711db7805b..55437c25426 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 087bd57df9e..f9881519866 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 bc3a68f064f..14a9ec8ab06 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 0cfd5f4bcac..5fc486d1cbd 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 ed52c502a37..605ae5604cc 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 54cd33939cb..b3ddeff8976 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 f93e3c86887..18e883d87b5 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 cd2dc5c526b..02f5c5bb464 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 2116b8cb542..2120e35e040 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 013cd4d321a..69b6a6c7869 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 01593ffcf37..438b93a701e 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 45d1eac0b65..e23d92f7f81 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 736e650361f..375686a4a22 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 713acdd8606..50111c0ab4a 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 5494a86ebdc..9a8a4d9a217 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 5cd0c3fd24c..8087867c9bd 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 24217070104..ed3d8981875 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 f098d3bd564..4ff92c2abc6 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 27b8d6de5b2..47514a352c9 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 884fece763e..5af63162d68 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 5e330f61d5c..c87f0d75c2d 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 f858c828585..8c5c69ee2af 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 647c259008b..f01c6acb63d 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 5787e3dcc68..cb27dd00e35 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 c04b666887c..46eab97d44f 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 d9f8264b140..dcebd156e00 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 cabf3013bbd..d76d75b3ef5 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 9472463762e..8b235ab650b 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 e72bcf6fe17..2171993c57b 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 8f821e202ca..d0f4245a8ce 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 f4edd2199a3..a0bd3d17ea9 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 9b6768a6cef..7a6435e88d1 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 11196323db1..35e30fef94a 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 32fc8f65560..27245a8e08a 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 cc3ede886f0..c3c53c6a23e 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 92b955f1d5b..908f939de6c 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 7ce174a94d5..37e021301c1 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 0a85e7bcffd..25158125494 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 7a682384e59..c4c8085b258 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 de5e574714d..7123a825c6f 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 4a4a33e5e08..59edb401d20 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 eb0e22aebc7..e09aeac3d67 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 604e32b6693..ebc7e1afd16 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 5cfa52ff886..293ae904ddd 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 eb12815efad..bc4c8112d9e 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 357f12691fb..fc2535392fb 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 9bcf0aa5d7f..cbc1a9a5b25 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 1d08278c523..55f60d0a793 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 5179a888ba9..46576a74094 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 c7c9405f143..b1f45be09bd 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 38e62cbc333..667248bdea2 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 f634dafddef..5d481a0150e 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 877599a4809..91914bc00c6 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 de7366e9ae2..29a891ef9c5 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 c33d3fe483d..43187a7c034 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 b7986b7e36f..ccbe8b9531f 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 2c507f5a0dd..005773a2a9a 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 6d88a7aea22..3d2810db65b 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 648233a5219..a4f7e267d03 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 8a980afc92a..b7dab85d2a2 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 c7b03792b10..31f67677554 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 ab482c04ffc..41300d78826 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 dbc1c342cbc..4b82b05c44a 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 63df55436d9..7660dcf43b0 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 f492bd763e9..7ea76ce3527 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 0818b8609b2..6bdc9dd56e6 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 caffeebdd96..fae7307ffaf 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 2f9c15cf8a2..93be1fa0b2d 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 e696fc1a751..77686c6cec1 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 cce8a0cf13a..7cba7d88871 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 52589552cce..91677e95ccc 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 999ec0c4c1a..3086aec462a 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 3639f2341f4..21ec0ce8abd 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 a1f0a7da2c4..060b6160057 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 6323a6651b6..5dbe25850b8 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 5589070c4da..83f71782aae 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 45340c1e0c2..8361bdb1852 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 76c99ab9df5..3df1f074965 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 58799329974..b57e46b849f 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 28ae68224a4..76f1ef4ccaa 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 68e3e307d45..69a00b28409 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 ba7d9f87965..2e4cbc1c309 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 f74b4a64c2e..c681f44b8a6 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 c00448a0b8f..edda5d733db 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 4ae5b7740ec..d20e4d32aed 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 b8db1291207..c8c26b82b8c 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 99c6be0a61b..67ad107231b 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 d87589ef9a6..92e5de1fb8b 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 50486251793..149433d9465 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 1facda40941..1f735ea3dee 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 55c1ce82bfa..7707ef8693c 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 92d48ad8bc6..081516d3a89 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 bb36c09adbd..511903edd20 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 7eba0c3d2ef..c757acc8027 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 9d8e50d7c4c..9ee87ea36c0 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 1e5e5b326ae..5c10fe6b46a 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 bdf00071848..c44708febfb 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 8548a42557f..f2a0cfc3cdf 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 79fdb7fbff0..2a4df8c44f9 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 80b3bc97ab0..900556191ca 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 1b23d20c368..e39138fd595 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 86b194c0013..5a70ae71e72 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 5b9c98d1a6d..ed41e9b02c2 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 f3b7fb97389..c528e08e758 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 416589c935f..5d6e674feae 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 9f14aaa9157..a392273dad7 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 fce18f08aec..0f1ce308524 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 f400b59ae2d..dd3fb595542 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 7f96a3f22af..1d3949d015e 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 cb90f1dd2ac..a029fb8adef 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 249152333da..93724052fc0 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 669aa0d211f..e5579e366ea 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 6b4ab20dd10..9297bc7b677 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 a1c44b392ad..ccf96e5fb27 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 1ea0eca1b72..d53f2544765 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 ddf586d945d..7728bc8f4e9 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 354e1407655..9053a0def66 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 884626d6ad2..186274fe3e3 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 86e38e3163c..7c032507f0f 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 718c6456c7b..2f0070cfb31 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 4d103190286..444ff1d8bad 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 39c0d2c7455..85162fd123b 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 0dc997fda93..416c0d08064 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 d2350fb7588..d315fb26e64 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 a2a044523a0..24b237855ea 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 0e8003a6e3d..c7f23115645 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 59db611a77f..56e7b290444 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 eb85babf56e..46bb4e6b283 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 ba8354f807b..86b3714add5 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 5144d6931b6..b633eed780e 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 177666127b0..8480e463245 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 79faf9536e7..31dd7f224e1 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 ce0c1963ca4..84151f5554f 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 d805242c2ef..f6239be4cbd 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 15f44888e1c..26e3cf76c8a 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 2ad40576efe..6f1219240f9 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 1d2a6358c18..653d3243fbd 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 45652ec8819..45891e8d6a1 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 5b77dd8f5d9..c4eba8c7424 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 85b3d15efcf..75de26333ac 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 f04c9b88839..24edb14757f 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 f36cab51892..741f51a08c6 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 9efdc74d298..d44ba281352 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 7249253220c..1d49dc49e32 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 1dc3a26108b..555780f4f2a 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 1ea260e0c5d..39126ffcc77 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 478d4feb5f8..568d27b983b 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 5184155482b..5b858aff526 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 4b568a1fb0d..54c04774013 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 77d86acddc1..604540f373b 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 bb51148330b..6229abab030 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 4f2317e0d82..aa3b06f827d 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 4ca73e8bb8d..2b013128b3a 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 5e25326bf7b..29d810149e1 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 b42b07dc9c2..29ca97246d5 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 39327551714..6953692dfba 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 f3173184cec..2d2dbe70c93 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 7cb534a444f..beaad3851d6 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 ec445e469b5..7c40d5b5106 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 0338dfdb601..e25084b0a40 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 f21c67a533e..d1c5e29caf1 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 f8e959cfa21..b3f600c5495 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 ec62425e31e..5c645b2240a 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 450ae5daa40..fe761a54b04 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 7ec63bb7b97..dc37dd45c05 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 34b651296e6..27436d35a26 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 daad50ceab3..04626e74a37 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 d6d2685e8cf..12a390adcd0 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 b88a8eaaf25..c422716a820 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 59a26dbb08a..dda8eb982cf 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 a0389f481e2..7cd1758b3ec 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 64baf92cc7a..832e69d791e 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 38b45e59454..78d59cf146d 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 d2c0958dcef..e863b7f6b58 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 b5c088bbd1d..41f450ea0f5 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 380852721cf..8c52f824190 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 a7887642723..bd265a5a711 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 a093eab9ec7..f1a238f0602 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 31c31535d0f..272a86c7f88 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 b38676e8cd1..d45f0eecd4f 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 0a9728d54d5..627aeeb9836 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 8cc23621b54..8d67db19f5e 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 defe86af230..3af091a3f33 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 10972c8b029..2549d45ec3e 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 686dc4dc334..d556c9e84e7 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 d1caf7c0c2f..da42abb7dfa 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 c4b8481afb7..0efc867fc9e 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 f19d8e39812..63d51ac5efe 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 f44cdb7ca1e..ff2bdced4af 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 c6251760699..5386fa6a9eb 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 6699a077f49..7abed9c5ba3 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 152099b000c..a5be7ad5e5a 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 c7afed50f19..bc53389c390 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 5651dcac311..36959af196f 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 3933adcde16..21ed89ba9cb 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 1ee13c4cbb7..e644aafe276 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 16726a4ad43..4c23590db1a 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 96b01be6685..938397b5b33 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 3b7b1d45f45..8e761206634 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 3ca601baceb..e14177e9724 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 929079e7b3e..39156846211 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 d0a400d8230..36879be3d1a 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 fb60bf9014c..ce9eb980a46 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 8bfdaf3663c..ae5ef260114 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 299f42255f3..91e46fb2ae2 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 184e0448a5f..1410f92de87 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 ebda546cbc6..22c7c0d77a0 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 7e90941edde..c70bef8df5f 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 bcec31a6735..55718272374 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 7e3f8e8dd72..c3f1fcf5d23 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 f0201e6cb68..d9f5b86698d 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 1e49c90e8b8..0363735f427 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 76d58275284..a09a7fa616f 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 1419a55d4d0..99a5d1928f8 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 75469038998..a4d29eb70a5 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 b9e3bf3864c..3287d2a99f1 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 e09f6cdf0d1..824061a2af4 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 d6cb754b096..f7c10b34eac 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 5475342e97b..b1055141901 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 ae1946841a4..3e6de686cb1 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 3a1547d2921..2114695c3f9 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 ef131e6e317..9c882ba1396 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 51e826cff23..5d934b1ac24 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 a83abbaef18..9e0a0e0adec 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 b3438f8d93d..69299cba8fa 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 e8aca73d1a4..91bdd38b8ae 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 73a0d5e38b9..00e74407cc3 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 aa21479d6b4..82cb66eee2c 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 8b1ddb18be5..91de480703e 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 312803385be..4923495cab6 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 6be83690911..75db66aa7ea 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 899d0feaf53..fee6c65838d 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 420ccb09b68..05a1f086921 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 0eb05b9927c..0cf01096ab1 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 4be32c648fa..fbb678bcca4 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 a5932668472..97c95023c33 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 05cde80c963..905b4187686 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 bfa9438e23b..f6a4c7a2c40 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 bc4beb54042..3b89af635d9 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 568b101743a..53ad8801be8 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 2dcba03e6fc..37a26873af1 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 e1900f729f8..11a5e7b0589 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 ebba965c7df..1387cf2f369 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 64e139b01fa..d3a8a1be20e 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 30fa8205f22..d2589ddd3b4 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 2856d157cee..02e3cc1e2fc 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 266f9344cf2..e37fcc15b2a 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 6210cd1422b..d9cd879e418 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 8432988dfda..7b2a6b35640 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 5cd4477bb4d..f9be3591c6c 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 0e412b20ef5..b158bc82c6b 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 1f6411dfb26..7fe72e0bd48 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 23c35b74b0a..7f0a649472c 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 7b85ecd6011..50fd40f66f1 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 734028b3009..734c3b48be8 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 25a285939c6..aaba261a16a 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 5ecf68a0414..7afb621efaa 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 9e72fcb1f56..af1d5b53f05 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 eb41969dfd7..ff5efd107eb 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 90fca553198..1323650788f 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 61c4ddecd11..7fbb47b83d9 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 0691ad4fb24..fe653d55b4f 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 a01155ac666..3af3e3b41a6 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 4fd6e58159b..485d0ae37cb 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 dc9b850114e..f1360b57f82 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 cb4692838aa..c6f6693fe25 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 7832916326e..4123a48a7a4 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 8b986007665..fb7777c57cc 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 f067ecfe396..288709c4409 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 16abef80ff9..20407989f8c 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 81b56e5cd98..612e8d39211 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 ab44243578e..4fe7d189579 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 cab3742ef2d..49b0c78206d 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 ffcce9a4cd2..37f171d8db0 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 eb17f3632f2..639466e7244 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 c744abd880f..0a2b4c8622d 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 fb069604b17..d41db114a7d 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 4b8ebee3491..8df7d46fce6 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 9085a51f9f8..a5f93eb61ba 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 dd1abbe9d1a..9974318772e 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 24409d72233..8e756e8fe46 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 13fd20cb47f..f7aae9cf367 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 77e8fdb3fe7..e5b6b39f242 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 f86677ba4b2..dd0c465553b 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 c1f727ff655..6935a75b84c 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 ab533c17104..175229a7f06 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 9b81a2511f9..53e80925bc0 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 d5bf2af1db1..87d214ba4d0 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 ab908f4c826..f8948caae93 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 103e52515f4..b01bf265d4d 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 ad6026cdabe..ab6aa9300d8 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 8a333898de0..7218d67364d 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 0e8f26a8c24..1ef649f6c6f 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 54d33bc26ee..57bbf8434cc 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 a4c36d5b8c0..d8d917930ae 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 38c9fde40fa..ad94fc8e427 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 00000000000..bc048e2b5cf --- /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 f8a38e00e88..c28933251b5 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 dc2a0a212a6..92625bdbbf7 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 db0c8c55735..b1dc5b04627 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 adbf14a9b65..b3cd03e8782 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 8d4050114af..b96eb589df3 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 a2c1e93a1c9..f2ff1773854 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 e978c946329..85fedd8b2f0 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 5545265c623..2763e317248 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 d458f8283ff..ba8a7a73079 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 b2da9093f0f..848d743417f 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 d19a39b9708..6cbbc0b1bf7 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 ba0b48ce7ef..4e74e7ab997 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 f8a3e64b01c..82f6a516414 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 251a61417b1..edeeeed6eb8 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 c7d2e71b3da..00dc2f9b19b 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 02a8d5aa953..e5e3eda4a7a 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 ad2ed5a7129..54a7e1cd93f 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 f4c88089132..4787d8fbec7 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 5e1dd60404a..3c354ae6653 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 2da7f7dac4a..f7243d0decc 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 74a6796c1ef..635cdfa106d 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 88a52634897..e6aba4da568 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 713cf3f6262..f54148af226 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 bf289721bc8..c540478fa51 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 4a22939dba2..9713b10a520 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 a2cf7158181..4f6f31a9c0c 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 dd5b83762f6..bb875b17081 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 40963783e4f..987f2879d3a 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 15f2714c056..8d9640481fe 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 51c9f95cb97..11225d42b71 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 9d1f4346edd..d4c7bf8d605 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 04e6f604e0a..dfdbaa44762 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 2d376f8755d..942b9d923bc 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 47933234585..d6b47b27e63 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 04f6120b74b..5bf8d621395 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 1c287560050..15c0e5f435b 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 b316fd2b6dd..43599e9ea94 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 72b61c0fbd7..2832c502d64 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 748b86e42f3..cb452e96e4e 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 31fe39c483e..76e157466e0 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 f6bcf2a1358..7a32951b3fe 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 75bdb0e5503..ab9c2201a5e 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 ecf965bb179..21cf51f818b 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 3de69057550..4a2ebbe6a7b 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 3fb20bc328c..aae02c9b1c4 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 a0bd96a28e7..ded5610ae61 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 81a933789d3..d85594b38d4 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 73a07633520..dd85c2f4ff5 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 0beeafc279a..6b08faf8075 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 a0cba465d33..39039fcd638 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 623134755cd..ec5b3176765 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 bb838a6173c..71808b86e20 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 c90b57e608e..dc6867da86c 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 7553fa9adc6..c37979e9413 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 84906429d77..7e995c79059 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 f622c9317da..b975d1aacfb 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 6762cd46161..36a4ee7af9d 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 341ff73e606..3de912a04d2 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 890307d28b2..d7f79a7e655 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 f8e6da476ff..5a5b953684a 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 8eeeece1860..0a13c02fead 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 67d5270f54a..f8346456cc8 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 4163d7e19a9..20ada8bae07 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 77d2cea8c52..9da592289ee 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 00000000000..dd7428306fe --- /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 405ff2c5d4e..71b9708d482 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 00000000000..67974f5a0c1 --- /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 94a37f759a7..3854a11b5e5 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 c99377ef67e..6b141d0e25b 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 0cea57e4aff..8ddc3601f94 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 00000000000..df5c19ef5e8 --- /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 a3ec0f61dbe..00d4fe1bd7f 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 f91ec8246ce..52aacd8e4d0 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 ccf67d71655..2ed835aac60 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 82823519be8..da1c94bc1cf 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 0f077fb2b8b..0b67f429f60 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 1b48590db91..25884594ffb 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 9049b5700ca..863048e1ff7 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 6b5fd2f5b7d..b500b5a6af1 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 fb5a4de3529..f6aa6d63323 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 8821714d7bd..c8df5bd402f 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 e951118079e..0fea1f2efa6 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 95104bc45ee..424c5fe8cf3 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 ffd9071cb02..8df615c8fbd 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 a34f0447ddb..ee9e462039f 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 e518fbeccf9..78be301eeaf 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 1dd56270fa3..d942afae6bc 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 a72a5a85bcb..2fa76f114a3 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 88758b07b95..e971a9e62c5 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 90fca553198..1323650788f 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 90fca553198..1323650788f 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 560030d7208..75b53998f2f 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 5f187ad0cd7..4769c3c6c4a 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 51073744aed..9234b292972 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* () {