release: promote dev → master (post-v0.4.0)#438
Merged
Merged
Conversation
…398) * feat(oauth): per-org oauth_redirect_url + opt-in white-label switch Replace the per-request white-label `redirect_uri` override and the `oauth_callback_allowed_hosts` allow-list with a single admin-set per-org `oauth_redirect_url`, opted into per request via a `use_org_redirect` bool. Why a per-request opt-in (not a global per-org rule): a global override would break overslash's own dashboard connect flows ("Try it"/Connect/reconnect/ upgrade-scopes), which rely on the default `/v1/oauth/callback` so the browser completes the flow in place. The bool defaults to false, so those flows are untouched in white-label orgs; only the partner sets `use_org_redirect: true`. Backend: - migration 081: add `orgs.oauth_redirect_url`, drop `oauth_callback_allowed_hosts` - kernel: `use_org_redirect` resolves the org URL (400 if unconfigured), else the default callback; flow-row contract unchanged (Some -> exchange, NULL -> callback) - drop the per-request `redirect_uri` fields on `POST /v1/connections`, `/upgrade_scopes`, and MCP `create_service` (now `use_org_redirect` / `connect_use_org_redirect`); remove `callback_host_allowed` - settings API: `GET/PATCH /v1/orgs/{id}/oauth-redirect-settings` (admin, audited, URL validated on write via the shared `parse_redirect_uri`) - keep `/v1/oauth/exchange`, `include_raw`/wrap-raw, default callback path Dashboard: - org settings: replace the OAuth callback hosts list with a single OAuth redirect URL input; the dashboard's own Connect flows still use the default callback Tests: rewrite as `oauth_org_redirect_url.rs` (org-url-as-redirect, missing-URL 400, default-callback fallback, exchange/guard, upgrade-scopes, settings API). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(dashboard): update org redirect-url screenshot scenario Rename the stale 'OAuth callback hosts' screenshot script/helper to the new single 'OAuth redirect URL' card (setOauthRedirectUrl → oauth-redirect-settings). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cloud Build runs each build on a fresh ephemeral VM, so the previous `docker build` steps always started from a cold layer cache and recompiled every Rust dependency from scratch (~6 min for the API image). The Dockerfiles are carefully structured around a dependency- caching layer (dummy-source trick), but with no persistent cache that optimization was dead on arrival. Replace the `docker build` + `docker push` steps in all three build modules (overslash-api, oversla-sh, metrics-exporter) with a single Kaniko step using `--cache=true`. Kaniko stores each layer — including the builder-stage dependency layer — as a content-addressed blob in a dedicated cache repo (<dest>/cache), keyed by command+input hash, so unchanged layers are reused across builds. It also pushes the image directly, so the separate push step is no longer needed. The cache repo lives in the same Artifact Registry repository and is derived from project_id/repository_name, so dev and prod caches are isolated automatically and no extra IAM is required. --cache-ttl=168h bounds staleness to one week. Caching does not depend on a :latest tag existing, so first builds (and fresh environments) work unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The registry previously had only a KEEP cleanup policy (keep 10 most recent), which in Artifact Registry is a no-op without a paired DELETE policy — so nothing was ever deleted and the repo grew unbounded (~11.6 GB on dev). With Kaniko layer caching now pushing cache blobs on every build, that growth would accelerate. Add a DELETE policy that removes artifact versions older than a configurable threshold (default 30 days), covering both deployed images and Kaniko cache layers. The threshold must stay above the Kaniko --cache-ttl (168h): any cache layer Kaniko would reuse is re-pushed within the TTL and is therefore always younger than the delete age, so in-use cache is never pruned. The existing keep-recent policy still protects the 10 newest versions per package as a rollback safety net and takes precedence over the DELETE policy. Both the delete age and a dry-run toggle are exposed as variables with defaults, so callers (dev/prod) need no changes; flip cleanup_dry_run to preview deletion scope before enforcing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kaniko otherwise infers the cache repository from a --destination, but both destinations carry a dynamic $COMMIT_SHA tag. Relying on Kaniko's implicit tag-stripping to land on a stable cache path is fragile; pin --cache-repo to a fixed <repo>/<image>/cache path in all three build modules so layer caching reliably reuses blobs across commits. Addresses Sentry review on #399. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ci(infra): Kaniko layer caching + age-based registry cleanup
…ctions/import) (#400) * feat(oauth): white-label connections as a token vault (POST /v1/connections/import) Partners that already own their OAuth (e.g. Overfolder) no longer route the dance through overslash. They run authorize + code-exchange themselves and `POST /v1/connections/import` the resulting tokens; overslash stores the connection (identical row to an orchestrated callback), refreshes it, and injects it at execution — and issues no `redirect_uri`. This dissolves the per-provider redirect problem entirely (overslash issues no redirect URI). Refresh mode is fixed per import: - self-refresh: a pinned `byoc_credential_id` (validated at import — a bad id 400s here, not at first refresh); overslash refreshes via the refresh-token grant, hard-pinned to that client (never the cascade). - integration-managed: a null `byoc_credential_id` flags `connections.integration_managed`. Overslash injects the stored token until expiry, then surfaces `reauth_required` marked integration-managed with NO reconnect link (auth_url omitted, provider + integration_managed: true) and fires a `connection.refresh_required` webhook — the partner refreshes and re-imports. It never refreshes and never borrows the env/org `OAUTH_*_CLIENT` cascade (a refresh token is valid only against its issuing client). Also covers opaque bearer tokens / PATs with no client at all. Re-import is idempotent, keyed on (identity, provider, account_email) — it updates the row in place rather than accreting duplicates each refresh cycle; a distinct account_email vaults a second account. Removed (now obsolete / dead once no flow can set a custom redirect): - all of #398: `orgs.oauth_redirect_url`, the `use_org_redirect` switch, the `/v1/orgs/{id}/oauth-redirect-settings` endpoints, the dashboard section. - `POST /v1/oauth/exchange` + the callback custom-redirect guard. - `include_raw` / the raw authorize-URL surface on `POST /v1/connections`, `/upgrade_scopes`, MCP `create_service`, the `raw` envelope fields, and the MCP chat-delivery strip — partners build their own authorize URLs now. - `oauth_connection_flows.redirect_uri`. Migration `082_connection_token_vault` adds `connections.integration_managed` and drops `orgs.oauth_redirect_url` + `oauth_connection_flows.redirect_uri`. Dashboard: connection detail surfaces the integration-managed credential source; org settings drops the OAuth redirect URL card. Tests: new `connection_import.rs` drives a fake integration partner through every path (integration-managed inject-then-reauth via a mock upstream, self-refresh BYOC validation, idempotent/multi-account re-import, expiry resolution, on_behalf_of binding, input validation). Reauth/oauth_x/ services-auto-connect suites updated for the no-`raw` envelope shape. Design: docs/design/white-label-token-vault.md; SPEC §7; DECISIONS D20. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(oauth): reject refresh-mode changes on connection re-import Seer review (HIGH): on re-import, a supplied `byoc_credential_id` that would change a connection's refresh mode was validated and then silently discarded (the update path only writes tokens/scopes), so a caller expecting self-refresh on an integration-managed row got a misleading 200 with the old mode. The mode is fixed at first import by design. Make that contract explicit: - omitting `byoc_credential_id` stays a token-only update preserving the mode (the hot path for integration-managed re-import); - supplying a `byoc_credential_id` that would flip the mode (integration-managed → self-refresh) or re-pin to a different client now returns 400 with a clear "delete and re-import to change" message. Adds a regression test (integration-managed → self-refresh re-import → 400) and documents the behavior in the design doc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(oauth): make import match mode-aware so it never overwrites orchestrated connections Seer review (HIGH): an emailless import resolves the re-import target via the `(identity, provider)` fallback in `find_for_import`, which matches on NULL account_email. An orchestrated connection whose userinfo fetch left account_email NULL could therefore be matched and have its tokens overwritten — and the previous mode guard only fired when `byoc_credential_id` was supplied, so an integration-managed import slipped through. Make the match mode-aware: - email-keyed match (caller named the account): an in-place update is intended, so a mode/client change is rejected with 400 (the prior fix, generalized). - emailless heuristic match: reuse the row ONLY when it is the same kind of vault connection (same mode + same pinned client). On a mismatch — notably an orchestrated connection — fall through to creating a fresh row instead of clobbering it. Adds a regression test (emailless integration-managed import leaves a NULL-email orchestrated connection untouched and creates a separate row) and documents the match semantics in the design doc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(oauth): preserve token_expires_at on a token-only connection re-import Seer review (MEDIUM): a re-import that carries no fresh `expires_at`/`expires_in` passed `None` to `update_tokens_and_scopes`, nulling `token_expires_at`. For an integration-managed connection that makes it look perpetually valid — it would never surface `reauth_required` and would keep injecting a token that has actually expired upstream. Fix surgically in the import kernel (not the shared repo fn the orchestrated upgrade callback also uses): on a re-import, fall back to the existing `token_expires_at` when the caller supplies no fresh expiry; a supplied expiry still overrides it. Adds a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(oauth): preserve existing scopes on a token-only connection re-import Seer review (CRITICAL): `ImportConnectionInput.scopes` defaults to `[]`, and the update path overwrote `scopes` unconditionally — so a token-only re-import that omitted `scopes` wiped the connection's granted scopes, 403ing every subsequent scope-gated action call. Mirror the expiry fix: on a re-import, preserve the existing scopes when the caller supplies none; a non-empty `scopes` still overrides. The effective scope set now also flows to the response, audit detail, and webhook payload. Adds a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(oauth): skip upgrade-URL mint for integration-managed missing_scopes Seer review (LOW): when an action needs a scope an integration-managed connection lacks, `check_required_scopes` called `mint_upgrade_auth_url` — which an integration-managed connection can't use (Overslash holds no client), doing wasted work and leaving a stray `oauth_connection_flows` row. Guard it: for integration-managed connections, return `missing_scopes` with `auth_url`/`short` omitted and no mint — the integration broadens the grant and re-imports. Adds a regression test (403 missing_scopes, no auth_url, zero flow rows created). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(oauth): reject /upgrade_scopes for integration-managed connections Seer review (MEDIUM): `POST /v1/connections/{id}/upgrade_scopes` didn't guard against integration-managed connections, so it would call kernel_create_connection and mint an orchestrated OAuth flow (incorrect when a fallback client exists, a generic 400 when not). The companion `check_required_scopes` fix already skips the mint for these connections, but the REST endpoint — which the missing_scopes `upgrade_url` points at — needed the same guard. Guard the handler: an integration-managed connection returns a clear 400 directing the caller to broaden the grant and re-import via POST /v1/connections/import. Adds a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(oauth): nullable connection scopes — unknown gets benefit of the doubt `connections.scopes` was `NOT NULL DEFAULT '{}'`, so an import that omitted `scopes` recorded an empty set — indistinguishable from "genuinely no scopes" — and the action scope-gate then 403'd `missing_scopes` on a token that was actually valid (Overslash can't know an imported token's real grants). Make scopes nullable to represent "unknown" distinctly (migration 083_connection_scopes_nullable; `ConnectionRow.scopes: Option<Vec<String>>`): - `POST /v1/connections/import` `scopes` now defaults to `null` (unknown), not `[]`. Re-import preserve-on-omit keeps the recorded set. - The scope-gate (`check_required_scopes`) and the dashboard credential-health badge (`derive_credentials_status` via a new `ScopeKnowledge` enum) treat `null` as benefit of the doubt — covering everything — so unknown-scope imports aren't falsely 403'd; a real shortfall still surfaces upstream. Orchestrated flows always record the concrete granted set, so they keep precise gating. - The `missing_scopes` envelope now reports both `required` (the action's full set) and `missing` (the delta), so a caller sees the target, not just the gap. Tests: import-without-scopes → recorded null + action executes (benefit of the doubt); known-but-insufficient scopes → 403 with required+missing; re-import preserves scopes; `ScopeKnowledge::Unknown` classifies Ok. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…te-label partners (#401) * feat(oauth): expose provider OAuth metadata + template scopes for white-label partners Two read-only additions a white-label partner (Overfolder) needs to run the token-vault flow generically (PR400 made it the token vault; partners run the OAuth dance themselves): - GET /v1/oauth-providers/{key}: full OAuth metadata for one provider (authorization_endpoint, token_endpoint, userinfo_endpoint, supports_pkce, supports_refresh, token_auth_method, extra_auth_params, default_identity_scopes) — straight from the oauth_providers row, so partners build authorize URLs + exchange codes without hardcoding per-provider config. - TemplateDetail.scopes: union of every action's required_scopes, so partners request exactly the scopes a service needs on the authorize URL. Both are read-only; secrets stay on the partner side. WriteAcl on the provider endpoint (mirrors the sibling list /v1/oauth-providers). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(oauth): cover provider detail endpoint + template scopes field PR #401 added GET /v1/oauth-providers/{key} and TemplateDetail.scopes with no tests, failing codecov/patch. Add integration coverage: - oauth_provider_detail_exposes_full_metadata: asserts the metadata fields (endpoints, flags, token_auth_method, identity scopes) and that no client secrets leak through the catalog endpoint. - oauth_provider_detail_unknown_key_404: unknown key -> 404. - test_global_template_detail_includes_scopes: google_calendar's required-scope union surfaces on the global detail (get_template path). - test_org_template_detail_includes_scopes: DB-tier template detail carries the scopes array (db_row_to_detail path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* wip(oauth): white-label headless OAuth — split refresh/flow-ownership axes
Recovered WIP for URL-less auth-recovery for white-label / BYOC orgs.
- migration 084: orgs.headless flag (per-org capability)
- migration 085: drop connections.integration_managed (split conflated axes)
- DB layer changes for org headless flag + drop integration_managed field
- platform_connections import derivation changes
Incomplete — sqlx cache not yet regenerated, error envelopes + auth-recovery
branches still pending. Committed to preserve work before continuing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(oauth): finish headless OAuth — URL-less auth-recovery for white-label orgs
Split the two axes the old integration_managed flag conflated:
- refresh is now purely structural (pinned byoc_credential_id self-refreshes;
null refreshes via the org/env cascade) — no stored boolean
- flow-ownership is a new per-org `headless` capability
Imports now require a byoc_credential_id (400 otherwise). For headless orgs,
auth-recovery (reauth_required / needs_authentication / missing_scopes) returns
typed URL-less envelopes carrying `headless: true` + provider/scopes/email and
mints NO gated /connect-authorize link and NO oauth_connection_flows row. The
gate stays fully intact for non-headless dashboard customers.
- error.rs: reshape the three envelopes; `headless` discriminator only on the
URL-less variant; drop integration_managed
- actions/auth.rs: org_is_headless() gate at reauth/needs_auth/missing_scopes;
delete integration-managed branches + helpers
- routes/orgs.rs: admin-only GET/PATCH /v1/orgs/{id}/headless
- connections.rs: require BYOC on import, drop integration_managed DTOs, block
upgrade_scopes for headless orgs
- oauth.rs / client_credentials.rs: drop integration-managed token path + source
- rewrite connection_import.rs for the BYOC-required contract; fix fixtures
- regen sqlx offline cache
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(oauth): add headless_oauth.rs integration tests
URL-less envelopes for reauth_required / needs_authentication / missing_scopes
on a headless org (headless: true, no auth_url/short/upgrade_url, no flow row),
plus a non-headless regression that still mints the gated link.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(oauth): document headless capability + BYOC-required import
- white-label-token-vault.md: rewrite refresh section (self-refresh only),
add "Headless orgs & URL-less auth-recovery", update import contract,
resolved decisions; drop integration-managed mode
- SPEC.md §659, STATUS.md, docs/design/INDEX.md: BYOC required, headless flag,
integration_managed removed
- DECISIONS.md: annotate D20 as partially superseded; add D21 (headless
capability replaces integration_managed)
CHANGELOG.md left to release-please (auto-generated per D19). Breaking change
(import requires byoc_credential_id; integration_managed dropped from API)
flagged in the PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(test): rustfmt headless_oauth.rs
cargo fmt --check fix — the test file was committed with --no-verify and
skipped formatting (long .expect() chain at line 327).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(db): force scopes nullable override in single-table connection SELECTs
CI Test shard caught a real offline-build bug: get_by_id / find_for_import /
get_by_ids decode the nullable `scopes` column (migration 083) as non-Option
under SQLX_OFFLINE and panic ("column 7: unexpected null") on a connection
imported without scopes — surfacing as a 500 from upgrade_scopes. The committed
.sqlx cache correctly marks scopes nullable, but the sqlx macro mis-decodes the
bare single-table column (the JOINed `c.scopes` queries in user_connections.rs
are unaffected). Add the explicit `scopes AS "scopes?: Vec<String>"` override.
Only manifested with a clean offline build (CI); local incremental builds
compiled against the live DB and masked it. Verified: full overslash-api suite
(1092 tests) passes under a clean SQLX_OFFLINE=true nextest run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(oauth): address Seer review — per-request pool + omit null auth_url
- org_is_headless now reads via the request's pool (state.db(ext) / scope.db())
instead of &state.db, so the headless lookup hits the right database under
the shared-router test harness (thread ext through the reauth/needs_auth
recovery path; check_required_scopes uses its OrgScope's pool). MEDIUM.
- NeedsAuthentication non-headless serialization omits `auth_url` when None
instead of emitting `"auth_url": null`, matching the ReauthRequired /
MissingScopes contract (error.rs + routes/actions/mod.rs). LOW.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Factory <factory@overslash.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The action-execution auth resolver looked OAuth connections up at the *calling* identity (`UserScope::new(org_id, calling_identity_id)`), so a child agent never inherited its owner user's connection. With Overfolder binding connections to the user identity (connect/reauth flow + `on_behalf_of` import), an agent holding its own stale connection got `reauth_required` for a row the user's reauth flow could never heal — an infinite reauth loop (dev trace d57fe333: agent connection f46cabbe broken, user connection 85844f1a healthy). Resolve connections at the OWNER identity instead. `resolve_service_auth`, `check_required_scopes`, and the auto-resolve fall-through of `resolve_instance_auth` now build their `UserScope` from `ceiling_user_id` (owner), and client-credential resolution plus the reauth_required / needs_authentication / missing_scopes URL minting key on the owner too — so a freshly minted connection lands on the owner and one reauth heals every agent. Service-instance binding and permission/group-ceiling checks stay on the calling agent. Connections remain identity-scoped in storage; this is a read-path change only. Owner-only, no caller fallback. Drops the now-unused `identity_id` param from `resolve_request`. Adds DECISIONS.md D22, new owner_scoped_connections.rs integration tests, and re-targets connection seeds to the owner identity across the affected existing tests (including the #[ignore]d E2E suites). Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ships services/google_tasks.yaml — an OpenAPI 3.1 template for Google
Tasks reusing the existing DB-seeded `google` OAuth provider. Full CRUD
over task lists and tasks, with a per-operation OAuth scope split:
- read actions (list/get) require only .../auth/tasks.readonly
- write/delete actions require the full .../auth/tasks scope
so a read-only connection is sufficient for reads but is rejected
(403 missing_scopes) on writes. Uses scope_param: tasklist for
resource-level scoping and resolve:{get,pick} for human-readable names
in approval UIs, mirroring google_calendar.yaml.
Adds crates/overslash-api/tests/google_tasks.rs — e2e tests against the
in-process mock upstream proving the scope split in both directions, and
bumps the shipped-template count in STATUS.md.
Co-authored-by: Factory <factory@overslash.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an OpenAPI 3.1 service template for Google Keep, reusing the shared `google` OAuth provider so a single Google connection covers Keep alongside Gmail/Calendar/Drive. Exposes Core CRUD over the official Keep API (keep.googleapis.com): list_notes, create_note, get_note, delete_note. - Single `auth/keep` scope (no per-op keep.readonly gating: the scope gate matches granted scopes literally and doesn't know keep ⊇ keep.readonly, so gating reads on readonly would lock out a full-keep connection). - scope_param: noteId on per-note ops; resolve block surfaces the note title in approvals; disclose surfaces the title on create. - Mock-based e2e test plus a parse smoke test; an #[ignore]'d real-API test skips cleanly without enterprise credentials. Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nagement (#407) Mirror the Services admin opt-in for the Connections view. An org admin (the is_org_admin flag, same check as the services list) can: - list every user's connections via GET /v1/connections?include_user_level=true (silently ignored for non-admins), with owner_identity_id on each row - open, delete, and set-default another user's connection — set_default demotes the sibling within the OWNER's identity, not the admin's Dashboard: a "Show all users' connections" toggle (admin-only) plus an Owner column resolved to display names, mirroring the services view. Detail/delete/set-default need no client gating now that the backend authorizes admins. Adds connections_admin_view integration tests and a scenarios-driven screenshot script. Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bumps the dashboard-deps group in /dashboard with 7 updates: | Package | From | To | | --- | --- | --- | | [@playwright/test](https://github.com/microsoft/playwright) | `1.60.0` | `1.61.0` | | [@storybook/addon-docs](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/docs) | `10.4.4` | `10.4.6` | | [@storybook/sveltekit](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/sveltekit) | `10.4.4` | `10.4.6` | | [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) | `2.65.0` | `2.66.0` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.3` | `26.0.0` | | [playwright](https://github.com/microsoft/playwright) | `1.60.0` | `1.61.0` | | [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/core) | `10.4.4` | `10.4.6` | Updates `@playwright/test` from 1.60.0 to 1.61.0 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](microsoft/playwright@v1.60.0...v1.61.0) Updates `@storybook/addon-docs` from 10.4.4 to 10.4.6 - [Release notes](https://github.com/storybookjs/storybook/releases) - [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md) - [Commits](https://github.com/storybookjs/storybook/commits/v10.4.6/code/addons/docs) Updates `@storybook/sveltekit` from 10.4.4 to 10.4.6 - [Release notes](https://github.com/storybookjs/storybook/releases) - [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md) - [Commits](https://github.com/storybookjs/storybook/commits/v10.4.6/code/frameworks/sveltekit) Updates `@sveltejs/kit` from 2.65.0 to 2.66.0 - [Release notes](https://github.com/sveltejs/kit/releases) - [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md) - [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.66.0/packages/kit) Updates `@types/node` from 25.9.3 to 26.0.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `playwright` from 1.60.0 to 1.61.0 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](microsoft/playwright@v1.60.0...v1.61.0) Updates `storybook` from 10.4.4 to 10.4.6 - [Release notes](https://github.com/storybookjs/storybook/releases) - [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md) - [Commits](https://github.com/storybookjs/storybook/commits/v10.4.6/code/core) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-version: 1.61.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dashboard-deps - dependency-name: "@storybook/addon-docs" dependency-version: 10.4.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dashboard-deps - dependency-name: "@storybook/sveltekit" dependency-version: 10.4.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dashboard-deps - dependency-name: "@sveltejs/kit" dependency-version: 2.66.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dashboard-deps - dependency-name: "@types/node" dependency-version: 26.0.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: dashboard-deps - dependency-name: playwright dependency-version: 1.61.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dashboard-deps - dependency-name: storybook dependency-version: 10.4.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dashboard-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the cargo-deps group with 6 updates: | Package | From | To | | --- | --- | --- | | [time](https://github.com/time-rs/time) | `0.3.47` | `0.3.49` | | [tower-http](https://github.com/tower-rs/tower-http) | `0.6.11` | `0.7.0` | | [redis](https://github.com/redis-rs/redis-rs) | `1.2.2` | `1.2.4` | | [fastembed](https://github.com/Anush008/fastembed-rs) | `5.16.1` | `5.17.2` | | [minijinja](https://github.com/mitsuhiko/minijinja) | `2.20.0` | `2.21.0` | | [bytes](https://github.com/tokio-rs/bytes) | `1.11.1` | `1.12.0` | Updates `time` from 0.3.47 to 0.3.49 - [Release notes](https://github.com/time-rs/time/releases) - [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md) - [Commits](time-rs/time@v0.3.47...v0.3.49) Updates `tower-http` from 0.6.11 to 0.7.0 - [Release notes](https://github.com/tower-rs/tower-http/releases) - [Commits](tower-rs/tower-http@tower-http-0.6.11...tower-http-0.7.0) Updates `redis` from 1.2.2 to 1.2.4 - [Release notes](https://github.com/redis-rs/redis-rs/releases) - [Commits](redis-rs/redis-rs@redis-1.2.2...redis-1.2.4) Updates `fastembed` from 5.16.1 to 5.17.2 - [Release notes](https://github.com/Anush008/fastembed-rs/releases) - [Commits](Anush008/fastembed-rs@v5.16.1...v5.17.2) Updates `minijinja` from 2.20.0 to 2.21.0 - [Release notes](https://github.com/mitsuhiko/minijinja/releases) - [Changelog](https://github.com/mitsuhiko/minijinja/blob/main/CHANGELOG.md) - [Commits](mitsuhiko/minijinja@minijinja-go/v2.20.0...minijinja-go/v2.21.0) Updates `bytes` from 1.11.1 to 1.12.0 - [Release notes](https://github.com/tokio-rs/bytes/releases) - [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md) - [Commits](tokio-rs/bytes@v1.11.1...v1.12.0) --- updated-dependencies: - dependency-name: time dependency-version: 0.3.49 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cargo-deps - dependency-name: tower-http dependency-version: 0.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-deps - dependency-name: redis dependency-version: 1.2.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cargo-deps - dependency-name: fastembed dependency-version: 5.17.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-deps - dependency-name: minijinja dependency-version: 2.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-deps - dependency-name: bytes dependency-version: 1.12.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…403) Bumps the github-actions-deps group with 1 update: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 6 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
… (D23) (#410) The connection write path now matches the D22 read path: kernel_create_connection and kernel_import_connection resolve the calling identity to its ceiling owner (user→self, agent→owner_id) by default instead of falling back to the calling agent when on_behalf_of is omitted. on_behalf_of is still validated when given; audit attribution stays on the caller. This stops connections accreting on agent identities — the cause of the reauth-loop mismatch. upgrade_connection_scopes' ownership check is relaxed to the caller's ceiling so an agent can still upgrade the owner connection it now shares (otherwise this change would regress import→upgrade). Migration 086_connection_owner_identity re-points existing agent/sub_agent connection rows to their owner: owner-wins delete for a same-(provider, account_email) owner row, most-recent-wins collapse among remaining agent rows, demote-before-re-point to respect idx_connections_one_default, re-point, then re-establish one default per (identity, provider). Down is a documented no-op. Tests: agent import without on_behalf_of lands on the owner; the migration collapses agent rows into the owner connection (runs the real 086 SQL). Existing import/upgrade tests updated to query/seed at the owner. Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…LL scopes (#411) When a Google/Gmail connection holds only a partial grant (e.g. gmail.metadata but not the restricted gmail.readonly), the agent got no structured signal: it discovered every action un-annotated, then burned several action calls on raw Google 403s before self-recovering. Two root causes: the call-time scope gate short-circuits on NULL recorded scopes ("benefit of the doubt"), and the discovery API carried no per-action coverage field at all. - Self-heal recorded scopes on token refresh: persist the authoritative `scope` from each refresh response, so NULL/stale scopes become the real granted set (heals legacy/pre-capture connections within ~1h). Empty scope responses fall back to token-only update so a known set is never clobbered. - Add `action_scope_coverage` helper + `ScopeCoverage` enum (covered / needs_reconnect / unknown) in platform_services; refactor derive_credentials_status to reuse it so call-time and discovery stay consistent. - Expose per-action `scope_coverage` + `missing_scopes` on the search results and the service-actions list (additive/optional fields), so the model sees "needs reconnect for this action (missing: gmail.readonly)" at discovery time. - overslash-fakes: opt-in `scoped:` refresh-token sentinel so the OAuth mock can echo a granted scope set for refresh tests (plain refresh tokens unchanged). - Tests: 5 hermetic e2e (partial_grant.rs) covering covered/needs_reconnect/ unknown discovery rows, action-list annotation, and refresh self-heal; plus unit tests for action_scope_coverage. agent-runner / headless-reauth follow-ups are tracked as Kanban tasks on the overfolder project, not in this PR. Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Update the 2026-04-13 analysis against executor.sh's current state (v1.5.17, ~2,200 stars) and a fresh clone of the source. - MCP server mode shipped on Overslash neutralizes April's main threat - Executor repositioned around context efficiency; pricing now public - Policy model verified as org|user glob-pattern ceiling, not just per-tool - vision.md confirms Executor is deliberately "not AI specific" - Flag "scope merging" roadmap as the multi-tenancy convergence vector - New top recommendation: market Overslash's token-collapse benefit Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ship a Notion service definition (search, query database, create page, append block children) as an OpenAPI 3.1 template under services/, wired to a new public-integration OAuth provider. - services/notion.yaml: OAuth2 (provider: notion, no scopes), four Mode-C actions with risk/scope_param. Pins the required Notion-Version header on every request via a template-declared header param default. - migration 087: seed the `notion` oauth_providers row — client_secret_basic token auth, owner=user authorize param, no PKCE, non-expiring tokens. - Header params: add ParamLocation::Header so `in: header` params route into the outgoing request headers (with apply_defaults filling constants) and are excluded from the query string / JSON body. This is the mechanism that lets a template pin a constant version/accept header; previously `in: header` silently fell through to Body. - tests/notion.rs: provider-specific e2e (Mode C connection-based execution) covering search + query_database (read) and create_page + append_block_children (write), asserting the Notion-Version header, auto-resolved OAuth token, path resolution, and body/query/header split; plus an #[ignore]d real-API test. Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e pairs (#420) * feat(executor): expand array-valued query params to repeated key=value pairs Mode C's query-string builder stringified serde_json arrays into literal JSON blobs (labelIds=%5B%22INBOX%22...), which providers with repeatable query params (OpenAPI form/explode, e.g. Gmail labelIds) silently ignore. Factor the per-param serialization into encode_query_param(), shared by the GET/HEAD branch and the write-branch query partition: arrays emit one URL-encoded pair per element, empty arrays emit nothing, scalars are unchanged. Gate the `?` on the emitted pairs rather than the params map so a lone empty array no longer produces a trailing `?`. Closes #419 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: seed ort-sys binary cache from a release asset (cdn.pyke.io 403s) cdn.pyke.io serves HTTP 403 (Cloudflare bot challenge) to non-browser clients since 2026-07-03, so every cold-cache CI build fails in the ort-sys 2.0.0-rc.12 build script's binary download (pulled in via fastembed). The build script skips the download when the dist-hash dir under ~/.cache/ort.pyke.io already exists, so seed it in the lint, coverage, and e2e jobs from the ort-sys-cache-ms-1.24.2 release asset — a re-host of the build script's own hash-verified extraction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Bump vulnerable transitive dependencies to their first patched versions to clear all 10 open Dependabot alerts (2 high, 2 moderate, 6 low). Rust (Cargo.lock): - openssl 0.10.78 -> 0.10.80 (high: OOB in X509Ref::ocsp_responders; medium: AES-KW-PAD OOB write / heap overflow) - rustls-webpki 0.103.10 -> 0.103.13 (high: panic on malformed CRL; low: name-constraint bypasses) - rand 0.8.5 -> 0.8.6, 0.9.2 -> 0.9.3 (low: unsoundness with custom logger) Dashboard (npm) via overrides: - cookie -> 0.7.2 (low: OOB chars in name/path/domain) - esbuild -> 0.28.1 (low: dev-server arbitrary file read on Windows) Verified: cargo check --workspace (offline) passes; npm audit reports 0 vulnerabilities. Dashboard build runs on CI Node 24 (local Node too old). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bumps the cargo-deps group with 3 updates: [uuid](https://github.com/uuid-rs/uuid), [time](https://github.com/time-rs/time) and [anyhow](https://github.com/dtolnay/anyhow). Updates `uuid` from 1.23.3 to 1.23.4 - [Release notes](https://github.com/uuid-rs/uuid/releases) - [Commits](uuid-rs/uuid@v1.23.3...v1.23.4) Updates `time` from 0.3.49 to 0.3.51 - [Release notes](https://github.com/time-rs/time/releases) - [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md) - [Commits](time-rs/time@v0.3.49...v0.3.51) Updates `anyhow` from 1.0.102 to 1.0.103 - [Release notes](https://github.com/dtolnay/anyhow/releases) - [Commits](dtolnay/anyhow@1.0.102...1.0.103) --- updated-dependencies: - dependency-name: uuid dependency-version: 1.23.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cargo-deps - dependency-name: time dependency-version: 0.3.51 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cargo-deps - dependency-name: anyhow dependency-version: 1.0.103 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cargo-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Ship a LinkedIn service definition as an OpenAPI 3.1 template under services/, wired to a new LinkedIn OAuth provider. - services/linkedin.yaml: get_profile (read, OIDC /v2/userinfo), create_post (write, UGC Posts /v2/ugcPosts, w_member_social), and an optional get_organization (read, partner-gated). Uses x-overslash aliases for risk / scope_param / provider / disclose. create_post is marked `write` so an uncovered permission chain routes it through human approval; the disclose filters surface the share text/visibility to the reviewer. - migration 088: seed the `linkedin` oauth_providers row — OIDC issuer + jwks, client_secret_post, no PKCE, default identity scopes openid/profile/email. - dashboard: LinkedIn brand tile in the Connections provider metadata. - tests/linkedin.rs: parse smoke test; mock-based Mode C get_profile + create_post execution with OAuth auto-resolve; create_post routing through approval while get_profile auto-approves; ignored real-API E2E. Run: cargo test --test linkedin -- --test-threads=4 Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the Gmail service beyond the original send/read/draft surface with
seven operations, all reusing the existing `google` OAuth provider and the
per-operation scope-gating already in place — no new provider, no new secret.
Service (services/gmail.yaml), following the unprefixed vendor-extension
aliases (risk / scope_param / resolve / disclose / provider):
- list_threads, get_thread — read conversations (gmail.readonly)
- modify_message — add/remove labels: star, mark read, archive
(gmail.modify); discloses add/remove label
lists for the approval reviewer
- untrash_message — restore a message from trash (gmail.modify)
- create_label / get_label / delete_label
— label management (gmail.modify / gmail.metadata)
Tests (crates/overslash-api/tests/gmail.rs): extends the existing #[ignore]
E2E with Mode C coverage for every new op — list/get threads, a full
create_label -> get_label -> modify_message (apply+remove) -> delete_label
lifecycle, and untrash_message restoring what the earlier trash step trashed
(so the suite no longer leaves a real inbox message in the trash).
Verified: overslash-core shipped_services_validate_clean / _load_clean pass;
gmail test binary compiles clean (SQLX_OFFLINE=true); vet reports no issues.
Co-authored-by: Factory <factory@overslash.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…418) * feat(services): add HubSpot CRM service + OAuth provider Ship a HubSpot CRM service definition (contacts, companies, deals, notes/engagements) as an OpenAPI 3.1 template under services/, following the existing conventions: - x-overslash-* extensions (via unprefixed aliases): risk class, scope_param permission scoping, resolve/pick parameter resolution, OAuth provider, and default_secret_name (Private App token scheme). - Wire the HubSpot OAuth provider (migration 087): authorization-code grant, client_secret_post, refresh-capable, no PKCE. - Provider-specific e2e tests in crates/overslash-api/tests/hubspot.rs (not integration.rs): a mock Mode C test covering list/get contacts, companies, and deals plus contact create/update and note create against the local echo fake, and an #[ignore]'d real-API test. Run: cargo test -p overslash-api --test hubspot -- --test-threads=4 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(services): disclose contact-write fields on HubSpot approvals Add `disclose` (x-overslash-disclose) blocks to create_contact and update_contact so approval + audit summaries carry labeled, human-readable fields instead of a raw request dump: - create_contact: Email, Name (firstname+lastname), Company - update_contact: Contact (target id), Updated fields (changed property keys), Email Optional fields use the `// empty` idiom so absent values are omitted. A new test (test_hubspot_contact_write_disclosure) forces the approval gate on both writes and asserts the extracted values, exercising the jq filters end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(mcp): OAuth-authenticated MCP servers + HubSpot via remote MCP Rewrite the HubSpot integration to target HubSpot's official remote MCP server (https://mcp.hubspot.com) instead of hand-rolled HTTP actions, and add first-class support for OAuth-authenticated MCP servers so it can authenticate with a custom HubSpot OAuth ("MCP auth") app via BYOC. Core: - New `McpAuth::OAuth { provider, scopes }` variant (extract + validate + serde). `x-overslash-mcp.auth: { kind: oauth, provider, scopes }`. - MCP tools may set `mcp_tool` to carry the upstream tool name when it isn't a valid Overslash action key — HubSpot names tools `hubspot-list-objects` (dashes), so the key is `hubspot_list_objects` and the dashed name rides on `mcp_tool`. API: - `resolve_mcp_oauth_bearer` resolves the caller's connection → access token (refreshing via the org/BYOC client), reusing the HTTP OAuth primitives. - MCP dispatch injects the bearer out-of-band (`McpTarget.auth_header` → `mcp_caller::invoke`), never persisting it; gates to `needs_authentication` with a minted OAuth URL when no connection exists. - Approval replay re-resolves the OAuth token fresh (credential-free payload), mirroring the HTTP replay path. - `derive_credentials_status`, template display, and resync handle oauth MCP. Template + provider: - services/hubspot.yaml is now an mcp-runtime template with a curated slice of HubSpot's remote-MCP tools (list/search/batch-read/create/update objects, engagements, associations, properties, schemas, user details), enriched with risk / scope_param (objectType) / disclose. - Migration 087 sets supports_pkce=true (HubSpot's remote MCP requires PKCE). Tests (crates/overslash-api/tests/hubspot.rs): a local MCP fake + OAuth connection/BYOC verify (1) tool calls inject the auto-resolved bearer, (2) a write with no permission gates to an approval carrying the disclose fields, (3) no connection yields needs_authentication with a minted URL. Run: cargo test -p overslash-api --test hubspot -- --test-threads=4 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(db): renumber HubSpot migration 087 → 088 (dev has 087_notion) The PR targets `dev`, which already ships `087_add_notion_oauth_provider`. Renumber the HubSpot provider migration to 088 to avoid the duplicate `_sqlx_migrations_pkey` version collision that failed Lint+SQLx Verify and E2E. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(db): bump HubSpot migration 088 → 089 (dev landed 088_linkedin) dev advanced again — it now ships 088_add_linkedin_oauth_provider, colliding with the HubSpot migration's version 88 in the PR merge. Renumber to 089 (confirmed free on the merge ref). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): reject non-string oauth scopes instead of dropping them Per Seer review: the `x-overslash-mcp.auth.scopes` parser used `filter_map(Value::as_str)`, silently discarding non-string elements — a malformed `scopes: [123, "crm.read"]` would grant fewer permissions than intended with no error. Parse, don't validate: surface a `mcp_invalid` issue on the first non-string element (and on a non-array `scopes`). Adds positive + negative compile tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ta-scope reconnect loop (#423) Root-caused on overslash-dev via GCP logs + read-only DB (connection `85844f1a`, a single google connection -- not a duplicate-connection issue). A partner reconnected and re-imported full gmail scopes incl. `gmail.readonly`, but a later self-refresh STRIPPED the gmail scopes down to `calendar/openid/email/profile`, flipping the connection to reauth and looping the partner's agent forever. The stored refresh token was metadata-only, so Google's refresh echoed only the metadata scopes and the code wrote that subset back over the recorded set -- healing in the wrong direction. Three fixes: 1. resolve_access_token (services/oauth.rs): a refresh must NEVER narrow the recorded scopes. New pure reconcile_refresh_scopes only widens/heals (union recorded and granted, or adopts an unknown NULL set); a refresh that echoes a subset of the recorded scopes is treated as a stale-refresh-token signal and leaves the recorded column untouched. Unit-tested. 2. kernel_import_connection (services/platform_connections.rs): reject an in-place re-import that BROADENS the recorded scopes while carrying no fresh refresh_token (when the existing connection already has one). New scopes_broadened helper + a typed 400 telling the partner to re-consent for a refresh token that backs the wider grant. Unit- + integration-tested. 3. Call time (routes/actions/{auth,call}.rs): when Google returns its metadata-scope PERMISSION_DENIED ("Metadata scope does not support..."), surface a typed reauth_required envelope instead of a 200 Called envelope with the upstream 403 buried in the body. New is_metadata_scope_denial / metadata_scope_reauth_envelope. Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude <noreply@anthropic.com>
… pool exhaustion (#424) Root-caused on overslash-dev: `create_app` built the pool with a bare `PgPool::connect`, taking sqlx's default max_connections=10. Those 10 connections were shared by every HTTP handler AND the ~7 tokio::spawn background loops (approval/execution/orphan/subagent expiry, oauth/mcp flow expiry, webhook retry + digest, embedding backfill). Under a burst the pool starved, the 30s acquire timeout fired, and outbound `connection.*` webhooks were dropped ("pool timed out while waiting for an open connection"). The Postgres app ceiling (~97) was never the constraint -- per-instance pool sizing was. No leak (pg_stat_activity was clean). - Replace the bare connect with `PgPoolOptions` (explicit max/min connections + acquire_timeout), all configurable via DB_MAX_CONNECTIONS (default 25), DB_MIN_CONNECTIONS (2), DB_ACQUIRE_TIMEOUT_SECS (10). Cloud Run maxScale 3 x 25 = 75 leaves headroom under the ~97 ceiling. - Give the background jobs their own small pool (`background_db`, DB_BACKGROUND_MAX_CONNECTIONS default 5) so an expiry/webhook burst can never starve request handling. The DB-pool-stats gauge deliberately keeps reporting the request-handler pool. - Document the connection-hold invariant on webhook `deliver()`: it already takes the pool (Arc-cheap) not a checked-out connection, and does all DB work only after the outbound HTTP round-trip, so a slow webhook endpoint can't pin a connection across the network call. Comment makes that a maintained invariant. Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude <noreply@anthropic.com>
Bumps the dashboard-deps group in /dashboard with 10 updates: | Package | From | To | | --- | --- | --- | | [@playwright/test](https://github.com/microsoft/playwright) | `1.61.0` | `1.61.1` | | [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) | `2.66.0` | `2.69.1` | | [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) | `4.3.1` | `4.3.2` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.0.0` | `26.1.0` | | [autoprefixer](https://github.com/postcss/autoprefixer) | `10.5.0` | `10.5.2` | | [playwright](https://github.com/microsoft/playwright) | `1.61.0` | `1.61.1` | | [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) | `5.56.3` | `5.56.4` | | [svelte-check](https://github.com/sveltejs/language-tools) | `4.6.0` | `4.7.1` | | [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.1` | `4.3.2` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.16` | `8.1.3` | Updates `@playwright/test` from 1.61.0 to 1.61.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](microsoft/playwright@v1.61.0...v1.61.1) Updates `@sveltejs/kit` from 2.66.0 to 2.69.1 - [Release notes](https://github.com/sveltejs/kit/releases) - [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md) - [Commits](https://github.com/sveltejs/kit/commits/HEAD/packages/kit) Updates `@tailwindcss/postcss` from 4.3.1 to 4.3.2 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/@tailwindcss-postcss) Updates `@types/node` from 26.0.0 to 26.1.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `autoprefixer` from 10.5.0 to 10.5.2 - [Release notes](https://github.com/postcss/autoprefixer/releases) - [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md) - [Commits](postcss/autoprefixer@10.5.0...10.5.2) Updates `playwright` from 1.61.0 to 1.61.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](microsoft/playwright@v1.61.0...v1.61.1) Updates `svelte` from 5.56.3 to 5.56.4 - [Release notes](https://github.com/sveltejs/svelte/releases) - [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md) - [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.56.4/packages/svelte) Updates `svelte-check` from 4.6.0 to 4.7.1 - [Release notes](https://github.com/sveltejs/language-tools/releases) - [Commits](https://github.com/sveltejs/language-tools/compare/svelte-check@4.6.0...svelte-check@4.7.1) Updates `tailwindcss` from 4.3.1 to 4.3.2 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/tailwindcss) Updates `vite` from 8.0.16 to 8.1.3 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.1.3/packages/vite) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-version: 1.61.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dashboard-deps - dependency-name: "@sveltejs/kit" dependency-version: 2.69.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dashboard-deps - dependency-name: "@tailwindcss/postcss" dependency-version: 4.3.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dashboard-deps - dependency-name: "@types/node" dependency-version: 26.1.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dashboard-deps - dependency-name: autoprefixer dependency-version: 10.5.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dashboard-deps - dependency-name: playwright dependency-version: 1.61.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dashboard-deps - dependency-name: svelte dependency-version: 5.56.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dashboard-deps - dependency-name: svelte-check dependency-version: 4.7.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dashboard-deps - dependency-name: tailwindcss dependency-version: 4.3.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dashboard-deps - dependency-name: vite dependency-version: 8.1.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dashboard-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* feat(slack): decorated MCP wrapper on the oauth auth kind (D24) Rebased onto dev's #418 (OAuth-authenticated MCP servers + HubSpot), which landed the `McpAuth::OAuth` provider-connection mechanism this Slack work targets. Drops the parallel infra implementation and keeps only the additive pieces, adapted to dev's API. - services/slack.yaml: wraps Slack's official MCP server (mcp.slack.com/mcp) as an `x-overslash-runtime: mcp` template on `auth.kind: oauth, provider: slack`. Decorates only tools that benefit: send_message (write) gets disclose + channel scope; read_channel_history/get_user get scope_param; list_channels/search_messages stay plain. - platform_services: `template_oauth_provider` now also returns an MCP `auth.kind: oauth` provider, and `kernel_create_service` derives `expected_provider` through it — so pinning a connection on an oauth MCP template is accepted and auto-connect orchestration fires. This also fixes HubSpot from #418 (both were MCP-blind). `derive_credentials_status` already handled mcp-oauth on dev. - McpDetail exposes `provider`; dashboard `McpDetail.auth_kind` gains 'oauth' and the service detail + create pages reuse the existing OAuth connect surface for oauth MCP services (vertical integration). - tests/slack.rs: parse smoke + mock MCP OAuth token-forwarding + missing-connection error + pinned-connection accepted. - Docs: external-mcp-services.md documents the oauth auth kind; DECISIONS D24; STATUS updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(services): include MCP oauth scopes in auto-connect scope set Seer flagged that `template_action_scopes` (used by the auto-connect flow in `kernel_create_service`) unions only per-action `required_scopes`, which are empty for MCP tools — so an oauth MCP service (Slack, HubSpot) requested no scopes at connect time and the minted token lacked every permission. Include `McpAuth::OAuth { scopes }` from the service-level auth block. Adds a unit test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dashboard): surface MCP oauth scopes so connect requests them Seer flagged the frontend analog of the auto-connect scope bug: `McpDetail` dropped the `McpAuth::OAuth { scopes }`, so the dashboard's `oauthScopes` defaulted to `[]` and reconnect/upgrade for an oauth MCP service (Slack, HubSpot) requested no scopes — minting a token missing every permission. - API: McpDetail exposes `scopes` (from the oauth block). - types.ts: McpDetail.scopes; service detail + create pages fall back to `template.mcp.scopes` when building the connect request. - slack.rs: assert scopes round-trip through the template-detail DTO. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(templates): include MCP oauth scopes in TemplateDetail.scopes Seer flagged that `template_required_scopes` (populating the top-level `scopes` on `GET /v1/templates/{key}`, consumed by white-label / token-vault partners) unioned only per-action `required_scopes` — empty for MCP tools — so oauth MCP templates (Slack, HubSpot) reported an empty scope set to external consumers. Include `McpAuth::OAuth { scopes }`, mirroring the platform_services + McpDetail fixes. Test asserts the top-level scopes too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(services): validate mcp oauth scopes in derive_credentials_status Seer flagged that for a connected MCP-oauth service, derive_credentials_status iterated per-action scopes (empty for MCP tools) and always returned Ok, even when the connection's known scopes were missing the service-level `McpAuth::OAuth { scopes }` — disagreeing with the dashboard's missing-scope warning. Check the mcp scopes against the granted set directly (all-or-nothing: one scope set, no per-action granularity) → NeedsReconnect when short, Ok when covered, benefit-of-the-doubt when scopes are unknown. Adds unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(slack): autodiscover:false + hide resync for oauth MCP Seer flagged that slack.yaml's `autodiscover: true` surfaces a "Resync tools" button on promoted (org/user-tier) templates, but the backend always 400s resync for oauth MCP servers (no per-user token on the admin path) — a broken workflow. - services/slack.yaml: `autodiscover: false` — the inline tool list is authoritative, matching hubspot.yaml (every tool already declares input_schema). - dashboard: also gate the "Resync tools" button on `auth_kind !== 'oauth'` so a promoted oauth template with autodiscover left on never shows a button the backend rejects (covers HubSpot-like cases too). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(deps): Bump the cargo-deps group with 4 updates Bumps the cargo-deps group with 4 updates: [time](https://github.com/time-rs/time), [rand](https://github.com/rust-random/rand), [redis](https://github.com/redis-rs/redis-rs) and [aes-gcm](https://github.com/RustCrypto/AEADs). Updates `time` from 0.3.51 to 0.3.53 - [Release notes](https://github.com/time-rs/time/releases) - [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md) - [Commits](time-rs/time@v0.3.51...v0.3.53) Updates `rand` from 0.10.1 to 0.10.2 - [Release notes](https://github.com/rust-random/rand/releases) - [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md) - [Commits](rust-random/rand@0.10.1...0.10.2) Updates `redis` from 1.2.4 to 1.3.0 - [Release notes](https://github.com/redis-rs/redis-rs/releases) - [Commits](redis-rs/redis-rs@redis-1.2.4...redis-1.3.0) Updates `aes-gcm` from 0.10.3 to 0.11.0 - [Commits](RustCrypto/AEADs@aes-gcm-v0.10.3...aes-gcm-v0.11.0) --- updated-dependencies: - dependency-name: time dependency-version: 0.3.53 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cargo-deps - dependency-name: rand dependency-version: 0.10.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cargo-deps - dependency-name: redis dependency-version: 1.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-deps - dependency-name: aes-gcm dependency-version: 0.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-deps ... Signed-off-by: dependabot[bot] <support@github.com> * fix(core): migrate crypto to aes-gcm 0.11 API aes-gcm 0.11 no longer re-exports aead::OsRng and deprecates Nonce::from_slice. Generate nonces with rand::rng() (the same CSPRNG used for token/secret generation elsewhere) and build nonces via From/TryFrom. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…actions (#425) * feat(services): explicit disclose declarations for all shipped write actions Every shipped write/delete-class action now declares x-overslash-disclose so approval reviewers see a labeled summary of what the action will do, not just a raw payload. Adds the lone credential-like redact target (stripe create_charge body.source) and two guard tests: shipped mutating actions must declare disclose (registry), and shipped disclose filters must compile as jq (templates), closing the gap where shipped globals bypassed register-time jq validation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(resend): format array-valued 'to' recipients in disclose summary Resend accepts 'to' as string or string[]; join arrays so the approval summary shows addresses instead of raw JSON. Addresses Seer review on PR #425. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drive): omit empty parents arrays from disclose summaries jq treats [] as truthy, so '.body.parents // empty' disclosed an empty string when a caller passed parents: []. Guard with select(length > 0) per Seer review on PR #425. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…closure (#426) Param resolvers ('resolve: {get, pick}') previously enriched opaque IDs into display text only for the interpolated action description, then the map was dropped. Reviewers saw "Delete file Q3 Budget.xlsx" in prose but disclosed fields could only echo the opaque ID. - Carry the resolved map (param name -> display string) on ResolvedMeta, next to 'params'. Resolution happens once at resolve time and rides in the meta, so delete actions still disclose the object's name at audit-write time even though the object no longer exists upstream. - Extend the disclosure jq projection to {method, url, params, body, resolved}. Filters can now write '.resolved.fileId // .params.fileId'. Only successfully resolved params appear in the map. - Wire it through both disclosure sites (approval-create and audit-write). MCP/platform projections are unchanged: resolvers are HTTP-action-only. - Route resolver GETs through service_base_overrides like the executor, so e2e stacks that rewrite hosts to fakes get display names too. - SPEC 12a: new jq input shape + a resolver-backed declaration example. - e2e: resolver-backed disclose test against a fake upstream GET, pinning the exactly-one-resolver-GET-per-call timing property. Grew out of PR #425 (explicit disclose declarations). Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… them into disclose (#427) Sweep of the shipped service defs building on the .resolved disclosure projection (#426): - x delete_tweet: resolve tweet_id to the tweet text (GET /2/tweets/{id}, pick data.text) and disclose "Tweet" as .resolved // .params. - gmail: resolve message ids to Subject (format=metadata&metadataHeaders) on trash/untrash/modify, draft ids to the message snippet on delete_draft/send_draft (drafts.get has no metadataHeaders), and label ids to the label name on delete_label; chain every Mailbox field onto the existing userId->emailAddress resolver. send_draft's body-located id uses the canonical x-overslash-resolve key since alias normalization only walks parameters arrays. - notion append_block_children: resolve block_id via GET /v1/blocks/{id} pick child_page.title (names the target page in the common case). - drive delete_permission: resolve permissionId to the grantee email (?fields=emailAddress) and chain both fields. - drive/calendar/tasks/keep: upgrade existing ID-only disclose filters to .resolved.<param> // .params.<param> wherever the action already carries a resolver, so summaries degrade gracefully to raw ids. github/stripe/eventbrite/linkedin need no resolvers (their mutating actions take names, not opaque ids); slack/hubspot/whatsapp are MCP runtime where param resolvers don't apply. Extends the resolver disclosure e2e with an always-failing resolver to pin the chained-field fallback path. Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… requested scopes when /token omits scope (#428) HubSpot's remote MCP (mcp.hubspot.com) replaced its entire tool catalog: the hubspot-list-objects-style tools the shipped template was authored against no longer exist, so every call answered JSON-RPC -32603 "Unknown tool" and surfaced as a 502 from /v1/actions/call. Verified live via tools/list (17 new tools) and re-authored services/hubspot.yaml against it: CRM search/read/SQL-query tools, property metadata, owners, campaign/content analytics, and manage_crm_objects as the single write tool with disclose declarations. Connector-internal tools and the landing-page CMS editor are intentionally not exposed. Separately, HubSpot's token endpoint never echoes `scope`, so connections landed with a known-empty scopes set ({}) even after a full grant. Per RFC 6749 §5.1 an omitted scope means "granted as requested" — the callback now records the flow row's requested set in that case (TokenResponse::granted_scopes_or_requested). A present-but-empty scope is still honored verbatim, and refresh reconciliation is unchanged. Tests: the hubspot fake now pins a LIVE_CATALOG snapshot and rejects unknown names with HubSpot's exact -32603 error, so catalog drift fails CI instead of 502ing in prod; new tests cover template↔catalog sync, drift → 502 with the RPC code preserved, and the callback scope fallback end-to-end (plus unit tests for the fallback semantics). Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Factory <factory@overslash.dev>
…#430) The service detail page's credentials dropdown listed the caller's own connections. Since a connection can only bind to a service owned by the same identity, an admin viewing another user's service saw their own (unbindable) connections instead of the owner's. Add an admin-or-self `owner_identity_id` filter to `GET /v1/connections` and have the service detail page pass the service's `owner_identity_id`. Self-owned services collapse to today's behavior. Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ds (#431) * feat(services): use_default_connection opt-out + atomic pin_service_ids on connection creation White-label integrations couldn't atomically associate a new service with a new connection. Two additions close the gap: - Per-instance `use_default_connection` flag (migration 090, default true). When false, an unbound OAuth instance no longer falls back to the identity's default connection — it reports needs_authentication until one is pinned. Honored in resolve_instance_auth, the scope pre-gate, and the credentials classifier so the dashboard badge matches execution. Plumbed through the service create/update API, responses, and a dashboard toggle. - `pin_service_ids` on connection creation. The import path (POST /v1/connections/import) binds atomically via OrgScope::create_connection_and_pin — a bad id rolls the whole import back so no orphan connection is left behind. The OAuth-orchestrated flow carries the ids on the flow row (migration 091, UUID[]) and binds them on callback, staying best-effort to preserve the exchanged tokens and the existing spoofed-id security contract. Singular service_instance_id kept as an alias. Adds tests/service_pin_connection.rs covering the fallback gate, atomic bind, and rollback-on-bad-id. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(connections): clarify success_redirect omits full binding set by design Addresses the Sentry review on the OAuth callback: the browser success redirect deliberately does not enumerate bound_service_instance_ids in the query string. A user-controllable, browser-visible URL is the wrong transport for authoritative binding state and can't losslessly carry a list. The authoritative set is the DB — a partner reads it back via GET /v1/connections/{connection_id} (its `used_by` list), keyed off the connection_id already present in the redirect. The JSON branch keeps the full list as a convenience for programmatic callers (authenticated response body, not a URL). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion tab (#432) PR #431 added the per-service use_default_connection opt-out toggle but placed it on the overview tab, not the credentials (Connection) tab where the provider connection is actually managed. Users looking at connection settings could neither see nor change the fallback behavior. Relocate the toggle into the credentials tab's OAuth branch, directly below the Connection picker and above the Save/Connect actions. The field was already wired end-to-end (types, updateService, save() diff); the same credentials-tab Save button persists it, so no other changes are needed. Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…433) * feat(members): promote/demote org admins + fix invite-as-admin grant Two related fixes to org admin management. 1. NEW — promote/demote existing members. Adds an AdminAcl-gated `PATCH /v1/org-members/{identity_id}` with `{ role: admin|member }`. Promotion/demotion is atomic across all three admin signals that authorization depends on: `user_org_memberships.role`, the per-identity `is_org_admin` flag, and `Admins`-group membership (the surface `AdminAcl`/`OrgAcl` actually read). Enforces the last-admin invariant (refuses to demote the final admin, including self-demotion). Previously only possible via direct psql. 2. BUG — invite-as-admin conferred no real admin. The invite-accept/JIT path wrote only `membership::create(.., role)`; an invited "admin" got `role='admin'` but failed `AdminAcl` (no flag, not in Admins group). Now the acceptance path grants real admin (flag + group) when the invite role is admin, unified with the existing second-IdP admin mirror through the shared `set_is_org_admin` primitive. Shared primitive: extracted `sync_admins_group_tx`; both `set_is_org_admin` (single identity, used by invite-accept) and the new atomic `set_org_member_admin` (whole membership, used by the endpoint) route through it so the flag and group can never drift. Dashboard: Members page gains a Role column/badge and an admin-only promote/demote control in the member drawer (`is_org_admin` now exposed on IdentityResponse). Tests: promote→real admin (passes AdminAcl), demote→loses AdminAcl, last-admin refusal (incl. self), non-admin 403, invalid role; extended the managed-signin suite to assert an invited admin passes an AdminAcl request. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(members): correct self-demotion comment to match behavior Seer review: the +page.ts comment claimed we hide self-targeting affordances, but the drawer intentionally allows an admin to demote themselves when other admins remain (surfacing a warning); the backend still refuses demoting the last admin. Reworded the comment to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…434) * feat(auth): decouple managed-IdP sign-in from invite-only admission Managed-signin orgs were forced into invite-only admission: the shared Overslash IdP could authenticate a user but membership always required a pending org_invites row. There was no way to combine "use the shared IdP" with "admit by email domain" — the domain path only worked with a per-org OAuth app. Migration 092 splits admission into two independent axes on `orgs`: * require_invite_admission (default true) — preserves invite-only. * managed_signin_allowed_domains text[] — org-wide allowlist consulted when require_invite_admission = false. Empty list is treated as misconfigured (reject), never as open admission. Admission matrix (managed sign-in ON): require_invite = true -> invite-only (unchanged default). require_invite = false -> admit if email domain in allowlist, else reject. Distinct rejection reasons: not_invited, domain_admission_not_configured, domain_not_allowed. Domain match splits the verified email on `@` (case-insensitive); it does not verify Google's `hd` claim (see TECH_DEBT). The allowlist lives on `orgs` rather than `org_idp_configs` because the managed path admits through multiple env-var providers (Google and GitHub) sharing one trust boundary; the legacy per-org-IdP path keeps org_idp_configs.allowed_email_domains. Includes: repo update_managed_admission, extended GET/PATCH managed-signin API with domain normalization, dashboard "Sign-in & members" UI (require- invite toggle + allowed-domains editor + misconfig warning), integration tests for all three managed sub-modes, and doc updates (SPEC, DECISIONS, multi_org_auth, TECH_DEBT). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(auth): clarify existing-member short-circuit is not a domain-removal bypass Address Seer review: expand the comment on the `existing_member` block to document that the domain allowlist is a point-in-time admission gate (like invites and the existing-identity refresh path), not continuous authorization. Removing a domain gates new admissions only; existing members keep access via their original IdP regardless, so the short-circuit is consistent — and re-gating a second IdP would fork a duplicate users row. No behavioral change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tripe trial (#436) Adds a time-boxed trial capability for orgs, with two mechanisms unified into one dashboard banner: - Instance-admin managed trials: new `plan='trial'` tier + `orgs.trial_ends_at` (migration 093, mirrors 052). Instance admins start (POST /v1/orgs/{id}/trial, defaulting to TRIAL_DEFAULT_DURATION_DAYS=30), bump (PATCH .../trial, extends from the later of current-end/now), and opt out (PATCH .../plan → standard|free_unlimited, clearing the window). Each invalidates the cache. - Self-serve trials: the "Not sure. Trial for free for a month" toggle on the create page drives Stripe's subscription_data[trial_period_days] — card on file, first charge deferred, status='trialing'. No billing bypass. Enforcement is banner-only (DECISIONS D25): an expired trial keeps serving API calls and stays under normal rate limits (it is not free_unlimited). Expiry only changes dashboard messaging. - billing_tier: FreeUnlimitedCache generalized to cache (plan, trial_ends_at) and answer both is_free_unlimited and trial_status (30s TTL, fail-open). - /auth/me/identity gains a `trial` summary so every member sees the banner; admin-only /v1/orgs/{id}/subscription gets a synthetic trial branch. - Dashboard: TrialBanner in the layout (active/expired), instance-admin start/extend/opt-out panel on the org page, self-serve toggle on new-team. - Config knob TRIAL_DEFAULT_DURATION_DAYS (default 30) + .env.example. - Tests: repo, endpoints (auth + audit), subscription active/expired, me/identity summary + free_unlimited exemption, and a banner-only proof that an expired trial is neither blocked nor granted unlimited. billing_tier unit tests for the trial-status derivation. Removes the now-orphaned org::get_plan (subsumed by get_billing). Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on gate (#435) Org admins can already restrict the global template catalog to a curated allow-list (`global_templates_enabled` + `enabled_global_templates`), but it had no dashboard UI and was only enforced on discovery surfaces — a caller who knew a curated-out key could still instantiate it. Backend: - New per-org `allow_services_outside_catalog` flag (migration 092, default false). When false, non-admins get 403 creating a service from a global template outside the curated catalog; when true, curated-out globals stay hidden from discovery but remain instantiable. Admins are always exempt. - Enforce at instantiation in `kernel_create_service`, reusing the same allow-list filter as discovery (`is_global_curated_out`) so the two can't diverge. - Add `GET /v1/orgs/{id}/template-settings` (was PATCH-only) and thread the new flag through the PATCH request/response, repo accessors, and audit detail. Dashboard: - Org Settings gains a "Service catalog" card with three toggles (make all global services available, allow services outside the curated catalog, allow user-defined templates). - Services → Catalog gains an admin-only per-global enable/disable curation grid backed by `/v1/templates/admin`, active only in curated mode. Tests + scenarios: integration coverage for the 403/soft/admin-exempt matrix and settings round-trip; a real-stack screenshot script for both surfaces. Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(services): allow Write members to manage their own services Service create is gated by WriteAcl, but delete/update/update-status required AdminAcl — so a Write-level member (e.g. an app acting via org-service-key + X-Overslash-As) could create a service instance but got 403 "admin access required" when deleting or editing its own. Lower those three handlers to WriteAcl and enforce a strict per-row ownership check (require_owner_or_admin): a caller may mutate a service it owns at Write level; another identity's service, or an org-level (owner_identity_id IS NULL) service, still requires Admin. This mirrors create_service (org-level create re-checks Admin) and update_template. The check is strict identity equality — deliberately NOT a ceiling/family check — so an agent cannot reach its owner-user's or a sibling agent's services through the API; those stay dashboard-managed. The is_system guard runs before the ownership check, so system services still 400. Adds route tests (services_admin_view.rs) covering own/other/org-level/ system/admin delete and an update-status parity case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(db): renumber duplicate migration 093 → 094 `dev` landed two migrations both numbered 093 — `093_org_trial` (#436) and `093_orgs_allow_services_outside_catalog` (#435) — merged from separate PRs. Applying them fails with `duplicate key value violates unique constraint "_sqlx_migrations_pkey" ... Key (version)=(93) already exists`, which breaks the "Run migrations" step (and every migration-applying CI job) for all PRs against dev. Renumber the newer of the two (#435, catalog) to 094; org_trial keeps 093 (its PR merged first). The two migrations touch orgs columns independently, so ordering is unaffected. No query changes, so the sqlx offline cache is untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Factory <factory@overslash.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Promotion PR: merge
devintomasterto cut the next release. 41 commits since v0.4.0.Merging this triggers release-please (
release-please.yml), which opens achore(release): vX.Y.ZPR bumping the version + CHANGELOG. Merging that PR creates the tag and GitHub release, which triggersrelease.ymlto build and attach binaries.Highlights since v0.4.0
OAuth & white-label
oauth_redirect_url+ opt-in white-label switch (feat(oauth): per-org oauth_redirect_url + opt-in white-label switch #398)POST /v1/connections/import(feat(oauth): white-label connections as a token vault (POST /v1/connections/import) #400)Services & actions
disclosedeclarations for all shipped write actions; resolve IDs to names on destructive actions (feat(services): explicit disclose declarations for all shipped write actions #425, feat(actions): make resolved display params a first-class map for disclosure #426, feat(services): resolve IDs to names on destructive actions and chain them into disclose #427)use_default_connectionopt-out + atomicpin_service_ids(feat(services): use_default_connection opt-out + atomic pin_service_ids #431, fix(dashboard): move use_default_connection toggle to service Connection tab #432)Auth & members
Fixes & infra
🤖 Generated with Claude Code