diff --git a/.changeset/scaffold-default-connector-executors.md b/.changeset/scaffold-default-connector-executors.md new file mode 100644 index 0000000000..ca3707fd43 --- /dev/null +++ b/.changeset/scaffold-default-connector-executors.md @@ -0,0 +1,28 @@ +--- +"create-objectstack": minor +--- + +feat(create-objectstack): the blank scaffold ships the three generic connector executors by default + +`npm create objectstack` now generates an `objectstack.config.ts` that wires the +`rest`, `openapi`, and `mcp` connector executor plugins (ADR-0022/0023/0024 + +ADR-0097) into `plugins:`, alongside `requires: ['automation']`. This closes the +last authoring gap in the ADR-0097 promise that integrations are expressible +**and executable** as pure metadata: an author (human or AI) can now add a +declarative `connectors:` entry naming `provider: 'rest' | 'openapi' | 'mcp'` +and have it materialize into a live, dispatchable connector at boot — with no +host-code edit. + +- `plugins:` — `new ConnectorRestPlugin()`, `new ConnectorOpenApiPlugin()`, + `new ConnectorMcpPlugin()` (zero-arg = contribute the provider factory only). +- `requires: ['automation']` — the automation service performs the + materialization and owns the registry the executors register into. It is also + a hard dependency of the connector plugins, so a scaffold that lists them in + `plugins:` without it fails boot; automation ships transitively via + `@objectstack/cli`. +- deps — `@objectstack/connector-rest`, `@objectstack/connector-openapi`, + `@objectstack/connector-mcp`. +- Security (#3055): declarative `mcp` stdio transports stay denied by default — + opt in per host with `new ConnectorMcpPlugin({ declarativeStdio: ['node'] })`. + +Brand connectors (Slack, …) remain marketplace/opt-in. diff --git a/.github/workflows/scaffold-e2e.yml b/.github/workflows/scaffold-e2e.yml index 1b476310e0..2e034da56f 100644 --- a/.github/workflows/scaffold-e2e.yml +++ b/.github/workflows/scaffold-e2e.yml @@ -19,7 +19,6 @@ on: pull_request: paths: - 'packages/create-objectstack/**' - - 'examples/docker/**' - 'docker/**' - '.github/workflows/scaffold-e2e.yml' schedule: @@ -116,8 +115,9 @@ jobs: kill "$SERVER_PID" - name: Build official runtime image from this checkout - # examples/docker builds FROM ghcr.io/objectstack-ai/objectstack. - # Build that base HERE from docker/Dockerfile instead of pulling, so + # The scaffolded app's Dockerfile builds FROM + # ghcr.io/objectstack-ai/objectstack. Build that base HERE from + # docker/Dockerfile instead of pulling, so # (a) PRs exercise the official runtime Dockerfile itself, and # (b) the e2e stays hermetic — no dependency on a prior release # having published the tag (chicken-and-egg on the very first one). @@ -126,12 +126,13 @@ jobs: --build-arg OS_CLI_VERSION=latest \ "$GITHUB_WORKSPACE/docker" - - name: Docker build and run (examples/docker) + - name: Docker build and run (scaffolded Dockerfile) + # The blank template ships its own Dockerfile / docker-compose.yml / + # .dockerignore, so build the generated project as-is — this exercises + # the exact Docker path a real `npm create objectstack` user ships (the + # standalone examples/docker copy was removed in 6c28bac). run: | cd "$RUNNER_TEMP/e2e-app" - cp "$GITHUB_WORKSPACE"/examples/docker/Dockerfile \ - "$GITHUB_WORKSPACE"/examples/docker/docker-compose.yml \ - "$GITHUB_WORKSPACE"/examples/docker/.dockerignore . docker build --pull=false -t e2e-app . docker run -d --name e2e -p 18080:8080 \ -e OS_SECRET_KEY="$(openssl rand -hex 32)" \ diff --git a/content/docs/automation/flows.mdx b/content/docs/automation/flows.mdx index d8e5d986cf..8be4fcf147 100644 --- a/content/docs/automation/flows.mdx +++ b/content/docs/automation/flows.mdx @@ -117,7 +117,7 @@ Each node performs a specific action in the flow. | `map` | Sequential multi-instance — run a per-item subflow for each element, one at a time (each may pause); batch approval (ADR-0039) | | `connector_action` | Execute an external connector action | -`connector_action` dispatches against the runtime **connector registry**, which connector plugins populate. The three **generic executors** — `rest`, `openapi`, and `mcp` — double as provider factories: with them in your app's `plugins:`, a declarative `connectors:` entry that names a `provider` is materialized into a live, dispatchable connector at boot with no plugin code (ADR-0097; the showcase example wires all three). Brand connectors (Slack, …) are installed separately. One security note: a declarative `mcp` instance using a **stdio** transport spawns a local process from metadata and is therefore denied by default — the host opts in with `new ConnectorMcpPlugin({ declarativeStdio: [''] })`; `http` transports need no opt-in. +`connector_action` dispatches against the runtime **connector registry**, which connector plugins populate. The three **generic executors** — `rest`, `openapi`, and `mcp` — double as provider factories: with them in your app's `plugins:`, a declarative `connectors:` entry that names a `provider` is materialized into a live, dispatchable connector at boot with no plugin code (ADR-0097). Scaffolded projects (`npm create objectstack`) ship all three executors by default — paired with the `automation` capability that performs the materialization — and the showcase wires them too. Brand connectors (Slack, …) are installed separately. One security note: a declarative `mcp` instance using a **stdio** transport spawns a local process from metadata and is therefore denied by default — the host opts in with `new ConnectorMcpPlugin({ declarativeStdio: [''] })`; `http` transports need no opt-in. ### Node Structure diff --git a/content/docs/getting-started/your-first-project.mdx b/content/docs/getting-started/your-first-project.mdx index 9acac917e2..e5d469c5a8 100644 --- a/content/docs/getting-started/your-first-project.mdx +++ b/content/docs/getting-started/your-first-project.mdx @@ -101,6 +101,9 @@ filename-suffix magic — metadata exists in the app only if it is imported here ```typescript title="objectstack.config.ts" import { defineStack } from '@objectstack/spec'; +import { ConnectorRestPlugin } from '@objectstack/connector-rest'; +import { ConnectorOpenApiPlugin } from '@objectstack/connector-openapi'; +import { ConnectorMcpPlugin } from '@objectstack/connector-mcp'; import * as objects from './src/objects/index.js'; export default defineStack({ @@ -111,12 +114,25 @@ export default defineStack({ type: 'app', name: 'My App', }, + // `automation` runs flows and, per ADR-0097, materializes declarative + // `connectors:` entries at boot. The three generic executors below register + // their `rest` / `openapi` / `mcp` provider factories with it. + requires: ['automation'], + plugins: [ + new ConnectorRestPlugin(), + new ConnectorOpenApiPlugin(), + new ConnectorMcpPlugin(), + ], objects: Object.values(objects), }); ``` The REST API needs no configuration — it is on by default for every object -with `apiEnabled`. +with `apiEnabled`. The `plugins:` array ships the three **generic connector +executors**, so you can integrate an external REST / OpenAPI / MCP service as +pure metadata: add a `connectors:` entry naming a `provider` and the +`automation` capability materializes it into a live connector at boot — see +[Automation → Flows](/docs/automation/flows). **`src/objects/note.object.ts`** is a complete data model — schema, validation, API surface, and searchability in one typed definition: @@ -151,6 +167,7 @@ framework surface you install: | `@objectstack/runtime` | The kernel that interprets metadata at runtime. | | `@objectstack/driver-memory` | In-memory database driver for development. Swap for SQLite / Postgres / MongoDB in production — [no code changes](/docs/data-modeling/drivers). | | `@objectstack/plugin-hono-server` | The HTTP server that mounts the generated REST API. | +| `@objectstack/connector-rest` · `-openapi` · `-mcp` | The three generic connector executors — register the `rest` / `openapi` / `mcp` provider factories so declarative `connectors:` entries materialize ([ADR-0097](/docs/automation/flows)). | | `@objectstack/cli` *(dev)* | The `os` / `objectstack` CLI: `dev`, `validate`, `build`, `start`. | ## 3. Run it diff --git a/packages/create-objectstack/src/templates/blank/README.md b/packages/create-objectstack/src/templates/blank/README.md index 0b7746a6d1..06ba2d7613 100644 --- a/packages/create-objectstack/src/templates/blank/README.md +++ b/packages/create-objectstack/src/templates/blank/README.md @@ -26,6 +26,32 @@ curl -b cookies.txt "http://localhost:3000/api/v1/data/" - `objectstack.config.ts` — environment manifest (objects, API, plugins) - `src/objects/` — object definitions (one file per object) +## Connectors (default providers) + +`objectstack.config.ts` wires the three **generic connector executors**, so you +can call an external system from a flow as pure metadata — no host code: + +| Provider | Package | Use for | +|:---|:---|:---| +| `rest` | `@objectstack/connector-rest` | Any JSON/HTTP REST API | +| `openapi` | `@objectstack/connector-openapi` | An API described by an OpenAPI document | +| `mcp` | `@objectstack/connector-mcp` | A Model Context Protocol server | + +Add a `connectors:` entry that names one of these `provider`s and the +`automation` capability materializes it into a live, dispatchable connector at +boot (ADR-0097); a flow's `connector_action` node then calls it. To add a brand +connector (e.g. Slack), install its package and add `new ConnectorSlackPlugin()` +to `plugins:`; to drop a provider, remove its plugin. + +> **Security — declarative MCP over stdio.** An `mcp` connector whose transport +> spawns a local process (`stdio`) is denied by default, because the command +> comes from metadata. Opt in per host with +> `new ConnectorMcpPlugin({ declarativeStdio: ['node'] })`; `http` transports +> need no opt-in. + +See [Automation → Flows](https://docs.objectstack.ai/docs/automation/flows) for +the full connector and `connector_action` guide. + ## Verify your changes After editing any metadata, run: diff --git a/packages/create-objectstack/src/templates/blank/objectstack.config.ts b/packages/create-objectstack/src/templates/blank/objectstack.config.ts index 2927273a32..9576fea072 100644 --- a/packages/create-objectstack/src/templates/blank/objectstack.config.ts +++ b/packages/create-objectstack/src/templates/blank/objectstack.config.ts @@ -1,4 +1,7 @@ import { defineStack } from '@objectstack/spec'; +import { ConnectorRestPlugin } from '@objectstack/connector-rest'; +import { ConnectorOpenApiPlugin } from '@objectstack/connector-openapi'; +import { ConnectorMcpPlugin } from '@objectstack/connector-mcp'; import * as objects from './src/objects/index.js'; export default defineStack({ @@ -15,5 +18,26 @@ export default defineStack({ // scripts/sync-template-versions.mjs. engines: { protocol: '^15' }, }, + + // `automation` backs flow execution and, per ADR-0097, materializes any + // declarative `connectors:` entry into a live, dispatchable connector at boot. + // The connector executors below register their provider factories with it — + // without `automation` loaded they have nowhere to register and boot fails, so + // keep this capability whenever `plugins:` lists a connector. + requires: ['automation'], + + // Generic connector executors (ADR-0022/0023/0024 + ADR-0097), default-present + // so you can add a `connectors:` entry naming `provider: 'rest' | 'openapi' | + // 'mcp'` and have it materialize with zero host code. Zero-arg = contribute the + // provider factory only. Brand connectors (Slack, …) stay marketplace/opt-in. + // Security (#3055): a declarative `mcp` stdio transport spawns a local process + // from metadata and is denied by default — opt in per host with + // `new ConnectorMcpPlugin({ declarativeStdio: [''] })`. + plugins: [ + new ConnectorRestPlugin(), + new ConnectorOpenApiPlugin(), + new ConnectorMcpPlugin(), + ], + objects: Object.values(objects), }); diff --git a/packages/create-objectstack/src/templates/blank/package.json b/packages/create-objectstack/src/templates/blank/package.json index 564b9efae0..5a1faddbd3 100644 --- a/packages/create-objectstack/src/templates/blank/package.json +++ b/packages/create-objectstack/src/templates/blank/package.json @@ -14,7 +14,10 @@ "@objectstack/spec": "^15.0.0", "@objectstack/runtime": "^15.0.0", "@objectstack/driver-memory": "^15.0.0", - "@objectstack/plugin-hono-server": "^15.0.0" + "@objectstack/plugin-hono-server": "^15.0.0", + "@objectstack/connector-rest": "^15.0.0", + "@objectstack/connector-openapi": "^15.0.0", + "@objectstack/connector-mcp": "^15.0.0" }, "devDependencies": { "@objectstack/cli": "^15.0.0",