From 3d8a91bca14cfc1cf18013e00f56c72ce006f3c3 Mon Sep 17 00:00:00 2001 From: Factory Date: Thu, 29 Oct 2026 08:56:34 +0000 Subject: [PATCH 1/2] feat(templates): info.summary as a one-line catalog tagline `info.description` is written for agents and runs to paragraphs, which is too long for a catalog tile. Read the standard OpenAPI 3.1 `info.summary` as a short dashboard-facing tagline and expose it as an optional `summary` on `/v1/templates` (list, search, detail, admin, draft preview). It is omitted when unset, and agent-facing `/v1/search` and MCP keep `description`. - Compile warns (never refuses) on a multi-line value or one over 80 chars. The same check covers a derived layer's new `delta.summary` relabel. - Every shipped template, plus the synthetic `http` service, now has a summary. `every_shipped_template_has_a_short_summary` keeps them short. - The dashboard Create Service cards show `summary ?? description` with the full description as a tooltip, and template search matches on summary too. Co-Authored-By: Claude Opus 5.5 --- SPEC.md | 3 +- .../src/routes/actions/service_resolve.rs | 1 + .../overslash-api/src/routes/approvals/dto.rs | 1 + .../src/routes/search/setup_hint.rs | 1 + .../src/routes/templates/admin.rs | 2 + .../src/routes/templates/drafts.rs | 3 + .../overslash-api/src/routes/templates/dto.rs | 11 ++ .../overslash-api/src/routes/templates/mod.rs | 5 + .../src/routes/templates/read.rs | 10 ++ .../src/services/platform_services/mod.rs | 4 + .../src/services/platform_services/status.rs | 1 + .../src/services/platform_services/verify.rs | 1 + .../src/services/service_setup/slots.rs | 1 + crates/overslash-api/tests/api.rs | 1 + .../overslash-api/tests/templates_summary.rs | 103 +++++++++++++++++ crates/overslash-core/src/instance_config.rs | 1 + .../overslash-core/src/openapi/compile/mod.rs | 34 ++++++ .../src/openapi/compile/tests_summary.rs | 104 ++++++++++++++++++ crates/overslash-core/src/openapi/mod.rs | 2 +- crates/overslash-core/src/registry/mod.rs | 1 + crates/overslash-core/src/registry/tests.rs | 25 +++++ crates/overslash-core/src/search/mod.rs | 1 + .../overslash-core/src/service_layer/apply.rs | 17 +++ .../overslash-core/src/service_layer/mod.rs | 1 + .../overslash-core/src/service_layer/types.rs | 3 + .../src/service_layer/validate.rs | 23 ++++ .../template_validation/core/action/tests.rs | 1 + .../src/template_validation/core/mod.rs | 2 + .../template_validation/core/service_shape.rs | 26 ++++- .../src/template_validation/json.rs | 1 + .../src/template_validation/mod.rs | 2 +- .../src/types/service/definition.rs | 7 ++ .../scripts/screenshot-template-summary.mjs | 27 +++++ .../components/services/TemplateCard.svelte | 6 +- .../templates/TemplateCatalog.svelte | 2 +- dashboard/src/lib/types.ts | 5 + .../src/routes/services/new/+page.svelte | 2 +- services/deepwiki.yaml | 1 + services/email.yaml | 1 + services/eventbrite.yaml | 1 + services/figma.yaml | 1 + services/github.yaml | 1 + services/gmail.yaml | 1 + services/google_calendar.yaml | 1 + services/google_drive.yaml | 1 + services/google_keep.yaml | 1 + services/google_tasks.yaml | 1 + services/holded.yaml | 1 + services/hubspot.yaml | 1 + services/langfuse.yaml | 1 + services/linkedin.yaml | 1 + services/metabase.yaml | 1 + services/notion.yaml | 1 + services/outlook.yaml | 1 + services/overslash.yaml | 1 + services/resend.yaml | 1 + services/shortcut.yaml | 1 + services/slack.yaml | 1 + services/stripe.yaml | 1 + services/telegram.yaml | 1 + services/test_email.yaml | 1 + services/whatsapp.yaml | 1 + services/x.yaml | 1 + 63 files changed, 459 insertions(+), 8 deletions(-) create mode 100644 crates/overslash-api/tests/templates_summary.rs create mode 100644 crates/overslash-core/src/openapi/compile/tests_summary.rs create mode 100644 dashboard/scripts/screenshot-template-summary.mjs diff --git a/SPEC.md b/SPEC.md index c22eb6c84..ea0e637cd 100644 --- a/SPEC.md +++ b/SPEC.md @@ -962,7 +962,7 @@ Templates live in a three-tier registry: **Org-admin visibility**: Org-admins can see all templates in the org (global + org + user-created) in a read-only list for security/compliance — they need to know what external APIs their users are connecting to. -**Layers (`extends`/`delta`).** Each org/user template row is a **layer**. A **standalone** layer (`extends` NULL) holds a full OpenAPI doc (the classic org/user template). A **derived** layer (`extends` set) holds a *delta* over a base template named by `extends` (resolved by key: DB rows then the global registry), and its effective template is the **fold** `resolve(layer) = apply(delta, resolve(extends))`. The delta's **masks** are restrictive and order-independent — action `allowlist` (∩) / `denylist` (\), per-action `risk` clamp-**up**-only, additive `disclose`, relabel, template `hidden` — so `resolve(child) ⊆ resolve(base)` (a child can never re-expose what a parent hid); its **extensions** add new actions/hosts only (no auth, no rebinding). An **org** layer may additionally carry `instance_defaults` — non-secret presets (endpoint `url` + `x-overslash-instance-config` values) that every service instance created from the layer inherits unless it sets its own (every template's endpoint is overridable per instance; `x-overslash-promoted` on `servers[0]` or on a config field only decides whether the instance form shows it up front or behind "Show more options"), so an org running its own deployment (its overfwd Mailbox Gateway, its self-hosted MCP server) names it once instead of on every user's instance. `extends` is a **live pointer**: editing a base (or Overslash shipping a new global version) propagates to every descendant immediately. `extends` and the layer's own `key` are decoupled — reusing the base key shadows it (curation that agents get transparently), a distinct key is a separate catalog entry alongside the base. Discovery, instantiation, and **execution** all resolve through the fold, so a masked-out action is unreachable everywhere. See [DECISIONS.md D29](DECISIONS.md) and [docs/design/layered-service-templates.md](docs/design/layered-service-templates.md). +**Layers (`extends`/`delta`).** Each org/user template row is a **layer**. A **standalone** layer (`extends` NULL) holds a full OpenAPI doc (the classic org/user template). A **derived** layer (`extends` set) holds a *delta* over a base template named by `extends` (resolved by key: DB rows then the global registry), and its effective template is the **fold** `resolve(layer) = apply(delta, resolve(extends))`. The delta's **masks** are restrictive and order-independent — action `allowlist` (∩) / `denylist` (\), per-action `risk` clamp-**up**-only, additive `disclose`, relabel (display name, description, summary), template `hidden` — so `resolve(child) ⊆ resolve(base)` (a child can never re-expose what a parent hid); its **extensions** add new actions/hosts only (no auth, no rebinding). An **org** layer may additionally carry `instance_defaults` — non-secret presets (endpoint `url` + `x-overslash-instance-config` values) that every service instance created from the layer inherits unless it sets its own (every template's endpoint is overridable per instance; `x-overslash-promoted` on `servers[0]` or on a config field only decides whether the instance form shows it up front or behind "Show more options"), so an org running its own deployment (its overfwd Mailbox Gateway, its self-hosted MCP server) names it once instead of on every user's instance. `extends` is a **live pointer**: editing a base (or Overslash shipping a new global version) propagates to every descendant immediately. `extends` and the layer's own `key` are decoupled — reusing the base key shadows it (curation that agents get transparently), a distinct key is a separate catalog entry alongside the base. Discovery, instantiation, and **execution** all resolve through the fold, so a masked-out action is unreachable everywhere. See [DECISIONS.md D29](DECISIONS.md) and [docs/design/layered-service-templates.md](docs/design/layered-service-templates.md). ### Template Definition @@ -1041,6 +1041,7 @@ paths: - **`x-overslash-pagination` / `pagination:`** — on an operation (or MCP tool), that this action returns one *page* of a larger collection, and how to reach the next one. Two halves. `page_size: {param, default, max}` names the declared numeric parameter that bounds a page; `default` **seeds that parameter's own `default:`** at compile time when it declares none, so the bound reaches the wire through `apply_defaults` like any other default (and appears on `/v1/search` rows for the same reason) rather than through a second injection path — a parameter that already declares one keeps it, and the two disagreeing is a warning. `max` is declarative: it bounds `default` at validation time and is *not* enforced against a caller who explicitly asks for more. `next: {style, param, from}` names the continuation: `cursor` (an opaque value read from the dotted body path in `from` — Slack's `cursor`, Notion's `start_cursor`, Google's `pageToken`), `offset` (advance `param` by the effective page size), `page` (increment `param` — whose own `default:` is required, since it is the only place a template says whether pages count from 0 or from 1), or `link` (the response names the whole next URL and the gateway lifts the declared query keys out of it — from the RFC 8288 `Link: <…>; rel="next"` header by default, or from the dotted body path in `from`, which is where Shortcut, Zendesk and Twilio put theirs; `param` there names the single query key the next URL may introduce although page one never sent it. Needs `from` on an MCP tool, whose results carry no headers). Optional `items` (dotted path to the principal collection) and `has_more` (dotted path to an explicit boolean) are how a last page is recognised. A **paged result** carries `_pagination: {has_more, next: {service, action, params}}`, where `params` is the *difference* between the call just made and the next one, in the action's own parameter names — merge it into what you sent. The gateway never follows a page itself. A malformed block is a template *error* and the action goes back to being unpaged; a `page_size.param` naming a parameter the action does not declare is an error too, since it reads as a bound and applies none. See §12b. - **`x-overslash-default_timeout_ms` / `default_timeout_ms:`** — under `info`, the same thing one rung less specific: the timeout every action of this service inherits unless it declares its own. The one-line answer to "this whole upstream is slow". - **`x-overslash-icon` / `icon:`** — under `info`, the mark the dashboard shows for this service. Two forms: `builtin:`, an asset Overslash ships and serves at `/icons/.svg`, and an `https://` URL hosted elsewhere. **Usually omitted**: a template whose key matches a shipped asset resolves to `builtin:` implicitly, which is why the shipped templates declare nothing. Explicit values are for the two cases the convention can't express — a key that deliberately differs from the asset it reuses, or a remote URL. Resolved server-side and surfaced as an absolute `icon_url` on the template and service-instance responses; a template with nothing renderable omits the field, and the dashboard falls back to a letter tile. Only `https://` ever reaches a browser — `http:`, `data:` and `javascript:` are template *errors*, checked both at write time and again when the response is built. Overslash never fetches a remote icon (that would be an SSRF vector and a boot-time network dependency), so there is no size or format validation of one. `${VAR}` expansion applies like anywhere else, which is how a self-hoster points the set at their own CDN. Icons are deliberately **not** on `/v1/search`: it fans out up to 100 rows per (instance × action) and the field would cost an agent's context window for something no model can render. +- **`summary:`** (standard OpenAPI 3.1 `info.summary`, not an extension) — under `info`, a one-line tagline for UI catalogs: one plain sentence, at most 80 characters, no trailing period. `info.description` is written for agents (it is folded into the text every action is scored against) and runs to paragraphs; `summary` is what a catalog tile shows, and consumers render `summary ?? description`. Surfaced as `summary` on `GET /v1/templates` (list, search, detail, admin, draft preview) and omitted when unset. Deliberately **not** on `/v1/search` or MCP: agents keep `description`. A multi-line or over-long value compiles with an `openapi_invalid` warning (an imported spec still loads); a derived layer may relabel it through `delta.summary`, which takes the same warnings. Every shipped template carries one, enforced by `every_shipped_template_has_a_short_summary`. - **`x-overslash-additional-properties` / `additional-properties:`** — on an operation or MCP tool, that the action's declared parameter set is a **floor, not a fence**: an argument the template never declared is forwarded upstream instead of returning `invalid_action_args`, and a declared `enum` names the members we know of rather than all of them. `required` is still enforced either way — it is a statement about the operation, not about how completely we transcribed it. Also settable under `info` as a service-wide default, where the nearest value wins, so an operation may write `false` to re-tighten under a service that relaxed globally. **Defaults to `false`**, and a malformed value (`additional-properties: "true"`) warns and stays strict, so a quoting mistake cannot grant the relaxation by accident. The name is borrowed from JSON Schema, where it governs undeclared keys only; here it deliberately covers `enum` too, because the same template that cannot name every parameter is the one whose enum snapshot has gone stale. For a relaxed action whose operation declares no `requestBody`, a non-`GET` call synthesizes a JSON body so the accepted argument actually reaches the wire; declaring the key on a body-carrying method whose `requestBody` is **non-JSON** is a template *error* (`additional_properties_needs_a_json_body`), since only JSON bodies are serialised and the argument would otherwise pass validation and then be dropped. **Not read on a platform action**: those answer from this process against a param set Overslash owns, so an undeclared argument would pass the gate and then be silently dropped — strictly worse than the 400. What the author gives up is the Levenshtein "did you mean" on a typo: a misspelled optional param now reaches the upstream instead of returning a self-correctable 400. Two security notes, both consequences of the gateway not knowing a name exists: permission-key derivation (`x-overslash-scope_param`) reads declared params only, so an undeclared argument can change what a call does without changing the key the chain checks; and `x-overslash-redact` cannot cover a path it has never seen, so a relaxed action's approval payload and audit row may carry undeclared values in the clear. Surfaced to agents as `additional_properties` on `/v1/search` rows, and to the dashboard on the action-detail response, which is what turns the API Explorer's enum `