Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .dagger/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,21 @@ export class AtomicServer {
`--test-threads ${this.hostKnobs.nextestTestThreads} ` +
`--retries ${this.hostKnobs.nextestRetries}`,
])
// The `plugin-routes` feature (#1711) is off in every release build,
// so gated code would rot unseen. Its tests run here, scoped to
// `atomic-server` (the only crate with the feature); the
// `release_feature_sets_exclude_plugin_routes` unit test above, in
// the default pass, fails if a release feature set ever turns it on.
.withExec([
'sh',
'-c',
'export PATH="/opt/cargo-bin/bin:$PATH" && ' +
'cargo nextest run --locked -p atomic-server --lib ' +
'--no-default-features --features light,wasm-plugins,plugin-routes ' +
`--build-jobs ${this.hostKnobs.nextestBuildJobs} ` +
`--test-threads ${this.hostKnobs.nextestTestThreads} ` +
`--retries ${this.hostKnobs.nextestRetries}`,
])
.stdout()
);
}
Expand Down Expand Up @@ -1622,6 +1637,20 @@ export class AtomicServer {
'--features',
'light,wasm-plugins',
])
// Gated `plugin-routes` code (#1711) is in no release build; lint it
// here so it doesn't rot. Only `atomic-server` has the feature.
.withExec([
'cargo',
'clippy',
'--locked',
'-p',
'atomic-server',
'--no-deps',
'--all-targets',
'--no-default-features',
'--features',
'light,wasm-plugins,plugin-routes',
])
.stdout();
}

Expand Down
11 changes: 11 additions & 0 deletions TESTING_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,17 @@ on its first render without fetching again; `helpers/integrationVisibility.test.
stored preferences. `integration-visibility.spec.ts` checks no "Show API
plugins" toggle is offered.

Plugin-routes gates (#1711): `server/src/plugin_routes.rs` unit tests cover the
three levels, the startup refusals (no `plugin-routes` feature; listeners or
sidecars below `read-write`), routes-origin validation, the catalog report,
and that no release feature set (nor atomic.place's) turns the feature on.
`config.rs` tests parse the flag and, in child processes, the env var;
`tests/it/server_cli.rs` checks the binary exits non-zero on
`ATOMIC_PLUGIN_ROUTES=read-only` without the feature. CI runs the lib tests
and clippy once more with `--features plugin-routes`. Not covered: no e2e
build has the feature yet (nothing is served behind it before #1714), and no
UI reads `hostFeatures` yet.

The data-browser no longer connects or syncs LocalThought platforms: that code
was removed, and plugins will run in their own iframe and make proxy calls
through the host (#1624). Nothing in this repo tests a LocalThought connection.
Expand Down
14 changes: 13 additions & 1 deletion browser/data-browser/src/routes/IntegrationStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ interface Listing {
world: string | null;
}

/**
* `/plugin-catalog` answers `{ entries, hostFeatures }`; servers before the
* plugin-routes gates (#1711) answered the bare array.
*/
function catalogListings(body: unknown): Listing[] {
if (Array.isArray(body)) return body as Listing[];

const entries = (body as { entries?: unknown } | null)?.entries;

return Array.isArray(entries) ? (entries as Listing[]) : [];
}

export const IntegrationStoreRoute = createRoute({
getParentRoute: () => appRoute,
path: pathNames.integrations,
Expand Down Expand Up @@ -183,7 +195,7 @@ function IntegrationStore(): React.JSX.Element {
void fetch(`${serverUrl}/plugin-catalog`, { signal: controller.signal })
.then(async response => {
if (!response.ok) throw new Error(await response.text());
const entries = await response.json();
const entries = catalogListings(await response.json());
if (!controller.signal.aborted) setListings(entries);
})
.catch(reason => {
Expand Down
41 changes: 26 additions & 15 deletions browser/e2e/tests/integration-visibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,37 @@ test('experimental plugins default off and the preference survives reload', asyn
catalogRequests.push(route.request().url());
await route.fulfill({
// The shape `/plugin-catalog` actually answers with: one flat object per
// Listing, as `plugin_release::catalog` builds it. The nested
// Listing in `entries`, as `plugin_release::catalog` builds it. The nested
// `{ metadata, verification }` this used to send is the *publish*
// payload, and reading `entry.domains` off it threw
// "domains is not iterable" out of the store's filter, which took the
// whole page down with an error boundary instead of rendering anything.
json: [
{
subject: 'https://example.com/listings/fixture',
name: 'Community fixture',
emoji: null,
description: 'Test listing',
publisher: 'test',
domains: [],
standards: [],
release: 'https://example.com/releases/fixture',
releaseId: 'fixture-release',
runtime: null,
world: null,
json: {
entries: [
{
subject: 'https://example.com/listings/fixture',
name: 'Community fixture',
emoji: null,
description: 'Test listing',
publisher: 'test',
domains: [],
standards: [],
release: 'https://example.com/releases/fixture',
releaseId: 'fixture-release',
runtime: null,
world: null,
},
],
hostFeatures: {
pluginRoutes: {
compiled: false,
level: 'off',
routesOrigin: null,
listeners: [],
sidecars: [],
},
},
],
},
});
});
await page.goto(new URL('/app/integrations', page.url()).href);
Expand Down
40 changes: 40 additions & 0 deletions docs/src/atomicserver/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,46 @@ That build needs `protoc` (`protobuf-compiler`) available. It does not currently

With such a build, pass **`--enable-vector-index`** (or set **`ATOMIC_ENABLE_VECTOR_INDEX`**) to turn it on, since loading embedding models and indexing every write has a real performance cost. Passing the flag to a build that lacks the feature logs a warning and does nothing. Once enabled, the server runs [fastembed](https://github.com/Anush008/fastembed-rs) locally by default. To use [OpenRouter](https://openrouter.ai/) embeddings instead, pass **`--openrouter-api-key`** and **`--openrouter-embedding-model`** (or set **`OPENROUTER_API_KEY`** and **`OPENROUTER_EMBEDDING_MODEL`** in your environment or `.env`). **`--openrouter-embedding-dimensions`** / **`OPENROUTER_EMBEDDING_DIMENSIONS`** is optional (some models ignore it). **`--gpu-indexing`** / **`ATOMIC_GPU_INDEXING`** uses GPU acceleration for the default local embedding and reranker, not for OpenRouter.

## Plugin public endpoints (opt-in)

Some plugins want to answer requests from other servers: a WebFinger or
ActivityPub endpoint, a `/.well-known/` name, a port for a sync protocol. That
changes what your server is, so it needs three separate yeses:

1. **Built in.** The binary was compiled with the `plugin-routes` feature. The
official release binaries, Docker images and the default
`cargo install atomic-server` build leave it out, and atomic.place never
includes it:

```sh
cargo install atomic-server --features plugin-routes
```

2. **Switched on.** Pass **`--plugin-routes <level>`** (or set
**`ATOMIC_PLUGIN_ROUTES`**):
- `off` (default): nothing is exposed, even with the feature built in.
- `read-only`: anonymous `GET`/`HEAD` routes and `/.well-known/` claims. No
inbound request can write data or make the server send a request.
- `read-write`: also routes that accept writes, host-held keys and tokens,
deliveries, listeners and sidecars.
3. **Approved per plugin.** Installing such a plugin shows every public
endpoint it opens, and someone who may install plugins has to approve it.

Setting `--plugin-routes` to anything but `off`, or any of the options below,
on a build without the feature stops the server at startup with a message
telling you to rebuild with `--features plugin-routes`. It is never silently
ignored.

| Option | Env var | What it does |
| --- | --- | --- |
| `--routes-origin` | `ATOMIC_ROUTES_ORIGIN` | A dedicated origin, e.g. `https://routes.example.net`, where each plugin installation gets its own subdomain. It has to be separate from the API, drive and website domains (`http://routes.localhost:PORT` works in development). Without it, plugin routes are only served under `/_routes/` on each drive, and the server says so at startup. |
| `--plugin-listeners` | `ATOMIC_PLUGIN_LISTENERS` | Ports you bind for server-extension plugins, as `name:port,...`, e.g. `willow-wgps:4455`. Needs `read-write`. |
| `--plugin-sidecars` | `ATOMIC_PLUGIN_SIDECARS` | Loopback daemons plugins may call, as `name=http://127.0.0.1:port,...`, e.g. `pds=http://127.0.0.1:2583`. Only loopback addresses are accepted. Needs `read-write`. |

Clients see what the server allows in `hostFeatures.pluginRoutes` of
`GET /plugin-catalog`: `compiled`, `level`, `routesOrigin` and the names of
listeners and sidecars (never their ports or URLs).

## Running using a tunneling service (easy mode)

If you want to make your -server available on the web, but don't want (or cannot) deal with setting up port-forwarding and DNS, you can use a tunneling service.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ For more information on how to create a plugin, see [Creating Plugins](plugins/c
Three classes describe a plugin's life on a server:

- A **Release** is an immutable, content-addressed package: JS source (`atomic-js/1`) or a WASM zip (`wasip2/1`), its manifest, and a `releaseId` (`blake3:…` over every field). It lives at `<server>/releases/<id>` on the server it was published to, and can be fetched from any other server.
- A **Listing** is a marketplace entry pointing at a Release: name, emoji, description, domains, standards. Every Listing the public can read on a server is part of that server's Store; `GET /plugin-catalog` returns them. Any drive of Listings is a marketplace.
- A **Listing** is a marketplace entry pointing at a Release: name, emoji, description, domains, standards. Every Listing the public can read on a server is part of that server's Store; `GET /plugin-catalog` returns them as `{ "entries": [...], "hostFeatures": {...} }`, where `hostFeatures.pluginRoutes` says whether this server lets plugins open public endpoints (see [Plugin public endpoints](atomicserver/installation.md#plugin-public-endpoints-opt-in)). Any drive of Listings is a marketplace.
- An **Installation** is one installed Release on one Drive. It pins the `release` URL and its `releaseId`, holds the `grants` you approved, your `config`, and an `installationStatus` (`draft`, `active`, `paused`, `revoked`). Committing it as `active` is the single install trigger for both runtimes; `paused` stops the plugin without uninstalling it, keeping its files, its config and the agent it signs as; `revoked` or destroying the resource uninstalls it and retires that agent. Updating a plugin means pointing the same Installation at a newer Release, which keeps everything the plugin already owns.

## Installing a plugin
Expand Down
7 changes: 7 additions & 0 deletions server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,13 @@ telemetry = [
img = ["webp", "image"]
https = ["rustls", "instant-acme", "rcgen", "rustls-pemfile"]
wasm-plugins = ["wasmtime", "wasmtime-wasi", "wasmtime-wasi-http", "zip"]
# Lets installed plugins open public endpoints (routes, `/.well-known/`
# claims, listeners, sidecars), once the operator also passes
# `--plugin-routes read-only|read-write`. Deliberately NOT in `default`,
# `light` or any release feature set: atomic.place builds must never contain
# it. `plugin_routes::tests::release_feature_sets_exclude_plugin_routes`
# fails if one does.
plugin-routes = ["wasm-plugins"]
image = ["dep:image"]
tonic = ["dep:tonic"]
tracing-opentelemetry = ["dep:tracing-opentelemetry"]
Expand Down
1 change: 1 addition & 0 deletions server/src/bin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod invite_token;
mod jsonerrors;
mod metrics;
mod node_key;
mod plugin_routes;
pub mod plugins;
mod rate_limit;
mod routes;
Expand Down
Loading
Loading