[pull] main from oomol-lab:main - #19
Open
pull[bot] wants to merge 65 commits into
Open
Conversation
## 中文 ### 概要 新增 Qdrant Cloud Provider,通过官方 REST API 提供 7 个可本地执行的向量数据库 Action: - `qdrant.list_collections` - `qdrant.get_collection` - `qdrant.create_collection` - `qdrant.upsert_points` - `qdrant.get_point` - `qdrant.query_points` - `qdrant.scroll_points` ### 主要变更 - 使用 `custom_credential`,支持 Qdrant Cloud `clusterUrl` 和 `apiKey`。 - Credential Validator 通过 `GET /collections` 验证数据库访问权限。 - 仅支持 HTTPS、官方 `*.cloud.qdrant.io` 主机和 `6333` 端口。 - 所有请求使用共享 SSRF 防护 Fetch,不启用私网访问或 DNS 校验绕过。 - 支持 unnamed dense vectors、JSON payload、基础 Qdrant filter 和单页 scroll 分页。 - Upsert 使用单次 `PUT .../points?wait=true` 请求,避免写入与可见性之间的竞态。 - 统一处理 Qdrant 响应包络、超时、取消、非 JSON 响应和 HTTP 错误。 - 严格拒绝 named/sparse vector 字段,保持首版能力边界清晰。 - 未增加 npm 依赖、共享运行时改动或 Provider Proxy。 ### 验证 - Qdrant 定向测试:9/9 通过。 - 完整测试:58 个文件、557 个测试全部通过。 - TypeScript 检查通过。 - oxlint 通过。 - oxfmt 格式检查通过。 - Catalog 已生成并包含 7 个 Qdrant Action;生成文件未提交。 ## English ### Summary Add a locally executable Qdrant Cloud Provider backed by the official REST API with seven vector-database actions: - `qdrant.list_collections` - `qdrant.get_collection` - `qdrant.create_collection` - `qdrant.upsert_points` - `qdrant.get_point` - `qdrant.query_points` - `qdrant.scroll_points` ### Changes - Add `custom_credential` authentication with Qdrant Cloud `clusterUrl` and `apiKey`. - Validate credentials through `GET /collections` to verify database access. - Restrict URLs to HTTPS, official `*.cloud.qdrant.io` hosts, and port `6333`. - Route every request through the shared SSRF-protected Fetch without private-network access or DNS-validation bypasses. - Support unnamed dense vectors, JSON payloads, basic Qdrant filters, and single-page scroll pagination. - Use one `PUT .../points?wait=true` request for upserts to provide write visibility without a separate race-prone request. - Normalize Qdrant response envelopes and handle timeouts, cancellation, non-JSON responses, and HTTP errors consistently. - Reject named/sparse vector fields explicitly to keep the initial capability boundary clear. - Add no npm dependencies, shared runtime changes, or Provider Proxy. ### Verification - Qdrant focused tests: 9/9 passed. - Full test suite: 58 files, 557 tests passed. - TypeScript check passed. - oxlint passed. - oxfmt check passed. - Catalog generation includes all seven Qdrant actions; generated files are not committed. ## Scope Notes This initial provider intentionally excludes self-hosted Qdrant, gRPC, named/sparse/multivectors, deletion operations, aliases, snapshots, payload indexes, and arbitrary endpoint proxying. --------- Co-authored-by: l1shen <648952316@qq.com>
## Summary - Home Assistant connections to a LAN instance fail with `request URL must not target private or reserved IP addresses`, even when the deployment sets `OOMOL_CONNECT_ALLOW_PRIVATE_NETWORK`. - `src/providers/home_assistant/executors.ts` never passed `allowPrivateNetwork` to `defineProviderExecutors`, so the flag had no effect for this provider. - Adds the one-line opt-in, matching the 26 other providers that already wire it. ## Problem `defineProviderExecutors` builds a private-network-aware egress fetch only when a provider opts in; otherwise it falls back to the public-only `providerFetch`. Home Assistant took that fallback, so the flag could never reach a `192.168.x.x` or `10.x.x.x` instance — the self-hosted case it exists for. `homeassistant.local:8123`, the placeholder shown in the connection form, was unreachable for the same reason. Default behavior is unchanged: with the flag unset, private targets stay blocked. Loopback, link-local, and cloud-metadata targets stay blocked either way. ## Scope Only the executor needed wiring. Home Assistant has no `assertPublicHttpUrl` call site to thread the flag into `resolveHomeAssistantBaseUrl` and `validateHomeAssistantCredential` both route through the provider-local `normalizeBaseUrl` — and no `proxy.registry.ts` entry. Migrating `normalizeBaseUrl` onto the shared `assertPublicHttpUrl` is a separate change, not attempted here. ## Tests Added to `src/providers/provider-runtime.test.ts` rather than a provider-local file, per AGENTS.md: "Keep open-source-only shared-infrastructure tests beside the shared module rather than inside a provider directory." Three cases cover the shared opt-in path: default blocks a private target, an opted-in executor reaches it once the flag is enabled, and loopback stays blocked even then. ## Verification `npm run fix-check` (clean) and `npm test` — 57 files, 566 tests passing.
… v1 (#221) Discover the served Grafana App Platform API version at runtime so dashboard and folder actions work across Grafana 12 and 13. Cache only successful discoveries with a bounded cache, and retry discovery after transient failures. Tests: npm run fix-check; npm test
…228) Fixes #229. `list_repository_issues` fetches one GitHub page and filters pull requests out of it, which destroys the only pagination signal page-number callers have: the filtered array's length says nothing about the raw page length. A short page may be a full page with PRs mixed in, and an empty page may be 100 consecutive PRs — so any paginating consumer that stops on a short or empty page silently drops every later issue, and no sound termination rule can be built from the filtered response alone (details in #229). The response now carries **`pageInfo.fetched`** — the number of items GitHub returned before filtering — declared in the output schema and in the action description: callers must continue paginating while `fetched` equals the requested page size, even when `issues` comes back short or empty. The PR-filtering behavior itself is unchanged. Tests cover the mixed page (`fetched=3`, filtered ids `[1,3]`), the all-pull-requests page (`fetched=2`, `issues=[]` — the case that defeats every downstream heuristic), and the schema declaration. `npm run typecheck`, oxfmt, and the full vitest suite (557 tests) pass. --------- Co-authored-by: l1shen <648952316@qq.com>
…ons (#227) ## Summary - add `openGuardedWebSocket` in `src/core/guarded-websocket.ts`, so provider egress that is not a fetch is held to the same SSRF policy - extract the per-hop check behind `createGuardedFetch` as `assertGuardedEgressUrl`, and make both transports use it - add five Home Assistant actions that the REST API cannot serve: registries, related-item search, device automation capabilities, script execution, and config validation - document the WebSocket egress rule in AGENTS.md ## Problem The Home Assistant provider exposes entity states but not the structure behind them. `/api/states` returns `light.living_room` and nothing that says which device, area, or floor it belongs to, so "turn off the lights downstairs" is a string-matching guess rather than a query. Those registries are only reachable over Home Assistant's WebSocket API. So are related-item search, per-device automation capabilities, multi-step script execution, and trigger/condition/action validation. None of them have a REST equivalent. ## Egress guard This is the part worth review attention. Every non-fetch egress in this repository so far targets a host fixed in code: the IMAP/SMTP runtime resolves its hosts from a per-provider allowlist (`netease_mail`) or a literal (`qq_mail`), and the Pushover realtime socket uses a constant `wss://` URL. None of them needed a guard, which is why `src/core` did not have one. A self-hosted Home Assistant is the first case where a non-fetch transport connects to a **credential-supplied host**, which is exactly the case AGENTS.md says the DNS resolved-address check must cover. Rather than add a second, narrower check for WebSockets, this extracts the hop check that `createGuardedFetch` already performs — `assertPublicHttpUrl` on the literal, then resolved-address validation — into `assertGuardedEgressUrl`, and has both transports call it. `ws`/`wss` targets are validated as their `http`/`https` equivalents and then connected over the normalized `ws` form. `allowPrivateNetwork` and `skipDnsValidation` behave as they do for fetch. Two limits are inherent to the transport and match what the fetch guard can offer: the constructor re-resolves the hostname itself, so low-TTL rebinding remains possible, and WebSocket handshakes have no redirects to revalidate. Both are stated in the JSDoc. ## Runtime support Node 22+ and workerd both expose a client `WebSocket` constructor, so this is one code path rather than a Node-only capability. Verified on workerd with `wrangler dev` and the repository's compatibility settings rather than from documentation: ```json { "hasConstructor": "function", "constructed": true, "readyState": 0, "hasAddEventListener": "function", "hasSend": "function", "hasClose": "function", "hasNodeDns": "function" } ``` `node:dns` being present under `nodejs_compat` means the resolved-address check applies on Cloudflare too, consistent with the note added in #135. The provider is therefore **not** marked `nodeOnly`; doing so would also drop the eight REST actions that work on Cloudflare today. Home Assistant authenticates in-band, with the token in a message rather than a header, so nothing here needs a constructor that can set request headers. Reachability is a separate matter from the guard: Workers cannot route to private addresses, so a LAN instance remains a Node/Docker/Fly deployment concern regardless of `OOMOL_CONNECT_ALLOW_PRIVATE_NETWORK`. AGENTS.md now says so. ## Actions | Action | WebSocket commands | | --- | --- | | `get_registries` | `config/{entity,device,area,floor,label}_registry/list` | | `search_related` | `search/related` | | `list_device_automations` | `device_automation/{trigger,condition,action}/list` | | `execute_script` | `execute_script` | | `validate_config` | `validate_config` | All five are one-shot commands, so they map onto the existing request/response executor model unchanged. Subscription commands are deliberately excluded. The action model has no streaming, and every Home Assistant subscription that carries an initial snapshot already has a one-shot equivalent — `render_template` is a subscription whose first message equals `POST /api/template`, and `subscribe_entities` starts from what `/api/states` already returns. What remains is increment-only (`subscribe_events`, `subscribe_trigger`, `logbook/event_stream`), which a bounded listen window would serve badly. That needs a webhook design, not an action. `get_registries` sends all five registry commands over one connection. The handshake costs two round trips before any command can be sent, so batching matters more here than it would over HTTP. Reference: https://developers.home-assistant.io/docs/api/websocket ## Verification - `npm run fix-check`, `npm test` (574 passing, 20 new) - workerd probe above, via `wrangler dev` - end-to-end against a real Home Assistant instance: `get_registries` returned 160 devices and 7 areas over a single connection with ids 1–5; `search_related` resolved a device to its area, config entry, integration, and three entities; auth failure mapped to 401; a loopback base URL stayed blocked with the private-network flag enabled
Reason: The MiniMax connector exposed only model listing, model retrieval, text Responses, and token estimation, so it could not generate video. This extends the existing `minimax` provider with asynchronous video generation actions: - `text_to_video` and `image_to_video` create `POST /v1/video_generation` tasks. `text_to_video` requires `model` and `prompt`; `image_to_video` requires `model` and `first_frame_image`. Both accept the optional `prompt_optimizer`, `fast_pretreatment`, `duration`, `resolution`, and `callback_url` request fields. - `query_video_generation` polls task progress via `GET /v1/query/video_generation` and surfaces `task_id`, `status`, and `file_id`. - `download_video` retrieves the generated file metadata and its download URL via `GET /v1/files/retrieve`. - The `model` field is an enum that defaults to `MiniMax-Hailuo-2.3` and covers the documented video models. - An optional `region` field selects the global (`https://api.minimax.io`) or China (`https://api.minimaxi.com`) host; the existing text actions keep using the global host. All requests continue to go through the shared SSRF-guarded fetcher. Checks: - `npm run fix-check` (oxlint + oxfmt + `src` typecheck) - `npm run generate:catalog` - `npm test` (vitest, 554 tests passing) --------- Co-authored-by: octo-patch <266937838+octo-patch@users.noreply.github.com> Co-authored-by: l1shen <648952316@qq.com>
## Summary - add a locally executable `cloudflare_mcp` provider backed by Cloudflare’s official unified Streamable HTTP MCP endpoint - expose the current Code Mode tools: `docs`, `search`, and `execute` - support both OAuth 2.0 and Cloudflare API Token / Bearer Token credentials - validate credentials through MCP `tools/list` and require the expected official tools - route all MCP traffic through the provider SSRF-guarded fetch implementation ## Authentication API tokens are sent as `Authorization: Bearer <token>`. Both user and account API tokens are supported; account tokens should include **Account Resources: Read** so the official MCP server can auto-detect the account. OAuth uses Cloudflare’s published authorization metadata: - authorization: `https://mcp.cloudflare.com/authorize` - token / refresh: `https://mcp.cloudflare.com/token` - PKCE: S256 - required base scopes: `user:read account:read offline_access` The open-source runtime uses bring-your-own OAuth clients. For Cloudflare MCP, register a public client through `https://mcp.cloudflare.com/register` using the callback URL shown by Open Connector, then save the returned client ID in the OAuth Client configuration. No client secret is required (`token_endpoint_auth_method=none`). ## Implementation notes - the endpoint is fixed to `https://mcp.cloudflare.com/mcp` - `search` and `execute` preserve JSON results when possible and plain text otherwise - structured MCP results (including `docs`) are returned directly - MCP authorization, transport, protocol, and tool errors are mapped to stable provider errors - no third-party logo asset is copied into the repository ## Verification - `oxlint .` - `oxfmt --check .` - `node scripts/generate-catalog.ts` - `node scripts/typecheck.ts src scripts-all examples` - `vitest run` — 59 files, 562 tests passed --------- Co-authored-by: l1shen <648952316@qq.com>
## Summary - configure the Cloudflare MCP client to use the MCP SDK's `CfWorkerJsonSchemaValidator` - add a regression test that disables string-based code generation while validating advertised MCP tool output schemas ## Root cause The MCP SDK client defaults to AJV for JSON Schema validation. When Cloudflare's MCP server advertises tool `outputSchema` values, AJV compiles them with `new Function(...)`. Cloudflare Workers disallow runtime code generation from strings, so credential validation and MCP requests fail with: ```text Cloudflare MCP request failed: Code generation from strings disallowed for this context ``` The SDK's Cloudflare-specific validator uses `@cfworker/json-schema` and performs validation without `eval` or `new Function`. ## Impact Cloudflare-hosted Open Connector deployments can validate Cloudflare MCP credentials and invoke its tools. Node deployments retain the same behavior. Because the provider is currently broken on Cloudflare in v1.3.3, could this be included in a patch release after review? ## Validation - `node scripts/typecheck.ts src scripts-all examples` - `vitest run` (61 files, 590 tests) - dedicated regression test with `Function` disabled and MCP `outputSchema` values present
## Summary - stop generating full executable action ID arrays in the Node and Cloudflare provider registries - use the loaded executor module service keys to identify locally executable providers - resolve those services to the exact action IDs present in the loaded catalog before creating `CatalogStore` - preserve explicit `executableActionIds`, runtime execution metadata, and public response shapes This is the first of the two changes discussed in #235. Catalog asset chunking will follow in a separate PR. ## Why The generated Cloudflare registry repeated all 12,743 executable action IDs even though each included provider already has a lazy executor module entry. The existing generator marks every action from an included provider executable, so the executor module service keys carry the same information more compactly. ## Wrangler size comparison Measured from `v1.3.3` and this branch with Wrangler 4.115.0, the same Cloudflare bindings/configuration, `--dry-run`, and `--minify`: | Build | Minified upload | Gzip | | --- | ---: | ---: | | `v1.3.3` baseline | 11,935.15 KiB | 2,854.70 KiB | | This PR | 11,566.36 KiB | 2,777.99 KiB | | Reduction | **368.79 KiB** | **76.71 KiB** | The emitted `cloudflare.js` decreased from 12,221,596 to 11,843,955 bytes, a reduction of 377,641 bytes. ## Validation - generated both provider registries for 1,194 total / 1,192 Cloudflare providers - `oxlint . --fix` - `oxfmt .` - `node scripts/typecheck.ts src scripts-all examples` - `vitest run` — 61 test files and 591 tests passed - Wrangler dry-run builds for both baseline and optimized revisions Refs #235
… calendars (#239) ## Summary - add automation, script, and scene config CRUD, reached over REST at `/api/config/<component>/config/<key>` - add history, logbook, calendar, and error log actions from Home Assistant's published REST API - fix `buildHomeAssistantUrl` dropping the instance base path Fifteen actions in total, all REST. No WebSocket code is touched. ## Problem Two separate gaps, both in the REST surface. **The editable config store was missing entirely.** Home Assistant serves automation, script, and scene configuration over REST, but at `/api/config/<component>/config/<key>` rather than under the endpoints listed in the published REST API reference. The provider could therefore run an automation but never write one. That endpoint family is absent from the reference page, which is the most likely reason it was never covered — the eight actions the provider started with map exactly onto that page and nothing outside it. **There was no time dimension.** `/api/states` answers "what is the temperature now"; nothing answered "how did it get there" or "what tripped that switch overnight". `history`, `logbook`, `calendars`, and `error_log` are all on the published reference page and were simply never implemented. ## Config CRUD `automation`, `script`, and `scene` differ only in three respects: the path segment, the input field carrying the key, and whether Home Assistant validates that key as a slug (`cv.slug` for script, `cv.string` for automation and scene). One descriptor captures those differences and parameterises shared read/write/delete handlers, rather than nine near-identical copies. The key travels in the path and Home Assistant injects it into the stored entry itself, so the request body is the bare config object. `POST` is create-or-update: writing to an unused key creates the entry, which is how the UI creates automations. Both endpoints require an admin token, and they only see entries Home Assistant manages itself — an automation defined in a separate YAML file or a package is not visible and returns not found. The action descriptions say so, because a non-admin token fails at the provider rather than at input validation. This pairs with `validate_config` from #227 to close the loop an agent needs for automation work: discover what a device can do, validate a candidate config, write it, then read the logbook to confirm it fired. `followUpActions` wires that sequence into the catalog so it is discoverable rather than something the caller has to know. ## History and diagnostics `get_history` exposes `minimalResponse` and `noAttributes` because a full-attribute history across several entities is large enough to matter for an agent's context budget. Against a real instance, one entity over 24 hours returned 7219 points; the compact form drops each row from the full state object to `{"state", "last_changed"}`. Home Assistant reads `minimal_response`, `no_attributes`, and `skip_initial_state` by presence rather than by value, so `false` omits the parameter instead of sending `0`. `significant_changes_only` is read by value and defaults to true, so it is encoded normally. `get_error_log` needs one caveat: Home Assistant registers that view only when the instance runs with file logging, so it can report not found on an otherwise healthy instance. Rather than surface a bare "Not Found", the handler maps that case to a message explaining why, and the action description repeats it. ## Base path fix `normalizeBaseUrl` accepts an instance URL with a path, so a Home Assistant behind a reverse proxy at `/ha` is a supported configuration. `buildHomeAssistantUrl` assigned the endpoint path over `url.pathname`, which dropped that prefix and sent every request to `/api/...` at the origin instead of `/ha/api/...`. It now appends to the base path. The WebSocket URL builder added in #227 already did this, so the two transports agree. This is a separate commit; it predates both PRs, and the fifteen new paths here would have multiplied it. ## Actions | Action | Endpoint | | --- | --- | | `get_automation_config` / `save_automation_config` / `delete_automation_config` | `GET`/`POST`/`DELETE /api/config/automation/config/{id}` | | `get_script_config` / `save_script_config` / `delete_script_config` | `GET`/`POST`/`DELETE /api/config/script/config/{key}` | | `get_scene_config` / `save_scene_config` / `delete_scene_config` | `GET`/`POST`/`DELETE /api/config/scene/config/{id}` | | `check_config` | `POST /api/config/core/check_config` | | `get_history` | `GET /api/history/period/{timestamp}` | | `get_logbook` | `GET /api/logbook/{timestamp}` | | `list_calendars` | `GET /api/calendars` | | `list_calendar_events` | `GET /api/calendars/{entity_id}` | | `get_error_log` | `GET /api/error_log` | Reference: https://developers.home-assistant.io/docs/api/rest ## Verification - `npm run fix-check`, `npm test` (584 passing) - read paths against a real instance: `check_config` returned valid; `get_history` returned 7219 points for one entity over 24 hours with the compact form visibly applied; `get_logbook` returned 4417 entries; `list_calendars` returned an empty list on an instance with no calendar integration; `get_error_log` returned the explanatory 404 described above - write paths against the same instance, creating and then removing an `oc_smoke_test` entry in each domain: create returned `ok`, the read-back showed the key injected into the stored entry, delete returned `ok`, and each subsequent read reported not found. A non-slug script key was rejected at the provider before any request. A following scan of 3830 entities found nothing left behind.
Adds four read-only alerting actions to the Grafana provider: | Action | Endpoint | |---|---| | `list_alert_rules` | `GET /api/v1/provisioning/alert-rules` | | `get_alert_rule` | `GET /api/v1/provisioning/alert-rules/:uid` | | `list_alert_instances` | `GET /api/alertmanager/grafana/api/v2/alerts` (supports `active`/`silenced`/`inhibited` filters) | | `list_contact_points` | `GET /api/v1/provisioning/contact-points` | These endpoints belong to Grafana's stable legacy REST API and are not affected by App Platform API version discovery (#221). This exact code has been running against Grafana 12.4.3 in a private deployment for two weeks. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary - Add a searchable Weights & Biases provider backed by the official W&B MCP server. - Map the server’s 22 default tools to typed Open Connector actions and discover the subset exposed by each deployment. - Use the hosted `https://mcp.withwandb.com/mcp` endpoint by default, with an optional full MCP endpoint for W&B Dedicated and self-hosted deployments. - Authenticate with the W&B API key as a Bearer token and route MCP traffic through the guarded provider fetch path. - Validate Streamable HTTP tool schemas with the Worker-safe validator and normalize structured, JSON-text, and plain-text tool results. ## Validation - `npm run generate:catalog` - `npm run fix-check` - `npm test` (60 files, 591 tests) - `npm run build` --------- Co-authored-by: l1shen <648952316@qq.com>
## Summary - wire the n8n executor, proxy, credential verification, and runtime URL validation to the existing `OOMOL_CONNECT_ALLOW_PRIVATE_NETWORK` opt-in - preserve public-hostname/DNS validation and always-blocked targets - add focused regression coverage for default rejection, explicit opt-in, and blocked ranges ## Validation - `npm run fix-check` - `npx vitest run src/providers/n8n/executors.test.ts` - `git diff --check` The change was trial-verified against a self-hosted n8n instance reachable over a trusted private network. No generated graph artifacts are included. --------- Co-authored-by: Kevin Cui <bh@bugs.cc>
## Summary - replace the monolithic Cloudflare `/catalog/apps.json` build output with a versioned `/catalog/index.json` - write deterministic, sequential catalog chunks capped at 4 MiB by their final UTF-8 byte size - load all indexed chunks through the existing `ASSETS` binding while preserving `CatalogStore` and public API response shapes - retain a loader fallback to `/catalog/apps.json` for deployments using the previous asset format - request catalog assets as JSON so a missing index is not treated as an SPA navigation This is the catalog follow-up requested in #235 after the executable registry reduction merged in #237. ## Why On the current `main` catalog (1,210 providers and 12,894 actions), the generated `apps.json` is 26,030,332 bytes (24.8245 MiB), leaving only 184,068 bytes before Cloudflare's 25 MiB per-asset limit. The provider catalog already consists of individual source files, so combining everything into one deployment asset creates an avoidable single-file limit. ## Cloudflare build comparison Measured on the same current `main` catalog with Wrangler 4.115.0, `--dry-run`, and `--minify`: | Measurement | Before | This PR | | --- | ---: | ---: | | Largest catalog asset | 26,030,332 bytes (24.8245 MiB) | 4,192,730 bytes (3.9985 MiB) | | Catalog asset files | 1 | 8 (1 index + 7 chunks) | | Total static assets | 11 | 18 | | Worker minified upload | 11,672.94 KiB | 11,674.91 KiB | | Worker gzip | 2,805.33 KiB | 2,806.08 KiB | New builds do not emit the complete `apps.json`. The small Worker increase is the index validation, compatibility fallback, and chunk loading logic. ## Compatibility and validation - provider files are sorted by filename before chunking for deterministic output - chunk limits include JSON brackets, commas, trailing newline, and UTF-8 multibyte characters - a provider too large for one chunk fails the build with its filename and byte count - index version, fields, chunk names, duplicate names, chunk shapes, and provider count are validated before creating the catalog - indexed chunks are loaded in parallel and flattened in index order - a missing `index.json` falls back to the legacy `apps.json`; new builds emit only the indexed format - existing executable-service resolution from #237 remains unchanged Checks: - `node scripts/generate-catalog.ts` — 1,210 apps / 12,894 actions - `oxlint . --fix` - `oxfmt .` - `node scripts/typecheck.ts src scripts-all examples` - `vitest run` — 61 test files and 613 tests passed - Wrangler dry-run builds for both the current baseline and this branch Closes #235 --------- Co-authored-by: Kevin Cui <bh@bugs.cc>
## What Adds a `generic_imap` provider so that any mailbox exposing IMAP over implicit TLS (port 993) can be connected with an application password, regardless of the mail host. Open Connector currently ships provider-specific mail integrations (`qq_mail`, `netease_mail`) whose IMAP/SMTP hosts are hardcoded. A mailbox on any other host (OVH, Fastmail, self-hosted Dovecot, corporate mail, ...) cannot be connected at all today. ## How - The provider is built on the shared `MailRuntimeConfig` factory (`src/mail/imap-smtp/`), so it exposes the same 12 actions as the existing mail providers (`send_email`, `list_folders`, `search_emails`, `get_email`, `download_attachment`, `mark_email_read`, `mark_email_unread`, `move_email`, `delete_email`, `get_folder_status`, `reply_email`, `forward_email`). No new mail plumbing. - Credential fields: `email`, `password` (application password, secret), `imapHost` (required), `smtpHost` (optional, defaults to the IMAP host with the `imap.` prefix replaced by `smtp.`, port 465). - Marked `nodeOnly` like the other mail providers: IMAP/SMTP sockets are not reliable from Cloudflare Workers, and the description says so. The generated Cloudflare registry excludes it. - `testAction` is `list_folders`, so the connection test authenticates against the user-supplied host before the credential is saved. ## SSRF hardening This is the first mail provider whose hosts come from user input, so the hosts are guarded in two places: when the credential is read, and again when the socket is opened. ### At credential time - Each host must be a bare dotted hostname (no scheme, userinfo, port, or path), so nothing can smuggle a different target past the URL parser. IP literals still match this syntax gate on purpose: they are then classified by the shared guard. - The host is then passed to the shared `assertPublicHttpUrl` (`src/core/request.ts`), so the provider inherits the existing cloud-metadata, loopback, link-local, reserved-range, and IPv6 blocklists rather than re-implementing them. - Private targets (RFC 1918, CGNAT `100.64.0.0/10`, private-suffix hostnames) stay blocked unless the deployment opts in through `OOMOL_CONNECT_ALLOW_PRIVATE_NETWORK`, mirroring the existing Dokploy behaviour. Loopback and metadata targets stay blocked even then. The flag is read per call via `isPrivateNetworkAccessAllowed()`, so it always reflects the current bootstrap configuration. - `docs/configuration.md` is updated to list the IMAP Mailbox host next to Dokploy as an opt-in consumer of the private-network flag. ### At connect time (DNS rebinding) A hostname check alone is not sufficient: the DNS record can point at an internal address by the time the socket is opened, and IMAP/SMTP connect over raw TLS, so the guarded fetch never sees these hosts. `pinMailHost` in `src/mail/imap-smtp/protocol.ts` closes that window: - the host is resolved (`dns.lookup` with `all: true`), and **every** returned address is screened with the shared `isBlockedIpAddress`. A host answering with one public and one private address is rejected outright, since the connection library is free to pick either one; - the connection is then made to the resolved literal address, with the hostname carried separately as `servername`. Both `imapflow` and `nodemailer` skip their own lookup when the host is already an IP (`net.isIP`), so no second, unchecked resolution can happen, while SNI and certificate verification still run against the name the user typed; - an IP-literal host skips resolution but is still screened; - `OOMOL_CONNECT_ALLOW_PRIVATE_NETWORK` is honoured here too, and loopback/metadata stay blocked even when it is set. This is gated behind a new `enforceHostNetworkPolicy` flag on `MailRuntimeConfig`, set only by `generic_imap`. Providers with hardcoded hosts (`qq_mail`, `netease_mail`) keep their current behaviour exactly: no resolution, no extra DNS round-trip. ## Tests `src/providers/generic_imap/network-access.test.ts` (9 cases) covers the credential-time policy: - private/reserved IP literals rejected by default (incl. `169.254.169.254`, `100.100.100.200`, `0.0.0.0`); - public mailbox hosts accepted; - loopback/link-local/metadata still blocked when `OOMOL_CONNECT_ALLOW_PRIVATE_NETWORK=true`, while RFC 1918/CGNAT/private hostnames become allowed; - non-bare-hostname inputs rejected (URL, `host:port`, userinfo, path, trailing dot, IPv6 literal, whitespace); - the guard applied to the SMTP host too, including the derived one; - host normalization (lowercasing) and the derived SMTP host following the normalized value; - the offending field named in the rejection message. `src/mail/imap-smtp/host-pinning.test.ts` (9 cases) covers the connect-time policy: - a hostname resolving to loopback or to the cloud-metadata address is rejected, and no client is constructed; - a host answering with one public and one private address is rejected as a whole; - the resolved address is pinned as `host` while the hostname is preserved as `servername`, for IMAP and for SMTP; - providers without the flag are untouched: the hostname is passed through and no resolution happens; - a private address is accepted when the deployment opted in, while loopback stays blocked in that same mode; - an IP-literal host is screened without being resolved. ## How to test ```sh npm install npm run typecheck # regenerates the provider registries, then typechecks npm test # full suite npx vitest run src/providers/generic_imap/network-access.test.ts src/mail/imap-smtp/host-pinning.test.ts ``` For a live check, connect an "IMAP Mailbox" credential from the catalog UI with any IMAP-enabled mailbox and an application password; the `list_folders` test action runs on save. Trying `127.0.0.1` or `169.254.169.254` as the IMAP host is rejected with a 400 naming the field, and a hostname that resolves to either is rejected when the connection is attempted. --------- Co-authored-by: Kevin Cui <bh@bugs.cc> Co-authored-by: l1shen <648952316@qq.com>
The `groqcloud` provider currently exposes chat completions and model listing only, so Groq's speech-to-text endpoints are unreachable — the provider has no proxy fallback either. | Action | Endpoint | |---|---| | `create_audio_transcription` | `POST /audio/transcriptions` | | `create_audio_translation` | `POST /audio/translations` | Audio is supplied inline as base64, or as a public URL that GroqCloud fetches itself through its native `url` field. Caller URLs are checked with `assertPublicHttpUrl` before being forwarded. The size cap applies to inline uploads; GroqCloud enforces its own limit on URLs it fetches. Translation is restricted to `whisper-large-v3`. Requesting `whisper-large-v3-turbo` there returns `The model 'whisper-large-v3-turbo' does not support 'translate'`, so the restriction is in the schema rather than surfacing as a runtime error. Exercised against the live API with a real key: inline base64 upload, forwarded URL, `verbose_json` with word-level `timestamp_granularities[]`, `language`/`prompt`/`temperature` passthrough, and both endpoints. --------- Co-authored-by: l1shen <648952316@qq.com>
Adds a Z-Library provider with search, metadata, download-to-file, download limits, and recent books actions. Closes #244
## Summary - add a locally executable Komari provider using the stable JSON-RPC 2.0 endpoint in Komari 1.3.2 - expose all 71 user-facing Komari 1.3.2 JSON-RPC methods as Actions: 12 public and 59 admin - support Bearer API key validation, reverse-proxy base paths, and deployment-gated private-network instances - declare each full JSON-RPC method name as the Action's provider-native capability - keep client enrollment tokens out of node/client results except for the explicit `get_client_token` Action - redact session tokens and IP addresses; expose stable SHA-256 session IDs for safe session revocation - add normalized handling for list results, mutation success responses, load history, and ping history ## Compatibility - targets the complete `public:*` and `admin:*` JSON-RPC surface in Komari 1.3.2 - excludes agent ingestion, WebSocket terminal/reporting, login/OAuth callbacks, multipart or binary endpoints, and unreleased Komari 1.4 plugin-only APIs - uses `/api/rpc2` ## CodeRabbit follow-up - explicitly thread the private-network policy through credential validation and execution - hoist node field allowlists and use a separate ping-task summary schema - replace conditional load-history spreads with an explicit typed result - declare provider-native capabilities and tighten required input schemas - redact session secrets while preserving deletion through stable IDs resolved server-side - cover HTTP failures, invalid JSON, missing JSON-RPC results, aborts, parameter conversion, capabilities, and safe session handling ## Validation - `npm run generate:catalog` - `npm run fix-check` - `npm test` (71 files, 749 tests passed) --------- Co-authored-by: l1shen <648952316@qq.com>
Adds a category dropdown to the provider browser with per-category counts, so users exploring by capability can filter instead of scrolling the flat list. Also renders each provider's short description in its card when one exists. Closes #219. --------- Co-authored-by: l1shen <648952316@qq.com>
## Summary - give the Cloudflare MCP provider an explicit Cloudflare Workers favicon - keep its GitHub repository as the provider homepage - prevent the generic homepage fallback from rendering the GitHub icon ## Validation - `npm run generate:catalog` - `npm run fix-check`
`expires_in` is optional on a refresh response and plenty of providers only send it on the initial grant. When it is missing the stored credential ends up with no `expiresAt`, and a credential with no expiry is never considered expired, so it is never refreshed again. There is no retry on a 401 either, so once the access token actually lapses every call through that connection fails until someone reconnects by hand. One refresh is enough to get there. The refreshed credential now falls back to the lifetime the provider last reported, which is already kept in the credential metadata. If no lifetime was ever reported the expiry stays unset, same as today. Worth noting why the fallback is not the previous `expiresAt`: a refresh only runs once that timestamp is in the past, so reusing it would mark the new token expired immediately and trigger a refresh on every request. --------- Co-authored-by: l1shen <648952316@qq.com>
Upsales declares record IDs as positive integers, but the runtime ran
them through a string cast that returns undefined for numbers, so every
by-ID action failed on input its own schema accepts.
```
get_company { id: 42 }
before: id is required.
after: GET /accounts/42
```
Same file, two smaller mismatches: the list actions declare a `filters`
object that never reached the query string, and the contact actions
declare `usingFirstnameLastname` while the runtime read
`useFirstNameLastName`. Filters now go out as query parameters keyed by
field name. A filter key that would overwrite a parameter the runtime
sets is rejected, so it cannot replace the API token.
I left `offset` alone. It is still declared and unused, but I could not
reach the Upsales API docs to confirm the parameter name for skip based
paging.
---------
Co-authored-by: l1shen <648952316@qq.com>
Add Slack message search using Slack's user-scoped OAuth grant while preserving the existing bot grant for other actions. Store and rotate the user and bot tokens together through the Slack-specific OAuth path. Co-authored-by: suin <suinyeze@gmail.com>
… real cap (#271) Fixes #269. `GET /im/v1/messages` rejects `page_size > 50` with **Feishu 99992402 (field validation failed)**, so `list_messages`' declared maximum of 100 admits inputs the provider can never serve — a consumer that trusts the schema's bound gets a hard runtime failure on every call. `list_thread_messages` (the same endpoint through the thread container, in `im-actions.ts`) already declares 50, so this brings `list_messages` in line: a dedicated 50-capped `pageSize` for it, leaving the shared 100 for the endpoints that genuinely accept it (`list_chats`, `list_chat_members`, `search_chats`). Verified live against a real workspace (gateway from this repo's main): `pageSize: 100` → 99992402 on every chat; `pageSize: 50` → success. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Map the declared update date range to Monday GraphQL arguments, forward pagination, enforce the documented date formats and page-size limits, and remove unsupported filter fields.
A reply set `References` to the parent's Message-ID and nothing else. RFC 5322 wants the parent's own chain with the parent appended, so replying to anything deeper than the first message split the thread in clients that follow the header. ``` parent References: <a> <b>, parent Message-ID: <c> before: references: "<c>" after: references: "<a> <b> <c>" ``` The chain was not available to fix before now. `References` is not part of the IMAP envelope, so it is fetched with the message and parsed onto the fetched result. Folded header lines are joined first, since a long chain always arrives wrapped. `forward_email` is unchanged, as a forward starts its own thread. --------- Co-authored-by: l1shen <648952316@qq.com>
`optionalIntegerOrNull("")` returned `0` rather than `null`, because
`Number("")` is `0` and passes `Number.isInteger`. A blank optional
field reached the provider as a real zero.
```
optionalIntegerOrNull("") before: 0 after: null
optionalIntegerOrNull(" ") before: 0 after: null
```
Blank strings are now reported as missing. That matches the sibling
helpers `integer` and `optionalIntegerLike`, which already exclude an
empty string before parsing. Eight providers read optional integers
through this helper.
) `list_linear_issues` declared `original_cursor` and `cursor_was_corrupted` as inputs. The runtime reads only `after`, `first`, `project_id` and `assignee_id`, so both were accepted and then ignored. `cursor_was_corrupted` also reads as diagnostic output rather than something a caller supplies, which invites an agent to set it and be silently ignored. Neither name appears anywhere else in the repo. Pagination through `after` and `first` is unchanged.
Allow deployments behind corporate VPNs or split DNS to trust narrowly scoped egress hostnames while retaining hard SSRF blocks for loopback, link-local, metadata, and other unsafe targets.\n\nDocument the deployment-wide security boundary and cover IPv4, IPv6, redirects, and hostname matching.
test(providers): remove transferred private coverage
## Summary
Extend the TickTick provider with two capabilities that the official
TickTick Open API supports but the current catalog does not expose.
## Changes
### 1. Task tags (`create_task` / `update_task`)
Add a `tags: string[]` field to the `createTaskInput` /
`updateTaskInput` schemas and forward it in the request body. The
TickTick Open API Task object includes `tags`, but the field was
previously absent from the schema, so any attempt to create or update a
tagged task failed schema validation with `additionalProperties` errors.
### 2. Batch task creation (`batch_add_tasks`)
Add a `batch_add_tasks` action backed by `POST /open/v1/task/batch` with
an `{ "add": [...] }` body (up to 50 tasks per request). Each task
reuses `createTaskInput`, so tags are supported in batch mode too. The
handler tolerates the batch endpoint's varying response shapes (array,
`{ add }`, `{ tasks }`, or empty body) and returns a `createdCount` so
callers can confirm how many tasks were created even when the response
omits the created task list.
## Verification
- `npm run typecheck`, `npm run lint`, `npx oxfmt --check` — all pass
- `npm test` — 713 tests passed (68 files)
- Live-tested against the TickTick API:
- tasks created/updated with tags persist them (`filter_tasks` by tag
returns them; tags are lowercased by TickTick, which is expected)
- `batch_add_tasks` creates all tasks with per-task tags
---------
Co-authored-by: CheerChen <meetcheerego@gmail.com>
## Summary
This PR expands the Mux Video provider beyond the existing on-demand
asset lifecycle. It adds asset metadata updates, Direct Upload
management, and Playback ID lookup based on the official Mux Video API.
## What changed
- Added `mux.update_asset` for updating asset `passthrough` and customer
metadata.
- Added `mux.create_direct_upload` for creating signed upload URLs
without proxying video bytes through OpenConnector.
- Added `mux.get_direct_upload` for checking upload status and the
resulting asset ID.
- Added `mux.list_direct_uploads` with page-based pagination.
- Added `mux.cancel_direct_upload` for cancelling uploads that are still
waiting.
- Added `mux.get_playback_id` for resolving a Playback ID to its
associated asset or live stream.
- Added runtime request mapping, input validation, response
normalization, and regression tests for the new actions.
- Preserved the existing Mux Basic Auth flow, guarded provider fetcher,
permission model, and upstream error handling.
## API research
The selected actions correspond to these official Mux Video API
endpoints:
- `PATCH /video/v1/assets/{ASSET_ID}`
- `POST /video/v1/uploads`
- `GET /video/v1/uploads/{UPLOAD_ID}`
- `GET /video/v1/uploads`
- `PUT /video/v1/uploads/{UPLOAD_ID}/cancel`
- `GET /video/v1/playback-ids/{PLAYBACK_ID}`
References:
- https://docs.mux.com/api-reference/video
-
https://github.com/muxinc/mux-node-sdk/blob/master/src/resources/video/api.md
## Validation
- `npm run generate:catalog`
- `npm run typecheck`
- `oxlint src/providers/mux/actions.ts src/providers/mux/runtime.ts
src/providers/mux/runtime.test.ts`
- `npm test` — 69 test files and 737 tests passed
Direct Upload responses expose the signed upload URL to the caller; the
connector does not handle or relay the media bytes.
---------
Co-authored-by: Valor <210239105+zrh805@users.noreply.github.com>
Co-authored-by: Kevin Cui <bh@bugs.cc>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )